2023-08-28 20:31:49 -04:00
|
|
|
import {
|
|
|
|
FileEntry,
|
|
|
|
createDir,
|
|
|
|
exists,
|
|
|
|
readDir,
|
|
|
|
writeTextFile,
|
|
|
|
} from '@tauri-apps/api/fs'
|
2023-09-12 18:46:35 -04:00
|
|
|
import { documentDir, homeDir } from '@tauri-apps/api/path'
|
2023-08-15 21:56:24 -04:00
|
|
|
import { isTauri } from './isTauri'
|
|
|
|
import { ProjectWithEntryPointMetadata } from '../Router'
|
|
|
|
import { metadata } from 'tauri-plugin-fs-extra-api'
|
|
|
|
|
|
|
|
const PROJECT_FOLDER = 'kittycad-modeling-projects'
|
|
|
|
export const FILE_EXT = '.kcl'
|
|
|
|
export const PROJECT_ENTRYPOINT = 'main' + FILE_EXT
|
|
|
|
const INDEX_IDENTIFIER = '$n' // $nn.. will pad the number with 0s
|
|
|
|
export const MAX_PADDING = 7
|
|
|
|
|
|
|
|
// Initializes the project directory and returns the path
|
2023-08-28 20:31:49 -04:00
|
|
|
export async function initializeProjectDirectory(directory: string) {
|
2023-08-15 21:56:24 -04:00
|
|
|
if (!isTauri()) {
|
|
|
|
throw new Error(
|
|
|
|
'initializeProjectDirectory() can only be called from a Tauri app'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-08-28 20:31:49 -04:00
|
|
|
if (directory) {
|
|
|
|
const dirExists = await exists(directory)
|
2023-08-15 21:56:24 -04:00
|
|
|
if (!dirExists) {
|
2023-08-28 20:31:49 -04:00
|
|
|
await createDir(directory, { recursive: true })
|
2023-08-15 21:56:24 -04:00
|
|
|
}
|
2023-08-28 20:31:49 -04:00
|
|
|
return directory
|
2023-08-15 21:56:24 -04:00
|
|
|
}
|
|
|
|
|
2023-09-12 18:46:35 -04:00
|
|
|
let docDirectory: string
|
|
|
|
try {
|
|
|
|
docDirectory = await documentDir()
|
|
|
|
} catch (e) {
|
2023-09-25 17:28:03 +10:00
|
|
|
console.log('error', e)
|
2023-09-12 18:46:35 -04:00
|
|
|
docDirectory = await homeDir() // seems to work better on Linux
|
|
|
|
}
|
2023-08-15 21:56:24 -04:00
|
|
|
|
2023-08-28 20:31:49 -04:00
|
|
|
const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER
|
2023-08-15 21:56:24 -04:00
|
|
|
|
2023-08-28 20:31:49 -04:00
|
|
|
const defaultDirExists = await exists(INITIAL_DEFAULT_DIR)
|
2023-08-15 21:56:24 -04:00
|
|
|
|
|
|
|
if (!defaultDirExists) {
|
2023-08-28 20:31:49 -04:00
|
|
|
await createDir(INITIAL_DEFAULT_DIR, { recursive: true })
|
2023-08-15 21:56:24 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return INITIAL_DEFAULT_DIR
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isProjectDirectory(fileOrDir: Partial<FileEntry>) {
|
|
|
|
return (
|
|
|
|
fileOrDir.children?.length &&
|
|
|
|
fileOrDir.children.some((child) => child.name === PROJECT_ENTRYPOINT)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-08-28 20:31:49 -04:00
|
|
|
// Read the contents of a directory
|
|
|
|
// and return the valid projects
|
|
|
|
export async function getProjectsInDir(projectDir: string) {
|
|
|
|
const readProjects = (
|
|
|
|
await readDir(projectDir, {
|
|
|
|
recursive: true,
|
|
|
|
})
|
|
|
|
).filter(isProjectDirectory)
|
|
|
|
|
|
|
|
const projectsWithMetadata = await Promise.all(
|
|
|
|
readProjects.map(async (p) => ({
|
|
|
|
entrypoint_metadata: await metadata(p.path + '/' + PROJECT_ENTRYPOINT),
|
|
|
|
...p,
|
|
|
|
}))
|
|
|
|
)
|
|
|
|
|
|
|
|
return projectsWithMetadata
|
|
|
|
}
|
|
|
|
|
2023-08-15 21:56:24 -04:00
|
|
|
// Creates a new file in the default directory with the default project name
|
|
|
|
// Returns the path to the new file
|
|
|
|
export async function createNewProject(
|
|
|
|
path: string
|
|
|
|
): Promise<ProjectWithEntryPointMetadata> {
|
|
|
|
if (!isTauri) {
|
|
|
|
throw new Error('createNewProject() can only be called from a Tauri app')
|
|
|
|
}
|
|
|
|
|
|
|
|
const dirExists = await exists(path)
|
|
|
|
if (!dirExists) {
|
|
|
|
await createDir(path, { recursive: true }).catch((err) => {
|
|
|
|
console.error('Error creating new directory:', err)
|
|
|
|
throw err
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
await writeTextFile(path + '/' + PROJECT_ENTRYPOINT, '').catch((err) => {
|
|
|
|
console.error('Error creating new file:', err)
|
|
|
|
throw err
|
|
|
|
})
|
|
|
|
|
|
|
|
const m = await metadata(path)
|
|
|
|
|
|
|
|
return {
|
|
|
|
name: path.slice(path.lastIndexOf('/') + 1),
|
|
|
|
path: path,
|
|
|
|
entrypoint_metadata: m,
|
|
|
|
children: [
|
|
|
|
{
|
|
|
|
name: PROJECT_ENTRYPOINT,
|
|
|
|
path: path + '/' + PROJECT_ENTRYPOINT,
|
|
|
|
children: [],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// create a regex to match the project name
|
|
|
|
// replacing any instances of "$n" with a regex to match any number
|
|
|
|
function interpolateProjectName(projectName: string) {
|
|
|
|
const regex = new RegExp(
|
|
|
|
projectName.replace(getPaddedIdentifierRegExp(), '([0-9]+)')
|
|
|
|
)
|
|
|
|
return regex
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns the next available index for a project name
|
|
|
|
export function getNextProjectIndex(projectName: string, files: FileEntry[]) {
|
|
|
|
const regex = interpolateProjectName(projectName)
|
|
|
|
const matches = files.map((file) => file.name?.match(regex))
|
|
|
|
const indices = matches
|
|
|
|
.filter(Boolean)
|
|
|
|
.map((match) => match![1])
|
|
|
|
.map(Number)
|
|
|
|
const maxIndex = Math.max(...indices, -1)
|
|
|
|
return maxIndex + 1
|
|
|
|
}
|
|
|
|
|
|
|
|
// Interpolates the project name with the next available index,
|
|
|
|
// padding the index with 0s if necessary
|
|
|
|
export function interpolateProjectNameWithIndex(
|
|
|
|
projectName: string,
|
|
|
|
index: number
|
|
|
|
) {
|
|
|
|
const regex = getPaddedIdentifierRegExp()
|
|
|
|
|
|
|
|
const matches = projectName.match(regex)
|
|
|
|
const padStartLength = Math.min(
|
|
|
|
matches !== null ? matches[1]?.length || 0 : 0,
|
|
|
|
MAX_PADDING
|
|
|
|
)
|
|
|
|
return projectName.replace(
|
|
|
|
regex,
|
|
|
|
index.toString().padStart(padStartLength + 1, '0')
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export function doesProjectNameNeedInterpolated(projectName: string) {
|
|
|
|
return projectName.includes(INDEX_IDENTIFIER)
|
|
|
|
}
|
|
|
|
|
|
|
|
function escapeRegExpChars(string: string) {
|
|
|
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
|
}
|
|
|
|
|
|
|
|
function getPaddedIdentifierRegExp() {
|
|
|
|
const escapedIdentifier = escapeRegExpChars(INDEX_IDENTIFIER)
|
|
|
|
return new RegExp(`${escapedIdentifier}(${escapedIdentifier.slice(-1)}*)`)
|
|
|
|
}
|