Stream comes up!
This commit is contained in:
@ -51,7 +51,7 @@ export const FileMachineProvider = ({
|
||||
commandBarSend({ type: 'Close' })
|
||||
navigate(
|
||||
`${paths.FILE}/${encodeURIComponent(
|
||||
context.selectedDirectory + sep() + event.data.name
|
||||
context.selectedDirectory + window.electron.path.sep + event.data.name
|
||||
)}`
|
||||
)
|
||||
} else if (
|
||||
@ -87,7 +87,7 @@ export const FileMachineProvider = ({
|
||||
services: {
|
||||
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
|
||||
const newFiles = isDesktop()
|
||||
? (await getProjectInfo(context.project.path)).children
|
||||
? (await getProjectInfo(context.project.file.path)).children
|
||||
: []
|
||||
return {
|
||||
...context.project,
|
||||
@ -104,7 +104,7 @@ export const FileMachineProvider = ({
|
||||
} else {
|
||||
createdPath =
|
||||
context.selectedDirectory.path +
|
||||
sep() +
|
||||
window.electron.path.sep +
|
||||
createdName +
|
||||
(createdName.endsWith(FILE_EXT) ? '' : FILE_EXT)
|
||||
await create(createdPath)
|
||||
@ -169,7 +169,7 @@ export const FileMachineProvider = ({
|
||||
file?.path.includes(event.data.path)) &&
|
||||
project?.path
|
||||
) {
|
||||
navigate(paths.FILE + '/' + encodeURIComponent(project.path))
|
||||
navigate(paths.FILE + '/' + encodeURIComponent(project.file.path))
|
||||
}
|
||||
|
||||
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
||||
|
@ -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.path, '.')}")\n` +
|
||||
`import("${fileOrDir.path.replace(project.file.path, '.')}")\n` +
|
||||
codeManager.code
|
||||
)
|
||||
codeManager.writeToFile()
|
||||
|
@ -52,7 +52,7 @@ function ProjectCard({
|
||||
}
|
||||
|
||||
// async function setupImageUrl() {
|
||||
// const projectImagePath = await join(project.path, PROJECT_IMAGE_NAME)
|
||||
// const projectImagePath = await join(project.file.path, PROJECT_IMAGE_NAME)
|
||||
// if (await exists(projectImagePath)) {
|
||||
// const imageData = await readFile(projectImagePath)
|
||||
// const blob = new Blob([imageData], { type: 'image/jpg' })
|
||||
@ -103,7 +103,7 @@ function ProjectCard({
|
||||
/>
|
||||
) : (
|
||||
<h3 className="font-sans relative z-0 p-2">
|
||||
{project.name?.replace(FILE_EXT, '')}
|
||||
{project.file.name?.replace(FILE_EXT, '')}
|
||||
</h3>
|
||||
)}
|
||||
<span className="px-2 text-chalkboard-60 text-xs">
|
||||
@ -169,11 +169,11 @@ function ProjectCard({
|
||||
onDismiss={() => setIsConfirmingDelete(false)}
|
||||
>
|
||||
<p className="my-4">
|
||||
This will permanently delete "{project.name || 'this file'}
|
||||
This will permanently delete "{project.file.name || 'this file'}
|
||||
".
|
||||
</p>
|
||||
<p className="my-4">
|
||||
Are you sure you want to delete "{project.name || 'this file'}
|
||||
Are you sure you want to delete "{project.file.name || 'this file'}
|
||||
"? This action cannot be undone.
|
||||
</p>
|
||||
</DeleteConfirmationDialog>
|
||||
|
@ -24,7 +24,7 @@ export const ProjectCardRenameForm = forwardRef(
|
||||
required
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
defaultValue={project.name}
|
||||
defaultValue={project.file.name}
|
||||
ref={ref}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
|
@ -5,7 +5,6 @@ import { paths } from 'lib/paths'
|
||||
import { isDesktop } from '../lib/isDesktop'
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Fragment, useMemo } from 'react'
|
||||
import { sep } from '@tauri-apps/api/path'
|
||||
import { Logo } from './Logo'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
@ -36,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.name : APP_NAME}
|
||||
{project?.name ? project.file.name : APP_NAME}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -208,12 +207,12 @@ function ProjectMenuPopover({
|
||||
<div className="flex flex-col items-start py-0.5">
|
||||
<span className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block">
|
||||
{isDesktop() && file?.name
|
||||
? file.name.slice(file.name.lastIndexOf(sep()) + 1)
|
||||
? file.name.slice(file.name.lastIndexOf(window.electron.path.sep) + 1)
|
||||
: APP_NAME}
|
||||
</span>
|
||||
{isDesktop() && project?.name && (
|
||||
<span className="hidden text-xs text-chalkboard-70 dark:text-chalkboard-40 whitespace-nowrap lg:block">
|
||||
{project.name}
|
||||
{project.file.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
@ -46,7 +46,7 @@ export const AllSettingsFields = forwardRef(
|
||||
location.pathname
|
||||
.replace(paths.FILE + '/', '')
|
||||
.replace(paths.SETTINGS, '')
|
||||
.slice(0, decodeURI(location.pathname).lastIndexOf(sep()))
|
||||
.slice(0, decodeURI(location.pathname).lastIndexOf(window.electron.path.sep))
|
||||
)
|
||||
: undefined
|
||||
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { Platform, platform } from '@tauri-apps/plugin-os'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export type Platform = 'macos' | 'windows' | 'linux'
|
||||
|
||||
export default function usePlatform() {
|
||||
const [platformName, setPlatformName] = useState<Platform | ''>('')
|
||||
|
||||
useEffect(() => {
|
||||
async function getPlatform() {
|
||||
setPlatformName(await platform())
|
||||
setPlatformName(window.electron.platform)
|
||||
}
|
||||
|
||||
if (isDesktop()) {
|
||||
|
@ -3,7 +3,6 @@
|
||||
// This prevents re-renders of the codemirror editor, when typing.
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { writeTextFile } from '@tauri-apps/plugin-fs'
|
||||
import toast from 'react-hot-toast'
|
||||
import { editorManager } from 'lib/singletons'
|
||||
import { Annotation, Transaction } from '@codemirror/state'
|
||||
@ -120,7 +119,7 @@ export default class CodeManager {
|
||||
// Wait one event loop to give a chance for params to be set
|
||||
// Save the file to disk
|
||||
this._currentFilePath &&
|
||||
writeTextFile(this._currentFilePath, this.code).catch((err) => {
|
||||
window.electron.writeFile(this._currentFilePath, this.code ?? '').catch((err) => {
|
||||
// TODO: add tracing per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
||||
console.error('error saving file', err)
|
||||
toast.error('Error saving file, please check file permissions')
|
||||
|
@ -15,7 +15,7 @@ class FileSystemManager {
|
||||
}
|
||||
|
||||
async join(dir: string, path: string): Promise<string> {
|
||||
return window.electron.ipcRenderer.invoke('join', [dir, path])
|
||||
return Promise.resolve(window.electron.path.join(dir, path))
|
||||
}
|
||||
|
||||
async readFile(path: string): Promise<Uint8Array | void> {
|
||||
@ -33,7 +33,7 @@ class FileSystemManager {
|
||||
return Promise.reject(new Error(`Error reading file: ${error}`))
|
||||
})
|
||||
.then((file) => {
|
||||
return window.electron.ipcRenderer.invoke('readFile', [filepath])
|
||||
return window.electron.readFile(filepath)
|
||||
})
|
||||
}
|
||||
|
||||
@ -51,8 +51,14 @@ class FileSystemManager {
|
||||
.catch((error) => {
|
||||
return Promise.reject(new Error(`Error checking file exists: ${error}`))
|
||||
})
|
||||
.then((file) => {
|
||||
return window.electron.ipcRenderer.invoke('exists', [file])
|
||||
.then(async (file) => {
|
||||
try { await window.electron.stat(file) }
|
||||
catch (e) {
|
||||
if (e === 'ENOENT') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
@ -71,8 +77,7 @@ class FileSystemManager {
|
||||
return Promise.reject(new Error(`Error joining dir: ${error}`))
|
||||
})
|
||||
.then((filepath) => {
|
||||
return window.electron.ipcRenderer
|
||||
.invoke('readdir', [filepath])
|
||||
return window.electron.readdir(filepath)
|
||||
.catch((error) => {
|
||||
return Promise.reject(new Error(`Error reading dir: ${error}`))
|
||||
})
|
||||
|
101
src/lang/wasm.ts
101
src/lang/wasm.ts
@ -19,6 +19,11 @@ import init, {
|
||||
parse_project_route,
|
||||
serialize_project_settings,
|
||||
} from '../wasm-lib/pkg/wasm_lib'
|
||||
import {
|
||||
configurationToSettingsPayload,
|
||||
projectConfigurationToSettingsPayload,
|
||||
SaveSettingsPayload,
|
||||
} from 'lib/settings/settingsUtils'
|
||||
import { KCLError } from './errors'
|
||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||
import { EngineCommandManager } from './std/engineConnection'
|
||||
@ -559,22 +564,26 @@ export function tomlStringify(toml: any): string | Error {
|
||||
return toml_stringify(JSON.stringify(toml))
|
||||
}
|
||||
|
||||
export function defaultAppSettings(): Configuration {
|
||||
return default_app_settings()
|
||||
export function defaultAppSettings(): Partial<SaveSettingsPayload> {
|
||||
// Immediately go from Configuration -> Partial<SaveSettingsPayload>
|
||||
// The returned Rust type is Configuration but it's a lie. Every
|
||||
// property in that returned object is optional. The Partial<T> essentially
|
||||
// brings that type in-line with that definition.
|
||||
return configurationToSettingsPayload(default_app_settings())
|
||||
}
|
||||
|
||||
export function parseAppSettings(toml: string): Configuration | Error {
|
||||
return parse_app_settings(toml)
|
||||
export function parseAppSettings(toml: string): Partial<SaveSettingsPayload> {
|
||||
return configurationToSettingsPayload(parse_app_settings(toml))
|
||||
}
|
||||
|
||||
export function defaultProjectSettings(): ProjectConfiguration | Error {
|
||||
return default_project_settings()
|
||||
export function defaultProjectSettings(): Partial<SaveSettingsPayload> {
|
||||
return projectConfigurationToSettingsPayload(default_project_settings())
|
||||
}
|
||||
|
||||
export function parseProjectSettings(
|
||||
toml: string
|
||||
): ProjectConfiguration | Error {
|
||||
return parse_project_settings(toml)
|
||||
): Partial<SaveSettingsPayload> {
|
||||
return projectConfigurationToSettingsPayload(parse_project_settings(toml))
|
||||
}
|
||||
|
||||
export function parseProjectRoute(
|
||||
@ -583,79 +592,3 @@ export function parseProjectRoute(
|
||||
): ProjectRoute | Error {
|
||||
return parse_project_route(JSON.stringify(configuration), route_str)
|
||||
}
|
||||
|
||||
const DEFAULT_HOST = 'https://api.zoo.dev'
|
||||
const SETTINGS_FILE_NAME = 'settings.toml'
|
||||
const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
|
||||
const PROJECT_FOLDER = 'zoo-modeling-app-projects'
|
||||
|
||||
const getAppSettingsFilePath = async () => {
|
||||
const appConfig = await window.electron.getPath('appData')
|
||||
const fullPath = window.electron.path.join(
|
||||
appConfig,
|
||||
window.electron.packageJson.name
|
||||
)
|
||||
try {
|
||||
await window.electron.exists(fullPath)
|
||||
} catch (e) {
|
||||
// File/path doesn't exist
|
||||
if (e.code === 'ENOENT') {
|
||||
window.electron.mkdir(fullPath, { recursive: true })
|
||||
}
|
||||
}
|
||||
return window.electron.path.join(fullPath, SETTINGS_FILE_NAME)
|
||||
}
|
||||
|
||||
const getInitialDefaultDir = async () => {
|
||||
const dir = await window.electron.getPath('documents')
|
||||
return window.electron.path.join(dir, PROJECT_FOLDER)
|
||||
}
|
||||
|
||||
export const readAppSettingsFile = async () => {
|
||||
let settingsPath = await getAppSettingsFilePath()
|
||||
try {
|
||||
await window.electron.exists(settingsPath)
|
||||
} catch (e) {
|
||||
if (e === 'ENOENT') {
|
||||
const config = defaultAppSettings()
|
||||
config.settings.project.directory = await getInitialDefaultDir()
|
||||
console.log(config)
|
||||
return config
|
||||
}
|
||||
}
|
||||
const configToml = await window.electron.readFile(settingsPath)
|
||||
const configObj = parseProjectSettings(configStr)
|
||||
return configObj
|
||||
}
|
||||
|
||||
export const writeAppSettingsFile = async () => {
|
||||
debugger
|
||||
console.log("STUB")
|
||||
}
|
||||
|
||||
let appStateStore = undefined
|
||||
|
||||
export const getState = async (): Promise<ProjectState | undefined> => {
|
||||
return Promise.resolve(appStateStore)
|
||||
}
|
||||
|
||||
export const setState = async (
|
||||
state: ProjectState | undefined
|
||||
): Promise<void> => {
|
||||
appStateStore = state
|
||||
}
|
||||
|
||||
const initializeProjectDirectory = () => {
|
||||
debugger
|
||||
console.log('STUB')
|
||||
}
|
||||
|
||||
export const login = () => {
|
||||
debugger
|
||||
console.log('STUB')
|
||||
}
|
||||
|
||||
export const getUser = (token: string, host: string) => {
|
||||
debugger
|
||||
console.log("STUB")
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Models } from '@kittycad/lib/dist/types/src'
|
||||
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'
|
||||
@ -8,14 +8,23 @@ import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
||||
import { components } from './machine-api'
|
||||
import { isDesktop } from './isDesktop'
|
||||
|
||||
export {
|
||||
readAppSettingsFile,
|
||||
writeAppSettingsFile,
|
||||
getState,
|
||||
setState,
|
||||
getUser,
|
||||
login,
|
||||
import {
|
||||
defaultAppSettings,
|
||||
tomlStringify,
|
||||
parseAppSettings,
|
||||
parseProjectSettings,
|
||||
} from 'lang/wasm'
|
||||
export {
|
||||
parseProjectRoute,
|
||||
} from 'lang/wasm'
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsUtils'
|
||||
|
||||
const DEFAULT_HOST = 'https://api.zoo.dev'
|
||||
const SETTINGS_FILE_NAME = 'settings.toml'
|
||||
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<{
|
||||
@ -34,15 +43,7 @@ export async function renameProjectDirectory(
|
||||
projectPath: string,
|
||||
newName: string
|
||||
): Promise<string> {
|
||||
return window.electron.ipcRenderer.invoke<string>(
|
||||
'rename_project_directory',
|
||||
{ projectPath, newName }
|
||||
)
|
||||
}
|
||||
|
||||
// Get the initial default dir for holding all projects.
|
||||
export async function getInitialDefaultDir(): Promise<string> {
|
||||
return window.electron.getInitialDefaultDir()
|
||||
debugger
|
||||
}
|
||||
|
||||
export async function showInFolder(path: string | undefined): Promise<void> {
|
||||
@ -50,15 +51,15 @@ export async function showInFolder(path: string | undefined): Promise<void> {
|
||||
console.error('path is undefined cannot call desktop showInFolder')
|
||||
return
|
||||
}
|
||||
return window.electron.ipcRenderer.invoke('show_in_folder', { path })
|
||||
debugger
|
||||
}
|
||||
|
||||
export async function initializeProjectDirectory(
|
||||
config: Configuration
|
||||
export async function ensureProjectDirectoryExists(
|
||||
config: Partial<SaveSettingsPayload>
|
||||
): Promise<string | undefined> {
|
||||
const projectDir = config.settings.project.directory
|
||||
const projectDir = config.app.projectDirectory
|
||||
try {
|
||||
await window.electron.exists(projectDir)
|
||||
await window.electron.stat(projectDir)
|
||||
} catch (e) {
|
||||
if (e === 'ENOENT') {
|
||||
window.electron.mkdir(projectDir, { recursive: true }, (e) => {
|
||||
@ -73,73 +74,360 @@ export async function initializeProjectDirectory(
|
||||
export async function createNewProjectDirectory(
|
||||
projectName: string,
|
||||
initialCode?: string,
|
||||
configuration?: Configuration
|
||||
configuration?: Partial<SaveSettingsPayload>
|
||||
): Promise<Project> {
|
||||
if (!configuration) {
|
||||
configuration = await readAppSettingsFile()
|
||||
}
|
||||
return window.electron.ipcRenderer.invoke('create_new_project_directory', {
|
||||
configuration,
|
||||
projectName,
|
||||
initialCode,
|
||||
})
|
||||
|
||||
const mainDir = await ensureProjectDirectoryExists(configuration)
|
||||
|
||||
if (!projectName) {
|
||||
return Promise.reject('Project name cannot be empty.')
|
||||
}
|
||||
|
||||
const projectDir = window.electron.path.join(mainDir, projectName)
|
||||
|
||||
try {
|
||||
await window.electron.stat(projectDir)
|
||||
} catch (e) {
|
||||
if (e === 'ENOENT') {
|
||||
window.electron.mkdir(projectDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
const projectFile = window.electron.path.join(
|
||||
projectDir,
|
||||
DEFAULT_PROJECT_KCL_FILE
|
||||
)
|
||||
await window.electron.writeFile(projectFile, initialCode ?? '')
|
||||
|
||||
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 */,
|
||||
kcl_file_count: 1,
|
||||
directory_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export async function listProjects(
|
||||
configuration?: Configuration
|
||||
configuration?: Partial<SaveSettingsPayload>
|
||||
): Promise<Project[]> {
|
||||
const projectDir = await initializeProjectDirectory(configuration)
|
||||
const projectDir = await ensureProjectDirectoryExists(configuration)
|
||||
const projects = []
|
||||
const entries = await window.electron.readdir(projectDir)
|
||||
for (let entry of entries) {
|
||||
// Ignore directories
|
||||
console.log(entry)
|
||||
const projectPath = window.electron.path.join(projectDir, entry)
|
||||
// if it's not a directory ignore.
|
||||
const isDirectory = await window.electron.statIsDirectory(projectPath)
|
||||
if (!isDirectory) { continue }
|
||||
|
||||
const project = await getProjectInfo(projectPath)
|
||||
// Needs at least one file to be added to the projects list
|
||||
if (project.kcl_file_count === 0) { continue }
|
||||
projects.push(project)
|
||||
}
|
||||
return projects
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
const collectAllFilesRecursiveFrom = async (path: string) => {
|
||||
// Make sure the filesystem object exists.
|
||||
try { await window.electron.stat(path) }
|
||||
catch (e) {
|
||||
if (e === 'ENOENT') {
|
||||
return Promise.reject(new Error(`Directory ${path} does not exist`))
|
||||
}
|
||||
}
|
||||
|
||||
// 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`));
|
||||
}
|
||||
|
||||
const pathParts = path.split('/')
|
||||
let entry = /* FileEntry */ {
|
||||
name: pathParts.slice(-1)[0],
|
||||
path: pathParts.slice(0, -1).join('/'),
|
||||
children: [],
|
||||
}
|
||||
|
||||
const children = []
|
||||
|
||||
const entries = await window.electron.readdir(path)
|
||||
|
||||
for (let e of entries) {
|
||||
// ignore hidden files and directories (starting with a dot)
|
||||
if (e.indexOf('.') === 0) { continue }
|
||||
|
||||
const ePath = window.electron.path.join(path, e)
|
||||
const isEDir = await window.electron.statIsDirectory(ePath)
|
||||
|
||||
if (isEDir) {
|
||||
const subChildren = await collectAllFilesRecursiveFrom(ePath)
|
||||
children.push(subChildren)
|
||||
} else {
|
||||
if (!isRelevantFile(ePath)) { continue }
|
||||
children.push(/* FileEntry */ {
|
||||
name: e,
|
||||
path: ePath.split('/').slice(0, -1).join('/'),
|
||||
children: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// We don't set this to none if there are no children, because it's a directory.
|
||||
entry.children = children
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
const getDefaultKclFileForDir = async (projectDir, file) => {
|
||||
// Make sure the dir is a directory.
|
||||
const isFileEntryDir = await window.electron.statIsDirectory(file.path)
|
||||
if (!isFileEntryDir) {
|
||||
return Promise.reject(new Error(`Path ${file.path} is not a directory`))
|
||||
}
|
||||
|
||||
let defaultFilePath = window.electron.path.join(file.path, DEFAULT_PROJECT_KCL_FILE)
|
||||
try { await window.eletron.stat(defaultFilePath) }
|
||||
catch (e) {
|
||||
if (e === 'ENOENT') {
|
||||
// Find a kcl file in the directory.
|
||||
if (file.children) {
|
||||
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()) {
|
||||
// Recursively find a kcl file in the directory.
|
||||
return getDefaultKclFileForDir(entry.path, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find a kcl file, create one.
|
||||
await window.electron.writeFile(defaultFilePath, '')
|
||||
return defaultFilePath
|
||||
}
|
||||
|
||||
const kclFileCount = (file /* fileEntry */) => {
|
||||
let count = 0;
|
||||
if (file.children) {
|
||||
for (let entry of file.children) {
|
||||
if (entry.name.endsWith(".kcl")) {
|
||||
count += 1
|
||||
} else {
|
||||
count += kclFileCount(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
/// Populate the number of directories in the project.
|
||||
const directoryCount = (file /* FileEntry */) => {
|
||||
let count = 0
|
||||
if (file.children) {
|
||||
for (let entry of file.children) {
|
||||
count += 1
|
||||
directoryCount(entry)
|
||||
}
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
|
||||
export async function getProjectInfo(
|
||||
projectPath: string,
|
||||
configuration?: Configuration
|
||||
): Promise<Project> {
|
||||
if (!configuration) {
|
||||
configuration = await readAppSettingsFile()
|
||||
}
|
||||
return window.electron.ipcRenderer.invoke('get_project_info', {
|
||||
configuration,
|
||||
projectPath,
|
||||
})
|
||||
}
|
||||
// Check the directory.
|
||||
try { await window.electron.stat(projectPath) }
|
||||
catch (e) {
|
||||
if (e === 'ENOENT') {
|
||||
return Promise.reject(new Error(`Project directory does not exist: ${project_path}`));
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseProjectRoute(
|
||||
configuration: Configuration,
|
||||
route: string
|
||||
): Promise<ProjectRoute> {
|
||||
return window.electron.ipcRenderer.invoke('parse_project_route', {
|
||||
configuration,
|
||||
route,
|
||||
})
|
||||
// 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}`));
|
||||
}
|
||||
|
||||
let walked = await collectAllFilesRecursiveFrom(projectPath)
|
||||
let default_file = await getDefaultKclFileForDir(projectPath, walked)
|
||||
|
||||
let project = /* FileEntry */ {
|
||||
file: walked,
|
||||
metadata: undefined,
|
||||
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[]> {
|
||||
return window.electron.ipcRenderer.invoke('read_dir_recursive', { path })
|
||||
}
|
||||
|
||||
// Read project settings file.
|
||||
export async function readProjectSettingsFile(
|
||||
projectPath: string
|
||||
): Promise<ProjectConfiguration> {
|
||||
return window.electron.ipcRenderer.invoke('read_project_settings_file', {
|
||||
projectPath,
|
||||
})
|
||||
debugger
|
||||
}
|
||||
|
||||
// Write project settings file.
|
||||
export async function writeProjectSettingsFile(
|
||||
projectPath: string,
|
||||
settings: ProjectConfiguration
|
||||
): Promise<void> {
|
||||
return window.electron.ipcRenderer.invoke('write_project_settings_file', {
|
||||
projectPath,
|
||||
configuration: settings,
|
||||
})
|
||||
export async function writeProjectSettingsFile({
|
||||
projectPath,
|
||||
configuration,
|
||||
}: {
|
||||
projectPath: string
|
||||
configuration: Partial<SaveSettingsPayload>
|
||||
}): Promise<void> {
|
||||
const projectSettingsFilePath = await getProjectSettingsFilePath(projectPath)
|
||||
const tomlStr = tomlStringify(configuration)
|
||||
return window.electron.writeFile(projectSettingsFilePath, tomlStr)
|
||||
}
|
||||
|
||||
const getAppSettingsFilePath = async () => {
|
||||
const appConfig = await window.electron.getPath('appData')
|
||||
const fullPath = window.electron.path.join(
|
||||
appConfig,
|
||||
window.electron.packageJson.name
|
||||
)
|
||||
try {
|
||||
await window.electron.stat(fullPath)
|
||||
} catch (e) {
|
||||
// File/path doesn't exist
|
||||
if (e.code === 'ENOENT') {
|
||||
window.electron.mkdir(fullPath, { recursive: true })
|
||||
}
|
||||
}
|
||||
return window.electron.path.join(fullPath, SETTINGS_FILE_NAME)
|
||||
}
|
||||
|
||||
const getProjectSettingsFilePath = async (projectPath: string) => {
|
||||
try {
|
||||
await window.electron.stat(projectPath)
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
window.electron.mkdir(projectPath, { recursive: true })
|
||||
}
|
||||
}
|
||||
return window.electron.path.join(projectPath, PROJECT_SETTINGS_FILE_NAME)
|
||||
}
|
||||
|
||||
export const getInitialDefaultDir = async () => {
|
||||
const dir = await window.electron.getPath('documents')
|
||||
return window.electron.path.join(dir, PROJECT_FOLDER)
|
||||
}
|
||||
|
||||
export const readProjectSettingsFile = async (projectPath: string): ProjectConfiguration => {
|
||||
let settingsPath = await getProjectSettingsFilePath(projectPath)
|
||||
|
||||
// Check if this file exists.
|
||||
try { await window.electron.stat(settingsPath) }
|
||||
catch (e) {
|
||||
if (e === 'ENOENT') {
|
||||
// Return the default configuration.
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const configToml = await window.electron.readFile(settingsPath)
|
||||
const configObj = parseProjectSettings(configToml)
|
||||
return configObj
|
||||
}
|
||||
|
||||
export const readAppSettingsFile = async () => {
|
||||
let settingsPath = await getAppSettingsFilePath()
|
||||
try {
|
||||
await window.electron.stat(settingsPath)
|
||||
} catch (e) {
|
||||
if (e === 'ENOENT') {
|
||||
const config = defaultAppSettings()
|
||||
config.app.projectDirectory = await getInitialDefaultDir()
|
||||
return config
|
||||
}
|
||||
}
|
||||
const configToml = await window.electron.readFile(settingsPath)
|
||||
const configObj = parseAppSettings(configToml)
|
||||
return configObj
|
||||
}
|
||||
|
||||
export const writeAppSettingsFile = async (
|
||||
config: Partial<SaveSettingsPayload>
|
||||
) => {
|
||||
const appSettingsFilePath = await getAppSettingsFilePath()
|
||||
const tomlStr = tomlStringify(config)
|
||||
return window.electron.writeFile(appSettingsFilePath, tomlStr)
|
||||
}
|
||||
|
||||
let appStateStore = 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 login = () => {
|
||||
console.log('STUB')
|
||||
}
|
||||
|
||||
export const getUser = async (
|
||||
token: string,
|
||||
hostname: string
|
||||
): Models['User_type'] => {
|
||||
// Use the host passed in if it's set.
|
||||
// Otherwise, use the default host.
|
||||
const host = !hostname ? DEFAULT_HOST : hostname
|
||||
|
||||
// Change the baseURL to the one we want.
|
||||
let baseurl = host
|
||||
if (!(host.indexOf('http://') === 0) && !(host.indexOf('https://') === 0)) {
|
||||
baseurl = `https://${host}`
|
||||
if (host.indexOf('localhost') === 0) {
|
||||
baseurl = `http://${host}`
|
||||
}
|
||||
}
|
||||
|
||||
// Use kittycad library to fetch the user info from /user/me
|
||||
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)
|
||||
}
|
||||
|
||||
const user = await window.electron.kittycad.users.get_user_self({
|
||||
client: { token },
|
||||
})
|
||||
return user
|
||||
}
|
||||
|
||||
|
@ -4,22 +4,46 @@ import fs from 'node:fs/promises'
|
||||
import packageJson from '../../package.json'
|
||||
|
||||
const readFile = (path: string) => fs.readFile(path, 'utf-8')
|
||||
const writeFile = (path: string, data: string) =>
|
||||
fs.writeFile(path, data, 'utf-8')
|
||||
const readdir = (path: string) => fs.readdir(path, 'utf-8')
|
||||
const exists = (path: string) =>
|
||||
new Promise((resolve, reject) =>
|
||||
fs.stat(path, (err, data) => {
|
||||
if (err) return reject(err.code)
|
||||
return resolve(data)
|
||||
})
|
||||
)
|
||||
const stat = (path: string) => fs.stat(path).catch((e) => Promise.reject(e.code))
|
||||
// Electron has behavior where it doesn't clone the prototype chain over.
|
||||
// So we need to call stat.isDirectory on this side.
|
||||
const statIsDirectory = (path: string) => stat(path).then((res) => res.isDirectory())
|
||||
const getPath = async (name: string) => ipcRenderer.invoke('app.getPath', name)
|
||||
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
readFile,
|
||||
readdir,
|
||||
path,
|
||||
exists,
|
||||
mkdir: fs.mkdir,
|
||||
getPath,
|
||||
packageJson,
|
||||
const exposeProcessEnv = (varName: string) => {
|
||||
return {
|
||||
[varName](value?: string) {
|
||||
if (value !== undefined) {
|
||||
process.env[varName] = value
|
||||
} else {
|
||||
return process.env[varName]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
import('@kittycad/lib').then((kittycad) => {
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
readFile,
|
||||
writeFile,
|
||||
readdir,
|
||||
path,
|
||||
stat,
|
||||
statIsDirectory,
|
||||
mkdir: fs.mkdir,
|
||||
getPath,
|
||||
packageJson,
|
||||
platform: process.platform,
|
||||
process: {
|
||||
// Setter/getter has to be created because
|
||||
// these are read-only over the boundary.
|
||||
env: Object.assign({}, exposeProcessEnv('BASE_URL')),
|
||||
},
|
||||
kittycad: {
|
||||
users: kittycad.users,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
@ -11,12 +11,11 @@ import {
|
||||
import { loadAndValidateSettings } from './settings/settingsUtils'
|
||||
import makeUrlPathRelative from './makeUrlPathRelative'
|
||||
import { sep } from '@tauri-apps/api/path'
|
||||
import { readTextFile } from '@tauri-apps/plugin-fs'
|
||||
import { codeManager, kclManager } from 'lib/singletons'
|
||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||
import {
|
||||
getProjectInfo,
|
||||
initializeProjectDirectory,
|
||||
ensureProjectDirectoryExists,
|
||||
listProjects,
|
||||
} from './desktop'
|
||||
import { createSettings } from './settings/initialSettings'
|
||||
@ -90,14 +89,14 @@ export const fileLoader: LoaderFunction = async ({
|
||||
if (!current_file_name || !current_file_path || !project_name) {
|
||||
return redirect(
|
||||
`${paths.FILE}/${encodeURIComponent(
|
||||
`${params.id}${isDesktop() ? sep() : '/'}${PROJECT_ENTRYPOINT}`
|
||||
`${params.id}${isDesktop() ? window.electron.path.sep : '/'}${PROJECT_ENTRYPOINT}`
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: PROJECT_ENTRYPOINT is hardcoded
|
||||
// until we support setting a project's entrypoint file
|
||||
const code = await readTextFile(current_file_path)
|
||||
const code = await window.electron.readFile(current_file_path)
|
||||
|
||||
// Update both the state and the editor's code.
|
||||
// We explicitly do not write to the file here since we are loading from
|
||||
@ -162,7 +161,7 @@ export const homeLoader: LoaderFunction = async (): Promise<
|
||||
}
|
||||
const { configuration } = await loadAndValidateSettings()
|
||||
|
||||
const projectDir = await initializeProjectDirectory(configuration)
|
||||
const projectDir = await ensureProjectDirectoryExists(configuration)
|
||||
|
||||
if (projectDir) {
|
||||
const projects = await listProjects(configuration)
|
||||
|
@ -27,7 +27,7 @@ import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||
* We do this because the JS settings type has all the fancy shit
|
||||
* for hiding and showing settings.
|
||||
**/
|
||||
function configurationToSettingsPayload(
|
||||
export function configurationToSettingsPayload(
|
||||
configuration: Configuration
|
||||
): Partial<SaveSettingsPayload> {
|
||||
return {
|
||||
@ -65,7 +65,7 @@ function configurationToSettingsPayload(
|
||||
}
|
||||
}
|
||||
|
||||
function projectConfigurationToSettingsPayload(
|
||||
export function projectConfigurationToSettingsPayload(
|
||||
configuration: ProjectConfiguration
|
||||
): Partial<SaveSettingsPayload> {
|
||||
return {
|
||||
@ -165,14 +165,12 @@ export async function loadAndValidateSettings(
|
||||
await initPromise
|
||||
|
||||
// Load the app settings from the file system or localStorage.
|
||||
const appSettings = onDesktop
|
||||
const appSettingsPayload = onDesktop
|
||||
? await readAppSettingsFile()
|
||||
: readLocalStorageAppSettingsFile()
|
||||
|
||||
if (err(appSettings)) return Promise.reject(appSettings)
|
||||
if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload)
|
||||
|
||||
// Convert the app settings to the JS settings format.
|
||||
const appSettingsPayload = configurationToSettingsPayload(appSettings)
|
||||
setSettingsAtLevel(settings, 'user', appSettingsPayload)
|
||||
|
||||
// Load the project settings if they exist
|
||||
@ -190,7 +188,7 @@ export async function loadAndValidateSettings(
|
||||
}
|
||||
|
||||
// Return the settings object
|
||||
return { settings, configuration: appSettings }
|
||||
return { settings, configuration: appSettingsPayload }
|
||||
}
|
||||
|
||||
export async function saveSettings(
|
||||
@ -207,7 +205,7 @@ export async function saveSettings(
|
||||
if (err(tomlString)) return
|
||||
|
||||
// Parse this as a Configuration.
|
||||
const appSettings = parseAppSettings(tomlString)
|
||||
const appSettings = { settings: parseAppSettings(tomlString) }
|
||||
if (err(appSettings)) return
|
||||
|
||||
const tomlString2 = tomlStringify(appSettings)
|
||||
|
@ -1,12 +1,4 @@
|
||||
let toast = undefined
|
||||
|
||||
try {
|
||||
global
|
||||
} catch (e) {
|
||||
import('react-hot-toast').then((_toast) => {
|
||||
toast = _toast
|
||||
})
|
||||
}
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
type ExcludeErr<T> = Exclude<T, Error>
|
||||
|
||||
@ -16,6 +8,7 @@ export function err<T>(value: ExcludeErr<T> | Error): value is Error {
|
||||
return false
|
||||
}
|
||||
|
||||
console.error(value)
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -146,7 +146,10 @@ async function getUser(context: UserContext) {
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((err) => console.error('error from Browser getUser', err))
|
||||
: getUserDesktop(context.token, VITE_KC_API_BASE_URL)
|
||||
: getUserDesktop(
|
||||
'f8864550-84a6-4a06-8d3f-68d29bbe5608' /* context.token */,
|
||||
VITE_KC_API_BASE_URL
|
||||
)
|
||||
|
||||
const user = await userPromise
|
||||
|
||||
|
@ -11,7 +11,7 @@ if (require('electron-squirrel-startup')) {
|
||||
}
|
||||
|
||||
const createWindow = () => {
|
||||
const mainWindow = new BrowserWindow({
|
||||
let mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
|
@ -82,7 +82,7 @@ const Home = () => {
|
||||
event: EventFrom<typeof homeMachine>
|
||||
) => {
|
||||
if (event.data && 'name' in event.data) {
|
||||
let projectPath = context.defaultDirectory + sep() + event.data.name
|
||||
let projectPath = context.defaultDirectory + window.electron.path.sep + event.data.name
|
||||
onProjectOpen(
|
||||
{
|
||||
name: event.data.name,
|
||||
@ -192,15 +192,15 @@ const Home = () => {
|
||||
new FormData(e.target as HTMLFormElement)
|
||||
)
|
||||
|
||||
if (newProjectName !== project.name) {
|
||||
if (newProjectName !== project.file.name) {
|
||||
send('Rename project', {
|
||||
data: { oldName: project.name, newName: newProjectName },
|
||||
data: { oldName: project.file.name, newName: newProjectName },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteProject(project: Project) {
|
||||
send('Delete project', { data: { name: project.name || '' } })
|
||||
send('Delete project', { data: { name: project.file.name || '' } })
|
||||
}
|
||||
|
||||
return (
|
||||
@ -296,7 +296,7 @@ const Home = () => {
|
||||
<ul className="grid w-full grid-cols-4 gap-4">
|
||||
{searchResults.sort(getSortFunction(sort)).map((project) => (
|
||||
<ProjectCard
|
||||
key={project.name}
|
||||
key={project.file.name}
|
||||
project={project}
|
||||
handleRenameProject={handleRenameProject}
|
||||
handleDeleteProject={handleDeleteProject}
|
||||
|
@ -495,7 +495,6 @@ pub fn default_app_settings() -> Result<JsValue, String> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let settings = kcl_lib::settings::types::Configuration::default();
|
||||
web_sys::console::log(&js_sys::Array::of1(&JsValue::from_str(&format!("{:?}", settings))));
|
||||
|
||||
serde_wasm_bindgen::to_value(&settings).map_err(|e| e.to_string())
|
||||
}
|
||||
|
Reference in New Issue
Block a user