Stream comes up!

This commit is contained in:
49lf
2024-07-26 15:52:35 -04:00
parent c678515c15
commit 397eb9bf5a
19 changed files with 462 additions and 221 deletions

View File

@ -51,7 +51,7 @@ export const FileMachineProvider = ({
commandBarSend({ type: 'Close' }) commandBarSend({ type: 'Close' })
navigate( navigate(
`${paths.FILE}/${encodeURIComponent( `${paths.FILE}/${encodeURIComponent(
context.selectedDirectory + sep() + event.data.name context.selectedDirectory + window.electron.path.sep + event.data.name
)}` )}`
) )
} else if ( } else if (
@ -87,7 +87,7 @@ export const FileMachineProvider = ({
services: { services: {
readFiles: async (context: ContextFrom<typeof fileMachine>) => { readFiles: async (context: ContextFrom<typeof fileMachine>) => {
const newFiles = isDesktop() const newFiles = isDesktop()
? (await getProjectInfo(context.project.path)).children ? (await getProjectInfo(context.project.file.path)).children
: [] : []
return { return {
...context.project, ...context.project,
@ -104,7 +104,7 @@ export const FileMachineProvider = ({
} else { } else {
createdPath = createdPath =
context.selectedDirectory.path + context.selectedDirectory.path +
sep() + window.electron.path.sep +
createdName + createdName +
(createdName.endsWith(FILE_EXT) ? '' : FILE_EXT) (createdName.endsWith(FILE_EXT) ? '' : FILE_EXT)
await create(createdPath) await create(createdPath)
@ -169,7 +169,7 @@ export const FileMachineProvider = ({
file?.path.includes(event.data.path)) && file?.path.includes(event.data.path)) &&
project?.path project?.path
) { ) {
navigate(paths.FILE + '/' + encodeURIComponent(project.path)) navigate(paths.FILE + '/' + encodeURIComponent(project.file.path))
} }
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${ return `Successfully deleted ${isDir ? 'folder' : 'file'} "${

View File

@ -171,7 +171,7 @@ const FileTreeItem = ({
// Import non-kcl files // Import non-kcl files
// We want to update both the state and editor here. // We want to update both the state and editor here.
codeManager.updateCodeStateEditor( codeManager.updateCodeStateEditor(
`import("${fileOrDir.path.replace(project.path, '.')}")\n` + `import("${fileOrDir.path.replace(project.file.path, '.')}")\n` +
codeManager.code codeManager.code
) )
codeManager.writeToFile() codeManager.writeToFile()

View File

@ -52,7 +52,7 @@ function ProjectCard({
} }
// async function setupImageUrl() { // 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)) { // if (await exists(projectImagePath)) {
// const imageData = await readFile(projectImagePath) // const imageData = await readFile(projectImagePath)
// const blob = new Blob([imageData], { type: 'image/jpg' }) // const blob = new Blob([imageData], { type: 'image/jpg' })
@ -103,7 +103,7 @@ function ProjectCard({
/> />
) : ( ) : (
<h3 className="font-sans relative z-0 p-2"> <h3 className="font-sans relative z-0 p-2">
{project.name?.replace(FILE_EXT, '')} {project.file.name?.replace(FILE_EXT, '')}
</h3> </h3>
)} )}
<span className="px-2 text-chalkboard-60 text-xs"> <span className="px-2 text-chalkboard-60 text-xs">
@ -169,11 +169,11 @@ function ProjectCard({
onDismiss={() => setIsConfirmingDelete(false)} onDismiss={() => setIsConfirmingDelete(false)}
> >
<p className="my-4"> <p className="my-4">
This will permanently delete "{project.name || 'this file'} This will permanently delete "{project.file.name || 'this file'}
". ".
</p> </p>
<p className="my-4"> <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. "? This action cannot be undone.
</p> </p>
</DeleteConfirmationDialog> </DeleteConfirmationDialog>

View File

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

View File

@ -5,7 +5,6 @@ import { paths } from 'lib/paths'
import { isDesktop } from '../lib/isDesktop' import { isDesktop } from '../lib/isDesktop'
import { Link, useLocation, useNavigate } from 'react-router-dom' import { Link, useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useMemo } from 'react' import { Fragment, useMemo } from 'react'
import { sep } from '@tauri-apps/api/path'
import { Logo } from './Logo' import { Logo } from './Logo'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'
import { useCommandsContext } from 'hooks/useCommandsContext' 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" className="hidden select-none cursor-default text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block"
data-testid="project-name" data-testid="project-name"
> >
{project?.name ? project.name : APP_NAME} {project?.name ? project.file.name : APP_NAME}
</span> </span>
)} )}
</div> </div>
@ -208,12 +207,12 @@ function ProjectMenuPopover({
<div className="flex flex-col items-start py-0.5"> <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"> <span className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block">
{isDesktop() && file?.name {isDesktop() && file?.name
? file.name.slice(file.name.lastIndexOf(sep()) + 1) ? file.name.slice(file.name.lastIndexOf(window.electron.path.sep) + 1)
: APP_NAME} : APP_NAME}
</span> </span>
{isDesktop() && project?.name && ( {isDesktop() && project?.name && (
<span className="hidden text-xs text-chalkboard-70 dark:text-chalkboard-40 whitespace-nowrap lg:block"> <span className="hidden text-xs text-chalkboard-70 dark:text-chalkboard-40 whitespace-nowrap lg:block">
{project.name} {project.file.name}
</span> </span>
)} )}
</div> </div>

View File

@ -46,7 +46,7 @@ export const AllSettingsFields = forwardRef(
location.pathname location.pathname
.replace(paths.FILE + '/', '') .replace(paths.FILE + '/', '')
.replace(paths.SETTINGS, '') .replace(paths.SETTINGS, '')
.slice(0, decodeURI(location.pathname).lastIndexOf(sep())) .slice(0, decodeURI(location.pathname).lastIndexOf(window.electron.path.sep))
) )
: undefined : undefined

View File

@ -1,13 +1,14 @@
import { Platform, platform } from '@tauri-apps/plugin-os'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
export type Platform = 'macos' | 'windows' | 'linux'
export default function usePlatform() { export default function usePlatform() {
const [platformName, setPlatformName] = useState<Platform | ''>('') const [platformName, setPlatformName] = useState<Platform | ''>('')
useEffect(() => { useEffect(() => {
async function getPlatform() { async function getPlatform() {
setPlatformName(await platform()) setPlatformName(window.electron.platform)
} }
if (isDesktop()) { if (isDesktop()) {

View File

@ -3,7 +3,6 @@
// This prevents re-renders of the codemirror editor, when typing. // This prevents re-renders of the codemirror editor, when typing.
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { writeTextFile } from '@tauri-apps/plugin-fs'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { editorManager } from 'lib/singletons' import { editorManager } from 'lib/singletons'
import { Annotation, Transaction } from '@codemirror/state' 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 // Wait one event loop to give a chance for params to be set
// Save the file to disk // Save the file to disk
this._currentFilePath && 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) // TODO: add tracing per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
console.error('error saving file', err) console.error('error saving file', err)
toast.error('Error saving file, please check file permissions') toast.error('Error saving file, please check file permissions')

View File

@ -15,7 +15,7 @@ class FileSystemManager {
} }
async join(dir: string, path: string): Promise<string> { 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> { async readFile(path: string): Promise<Uint8Array | void> {
@ -33,7 +33,7 @@ class FileSystemManager {
return Promise.reject(new Error(`Error reading file: ${error}`)) return Promise.reject(new Error(`Error reading file: ${error}`))
}) })
.then((file) => { .then((file) => {
return window.electron.ipcRenderer.invoke('readFile', [filepath]) return window.electron.readFile(filepath)
}) })
} }
@ -51,8 +51,14 @@ class FileSystemManager {
.catch((error) => { .catch((error) => {
return Promise.reject(new Error(`Error checking file exists: ${error}`)) return Promise.reject(new Error(`Error checking file exists: ${error}`))
}) })
.then((file) => { .then(async (file) => {
return window.electron.ipcRenderer.invoke('exists', [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}`)) return Promise.reject(new Error(`Error joining dir: ${error}`))
}) })
.then((filepath) => { .then((filepath) => {
return window.electron.ipcRenderer return window.electron.readdir(filepath)
.invoke('readdir', [filepath])
.catch((error) => { .catch((error) => {
return Promise.reject(new Error(`Error reading dir: ${error}`)) return Promise.reject(new Error(`Error reading dir: ${error}`))
}) })

View File

@ -19,6 +19,11 @@ import init, {
parse_project_route, parse_project_route,
serialize_project_settings, serialize_project_settings,
} from '../wasm-lib/pkg/wasm_lib' } from '../wasm-lib/pkg/wasm_lib'
import {
configurationToSettingsPayload,
projectConfigurationToSettingsPayload,
SaveSettingsPayload,
} from 'lib/settings/settingsUtils'
import { KCLError } from './errors' import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { EngineCommandManager } from './std/engineConnection' import { EngineCommandManager } from './std/engineConnection'
@ -559,22 +564,26 @@ export function tomlStringify(toml: any): string | Error {
return toml_stringify(JSON.stringify(toml)) return toml_stringify(JSON.stringify(toml))
} }
export function defaultAppSettings(): Configuration { export function defaultAppSettings(): Partial<SaveSettingsPayload> {
return default_app_settings() // 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 { export function parseAppSettings(toml: string): Partial<SaveSettingsPayload> {
return parse_app_settings(toml) return configurationToSettingsPayload(parse_app_settings(toml))
} }
export function defaultProjectSettings(): ProjectConfiguration | Error { export function defaultProjectSettings(): Partial<SaveSettingsPayload> {
return default_project_settings() return projectConfigurationToSettingsPayload(default_project_settings())
} }
export function parseProjectSettings( export function parseProjectSettings(
toml: string toml: string
): ProjectConfiguration | Error { ): Partial<SaveSettingsPayload> {
return parse_project_settings(toml) return projectConfigurationToSettingsPayload(parse_project_settings(toml))
} }
export function parseProjectRoute( export function parseProjectRoute(
@ -583,79 +592,3 @@ export function parseProjectRoute(
): ProjectRoute | Error { ): ProjectRoute | Error {
return parse_project_route(JSON.stringify(configuration), route_str) 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")
}

View File

@ -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 { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration' import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { Project } from 'wasm-lib/kcl/bindings/Project' 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 { components } from './machine-api'
import { isDesktop } from './isDesktop' import { isDesktop } from './isDesktop'
export { import {
readAppSettingsFile, defaultAppSettings,
writeAppSettingsFile, tomlStringify,
getState, parseAppSettings,
setState, parseProjectSettings,
getUser,
login,
} from 'lang/wasm' } 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. // List machines on the local network.
export async function listMachines(): Promise<{ export async function listMachines(): Promise<{
@ -34,15 +43,7 @@ export async function renameProjectDirectory(
projectPath: string, projectPath: string,
newName: string newName: string
): Promise<string> { ): Promise<string> {
return window.electron.ipcRenderer.invoke<string>( debugger
'rename_project_directory',
{ projectPath, newName }
)
}
// Get the initial default dir for holding all projects.
export async function getInitialDefaultDir(): Promise<string> {
return window.electron.getInitialDefaultDir()
} }
export async function showInFolder(path: string | undefined): Promise<void> { 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') console.error('path is undefined cannot call desktop showInFolder')
return return
} }
return window.electron.ipcRenderer.invoke('show_in_folder', { path }) debugger
} }
export async function initializeProjectDirectory( export async function ensureProjectDirectoryExists(
config: Configuration config: Partial<SaveSettingsPayload>
): Promise<string | undefined> { ): Promise<string | undefined> {
const projectDir = config.settings.project.directory const projectDir = config.app.projectDirectory
try { try {
await window.electron.exists(projectDir) await window.electron.stat(projectDir)
} catch (e) { } catch (e) {
if (e === 'ENOENT') { if (e === 'ENOENT') {
window.electron.mkdir(projectDir, { recursive: true }, (e) => { window.electron.mkdir(projectDir, { recursive: true }, (e) => {
@ -73,73 +74,360 @@ export async function initializeProjectDirectory(
export async function createNewProjectDirectory( export async function createNewProjectDirectory(
projectName: string, projectName: string,
initialCode?: string, initialCode?: string,
configuration?: Configuration configuration?: Partial<SaveSettingsPayload>
): Promise<Project> { ): Promise<Project> {
if (!configuration) { if (!configuration) {
configuration = await readAppSettingsFile() configuration = await readAppSettingsFile()
} }
return window.electron.ipcRenderer.invoke('create_new_project_directory', {
configuration, const mainDir = await ensureProjectDirectoryExists(configuration)
projectName,
initialCode, 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( export async function listProjects(
configuration?: Configuration configuration?: Partial<SaveSettingsPayload>
): Promise<Project[]> { ): Promise<Project[]> {
const projectDir = await initializeProjectDirectory(configuration) const projectDir = await ensureProjectDirectoryExists(configuration)
const projects = [] const projects = []
const entries = await window.electron.readdir(projectDir) const entries = await window.electron.readdir(projectDir)
for (let entry of entries) { for (let entry of entries) {
// Ignore directories const projectPath = window.electron.path.join(projectDir, entry)
console.log(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( export async function getProjectInfo(
projectPath: string, projectPath: string,
configuration?: Configuration
): Promise<Project> { ): Promise<Project> {
if (!configuration) { // Check the directory.
configuration = await readAppSettingsFile() try { await window.electron.stat(projectPath) }
catch (e) {
if (e === 'ENOENT') {
return Promise.reject(new Error(`Project directory does not exist: ${project_path}`));
}
} }
return window.electron.ipcRenderer.invoke('get_project_info', {
configuration,
projectPath,
})
}
export async function parseProjectRoute( // Make sure it is a directory.
configuration: Configuration, const projectPathIsDir = await window.electron.statIsDirectory(projectPath)
route: string if (!projectPathIsDir) {
): Promise<ProjectRoute> { return Promise.reject(new Error(`Project path is not a directory: ${project_path}`));
return window.electron.ipcRenderer.invoke('parse_project_route', { }
configuration,
route, 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[]> { export async function readDirRecursive(path: string): Promise<FileEntry[]> {
return window.electron.ipcRenderer.invoke('read_dir_recursive', { path }) debugger
}
// Read project settings file.
export async function readProjectSettingsFile(
projectPath: string
): Promise<ProjectConfiguration> {
return window.electron.ipcRenderer.invoke('read_project_settings_file', {
projectPath,
})
} }
// Write project settings file. // Write project settings file.
export async function writeProjectSettingsFile( export async function writeProjectSettingsFile({
projectPath: string,
settings: ProjectConfiguration
): Promise<void> {
return window.electron.ipcRenderer.invoke('write_project_settings_file', {
projectPath, projectPath,
configuration: settings, 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
}

View File

@ -4,22 +4,46 @@ import fs from 'node:fs/promises'
import packageJson from '../../package.json' import packageJson from '../../package.json'
const readFile = (path: string) => fs.readFile(path, 'utf-8') 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 readdir = (path: string) => fs.readdir(path, 'utf-8')
const exists = (path: string) => const stat = (path: string) => fs.stat(path).catch((e) => Promise.reject(e.code))
new Promise((resolve, reject) => // Electron has behavior where it doesn't clone the prototype chain over.
fs.stat(path, (err, data) => { // So we need to call stat.isDirectory on this side.
if (err) return reject(err.code) const statIsDirectory = (path: string) => stat(path).then((res) => res.isDirectory())
return resolve(data)
})
)
const getPath = async (name: string) => ipcRenderer.invoke('app.getPath', name) const getPath = async (name: string) => ipcRenderer.invoke('app.getPath', name)
contextBridge.exposeInMainWorld('electron', { 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, readFile,
writeFile,
readdir, readdir,
path, path,
exists, stat,
statIsDirectory,
mkdir: fs.mkdir, mkdir: fs.mkdir,
getPath, getPath,
packageJson, 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,
},
})
}) })

View File

@ -11,12 +11,11 @@ import {
import { loadAndValidateSettings } from './settings/settingsUtils' import { loadAndValidateSettings } from './settings/settingsUtils'
import makeUrlPathRelative from './makeUrlPathRelative' import makeUrlPathRelative from './makeUrlPathRelative'
import { sep } from '@tauri-apps/api/path' import { sep } from '@tauri-apps/api/path'
import { readTextFile } from '@tauri-apps/plugin-fs'
import { codeManager, kclManager } from 'lib/singletons' import { codeManager, kclManager } from 'lib/singletons'
import { fileSystemManager } from 'lang/std/fileSystemManager' import { fileSystemManager } from 'lang/std/fileSystemManager'
import { import {
getProjectInfo, getProjectInfo,
initializeProjectDirectory, ensureProjectDirectoryExists,
listProjects, listProjects,
} from './desktop' } from './desktop'
import { createSettings } from './settings/initialSettings' import { createSettings } from './settings/initialSettings'
@ -90,14 +89,14 @@ export const fileLoader: LoaderFunction = async ({
if (!current_file_name || !current_file_path || !project_name) { if (!current_file_name || !current_file_path || !project_name) {
return redirect( return redirect(
`${paths.FILE}/${encodeURIComponent( `${paths.FILE}/${encodeURIComponent(
`${params.id}${isDesktop() ? sep() : '/'}${PROJECT_ENTRYPOINT}` `${params.id}${isDesktop() ? window.electron.path.sep : '/'}${PROJECT_ENTRYPOINT}`
)}` )}`
) )
} }
// TODO: PROJECT_ENTRYPOINT is hardcoded // TODO: PROJECT_ENTRYPOINT is hardcoded
// until we support setting a project's entrypoint file // 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. // Update both the state and the editor's code.
// We explicitly do not write to the file here since we are loading from // 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 { configuration } = await loadAndValidateSettings()
const projectDir = await initializeProjectDirectory(configuration) const projectDir = await ensureProjectDirectoryExists(configuration)
if (projectDir) { if (projectDir) {
const projects = await listProjects(configuration) const projects = await listProjects(configuration)

View File

@ -27,7 +27,7 @@ import { BROWSER_PROJECT_NAME } from 'lib/constants'
* We do this because the JS settings type has all the fancy shit * We do this because the JS settings type has all the fancy shit
* for hiding and showing settings. * for hiding and showing settings.
**/ **/
function configurationToSettingsPayload( export function configurationToSettingsPayload(
configuration: Configuration configuration: Configuration
): Partial<SaveSettingsPayload> { ): Partial<SaveSettingsPayload> {
return { return {
@ -65,7 +65,7 @@ function configurationToSettingsPayload(
} }
} }
function projectConfigurationToSettingsPayload( export function projectConfigurationToSettingsPayload(
configuration: ProjectConfiguration configuration: ProjectConfiguration
): Partial<SaveSettingsPayload> { ): Partial<SaveSettingsPayload> {
return { return {
@ -165,14 +165,12 @@ export async function loadAndValidateSettings(
await initPromise await initPromise
// Load the app settings from the file system or localStorage. // Load the app settings from the file system or localStorage.
const appSettings = onDesktop const appSettingsPayload = onDesktop
? await readAppSettingsFile() ? await readAppSettingsFile()
: readLocalStorageAppSettingsFile() : 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) setSettingsAtLevel(settings, 'user', appSettingsPayload)
// Load the project settings if they exist // Load the project settings if they exist
@ -190,7 +188,7 @@ export async function loadAndValidateSettings(
} }
// Return the settings object // Return the settings object
return { settings, configuration: appSettings } return { settings, configuration: appSettingsPayload }
} }
export async function saveSettings( export async function saveSettings(
@ -207,7 +205,7 @@ export async function saveSettings(
if (err(tomlString)) return if (err(tomlString)) return
// Parse this as a Configuration. // Parse this as a Configuration.
const appSettings = parseAppSettings(tomlString) const appSettings = { settings: parseAppSettings(tomlString) }
if (err(appSettings)) return if (err(appSettings)) return
const tomlString2 = tomlStringify(appSettings) const tomlString2 = tomlStringify(appSettings)

View File

@ -1,12 +1,4 @@
let toast = undefined import toast from 'react-hot-toast'
try {
global
} catch (e) {
import('react-hot-toast').then((_toast) => {
toast = _toast
})
}
type ExcludeErr<T> = Exclude<T, Error> type ExcludeErr<T> = Exclude<T, Error>
@ -16,6 +8,7 @@ export function err<T>(value: ExcludeErr<T> | Error): value is Error {
return false return false
} }
console.error(value)
return true return true
} }

View File

@ -146,7 +146,10 @@ async function getUser(context: UserContext) {
}) })
.then((res) => res.json()) .then((res) => res.json())
.catch((err) => console.error('error from Browser getUser', err)) .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 const user = await userPromise

View File

@ -11,7 +11,7 @@ if (require('electron-squirrel-startup')) {
} }
const createWindow = () => { const createWindow = () => {
const mainWindow = new BrowserWindow({ let mainWindow = new BrowserWindow({
width: 800, width: 800,
height: 600, height: 600,
webPreferences: { webPreferences: {

View File

@ -82,7 +82,7 @@ const Home = () => {
event: EventFrom<typeof homeMachine> event: EventFrom<typeof homeMachine>
) => { ) => {
if (event.data && 'name' in event.data) { 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( onProjectOpen(
{ {
name: event.data.name, name: event.data.name,
@ -192,15 +192,15 @@ const Home = () => {
new FormData(e.target as HTMLFormElement) new FormData(e.target as HTMLFormElement)
) )
if (newProjectName !== project.name) { if (newProjectName !== project.file.name) {
send('Rename project', { send('Rename project', {
data: { oldName: project.name, newName: newProjectName }, data: { oldName: project.file.name, newName: newProjectName },
}) })
} }
} }
async function handleDeleteProject(project: Project) { async function handleDeleteProject(project: Project) {
send('Delete project', { data: { name: project.name || '' } }) send('Delete project', { data: { name: project.file.name || '' } })
} }
return ( return (
@ -296,7 +296,7 @@ const Home = () => {
<ul className="grid w-full grid-cols-4 gap-4"> <ul className="grid w-full grid-cols-4 gap-4">
{searchResults.sort(getSortFunction(sort)).map((project) => ( {searchResults.sort(getSortFunction(sort)).map((project) => (
<ProjectCard <ProjectCard
key={project.name} key={project.file.name}
project={project} project={project}
handleRenameProject={handleRenameProject} handleRenameProject={handleRenameProject}
handleDeleteProject={handleDeleteProject} handleDeleteProject={handleDeleteProject}

View File

@ -495,7 +495,6 @@ pub fn default_app_settings() -> Result<JsValue, String> {
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
let settings = kcl_lib::settings::types::Configuration::default(); 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()) serde_wasm_bindgen::to_value(&settings).map_err(|e| e.to_string())
} }