Settings move to rust (for read/write from files) (#2220)
* start of settings types Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * add validator Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * start of settings in rust Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix wasm Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix wasm Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * more tests Signed-off-by: Jess Frazelle <github@jessfraz.com> * derive docs Signed-off-by: Jess Frazelle <github@jessfraz.com> * configuration Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * read and write functions with migration Signed-off-by: Jess Frazelle <github@jessfraz.com> * make more dry Signed-off-by: Jess Frazelle <github@jessfraz.com> * more parsing of app settings Signed-off-by: Jess Frazelle <github@jessfraz.com> * more things Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup Signed-off-by: Jess Frazelle <github@jessfraz.com> * trim end Signed-off-by: Jess Frazelle <github@jessfraz.com> * project settings Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup tauri commands Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * refactor Signed-off-by: Jess Frazelle <github@jessfraz.com> * refactor Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * change to files Signed-off-by: Jess Frazelle <github@jessfraz.com> * better Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup more Signed-off-by: Jess Frazelle <github@jessfraz.com> * get rid of dead code Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixed Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup some more shit Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * add validation Signed-off-by: Jess Frazelle <github@jessfraz.com> * validation Signed-off-by: Jess Frazelle <github@jessfraz.com> * validate Signed-off-by: Jess Frazelle <github@jessfraz.com> * validate Signed-off-by: Jess Frazelle <github@jessfraz.com> * clippuy Signed-off-by: Jess Frazelle <github@jessfraz.com> * clippuy Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix; Signed-off-by: Jess Frazelle <github@jessfraz.com> --------- Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
@ -1,154 +1,19 @@
|
||||
import {
|
||||
mkdir,
|
||||
exists,
|
||||
readTextFile,
|
||||
writeTextFile,
|
||||
stat,
|
||||
} from '@tauri-apps/plugin-fs'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import {
|
||||
appConfigDir,
|
||||
documentDir,
|
||||
homeDir,
|
||||
join,
|
||||
sep,
|
||||
} from '@tauri-apps/api/path'
|
||||
import { appConfigDir } from '@tauri-apps/api/path'
|
||||
import { isTauri } from './isTauri'
|
||||
import type { FileEntry, ProjectWithEntryPointMetadata } from 'lib/types'
|
||||
import type { FileEntry } from 'lib/types'
|
||||
import {
|
||||
FILE_EXT,
|
||||
INDEX_IDENTIFIER,
|
||||
MAX_PADDING,
|
||||
ONBOARDING_PROJECT_NAME,
|
||||
PROJECT_ENTRYPOINT,
|
||||
PROJECT_FOLDER,
|
||||
RELEVANT_FILE_TYPES,
|
||||
SETTINGS_FILE_EXT,
|
||||
} from 'lib/constants'
|
||||
import { SaveSettingsPayload, SettingsLevel } from './settings/settingsTypes'
|
||||
import { initPromise, tomlParse } from 'lang/wasm'
|
||||
import { bracket } from './exampleKcl'
|
||||
import { paths } from './paths'
|
||||
|
||||
type PathWithPossibleError = {
|
||||
path: string | null
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export async function getInitialDefaultDir() {
|
||||
if (!isTauri()) return ''
|
||||
let dir
|
||||
try {
|
||||
dir = await documentDir()
|
||||
} catch (e) {
|
||||
dir = await join(await homeDir(), 'Documents') // for headless Linux (eg. Github Actions)
|
||||
}
|
||||
|
||||
return await join(dir, PROJECT_FOLDER)
|
||||
}
|
||||
|
||||
// Initializes the project directory and returns the path
|
||||
// with any Errors that occurred
|
||||
export async function initializeProjectDirectory(
|
||||
directory: string
|
||||
): Promise<PathWithPossibleError> {
|
||||
let returnValue: PathWithPossibleError = {
|
||||
path: null,
|
||||
error: null,
|
||||
}
|
||||
|
||||
if (!isTauri()) return returnValue
|
||||
|
||||
if (directory) {
|
||||
returnValue = await testAndCreateDir(directory, returnValue)
|
||||
}
|
||||
|
||||
// If the directory from settings does not exist or could not be created,
|
||||
// use the default directory
|
||||
if (returnValue.path === null) {
|
||||
const INITIAL_DEFAULT_DIR = await getInitialDefaultDir()
|
||||
const defaultReturnValue = await testAndCreateDir(
|
||||
INITIAL_DEFAULT_DIR,
|
||||
returnValue,
|
||||
{
|
||||
exists: 'Error checking default directory.',
|
||||
create: 'Error creating default directory.',
|
||||
}
|
||||
)
|
||||
returnValue.path = defaultReturnValue.path
|
||||
returnValue.error =
|
||||
returnValue.error === null ? defaultReturnValue.error : returnValue.error
|
||||
}
|
||||
|
||||
return returnValue
|
||||
}
|
||||
|
||||
async function testAndCreateDir(
|
||||
directory: string,
|
||||
returnValue = {
|
||||
path: null,
|
||||
error: null,
|
||||
} as PathWithPossibleError,
|
||||
errorMessages = {
|
||||
exists:
|
||||
'Error checking directory at path from saved settings. Using default.',
|
||||
create:
|
||||
'Error creating directory at path from saved settings. Using default.',
|
||||
}
|
||||
): Promise<PathWithPossibleError> {
|
||||
const dirExists = await exists(directory).catch((e) => {
|
||||
console.error(`Error checking directory ${directory}. Original error:`, e)
|
||||
return new Error(errorMessages.exists)
|
||||
})
|
||||
|
||||
if (dirExists instanceof Error) {
|
||||
returnValue.error = dirExists
|
||||
} else if (dirExists === false) {
|
||||
const newDirCreated = await mkdir(directory, { recursive: true }).catch(
|
||||
(e) => {
|
||||
console.error(
|
||||
`Error creating directory ${directory}. Original error:`,
|
||||
e
|
||||
)
|
||||
return new Error(errorMessages.create)
|
||||
}
|
||||
)
|
||||
|
||||
if (newDirCreated instanceof Error) {
|
||||
returnValue.error = newDirCreated
|
||||
} else {
|
||||
returnValue.path = directory
|
||||
}
|
||||
} else if (dirExists === true) {
|
||||
returnValue.path = directory
|
||||
}
|
||||
|
||||
return returnValue
|
||||
}
|
||||
|
||||
export function isProjectDirectory(fileOrDir: Partial<FileEntry>) {
|
||||
return (
|
||||
fileOrDir.children?.length &&
|
||||
fileOrDir.children.some((child) => child.name === PROJECT_ENTRYPOINT)
|
||||
)
|
||||
}
|
||||
|
||||
// Read the contents of a directory
|
||||
// and return the valid projects
|
||||
export async function getProjectsInDir(projectDir: string) {
|
||||
const readProjects = (
|
||||
await invoke<FileEntry[]>('read_dir_recursive', { path: projectDir })
|
||||
).filter(isProjectDirectory)
|
||||
|
||||
const projectsWithMetadata = await Promise.all(
|
||||
readProjects.map(async (p) => ({
|
||||
entrypointMetadata: await stat(await join(p.path, PROJECT_ENTRYPOINT)),
|
||||
...p,
|
||||
}))
|
||||
)
|
||||
|
||||
return projectsWithMetadata
|
||||
}
|
||||
import {
|
||||
createNewProjectDirectory,
|
||||
listProjects,
|
||||
readAppSettingsFile,
|
||||
} from './tauri'
|
||||
|
||||
export const isHidden = (fileOrDir: FileEntry) =>
|
||||
!!fileOrDir.name?.startsWith('.')
|
||||
@ -156,97 +21,6 @@ export const isHidden = (fileOrDir: FileEntry) =>
|
||||
export const isDir = (fileOrDir: FileEntry) =>
|
||||
'children' in fileOrDir && fileOrDir.children !== undefined
|
||||
|
||||
export function deepFileFilter(
|
||||
entries: FileEntry[],
|
||||
filterFn: (f: FileEntry) => boolean
|
||||
): FileEntry[] {
|
||||
const filteredEntries: FileEntry[] = []
|
||||
for (const fileOrDir of entries) {
|
||||
if ('children' in fileOrDir && fileOrDir.children !== undefined) {
|
||||
const filteredChildren = deepFileFilter(fileOrDir.children, filterFn)
|
||||
if (filterFn(fileOrDir)) {
|
||||
filteredEntries.push({
|
||||
...fileOrDir,
|
||||
children: filteredChildren,
|
||||
})
|
||||
}
|
||||
} else if (filterFn(fileOrDir)) {
|
||||
filteredEntries.push(fileOrDir)
|
||||
}
|
||||
}
|
||||
return filteredEntries
|
||||
}
|
||||
|
||||
export function deepFileFilterFlat(
|
||||
entries: FileEntry[],
|
||||
filterFn: (f: FileEntry) => boolean
|
||||
): FileEntry[] {
|
||||
const filteredEntries: FileEntry[] = []
|
||||
for (const fileOrDir of entries) {
|
||||
if ('children' in fileOrDir && fileOrDir.children !== undefined) {
|
||||
const filteredChildren = deepFileFilterFlat(fileOrDir.children, filterFn)
|
||||
if (filterFn(fileOrDir)) {
|
||||
filteredEntries.push({
|
||||
...fileOrDir,
|
||||
children: filteredChildren,
|
||||
})
|
||||
}
|
||||
filteredEntries.push(...filteredChildren)
|
||||
} else if (filterFn(fileOrDir)) {
|
||||
filteredEntries.push(fileOrDir)
|
||||
}
|
||||
}
|
||||
return filteredEntries
|
||||
}
|
||||
|
||||
// Read the contents of a project directory
|
||||
// and return all relevant files and sub-directories recursively
|
||||
export async function readProject(projectDir: string) {
|
||||
const readFiles = await invoke<FileEntry[]>('read_dir_recursive', {
|
||||
path: projectDir,
|
||||
})
|
||||
|
||||
return deepFileFilter(readFiles, isRelevantFileOrDir)
|
||||
}
|
||||
|
||||
// Given a read project, return the number of .kcl files,
|
||||
// both in the root directory and in sub-directories,
|
||||
// and folders that contain at least one .kcl file
|
||||
export function getPartsCount(project: FileEntry[]) {
|
||||
const flatProject = deepFileFilterFlat(project, isRelevantFileOrDir)
|
||||
|
||||
const kclFileCount = flatProject.filter((f) =>
|
||||
f.name?.endsWith(FILE_EXT)
|
||||
).length
|
||||
const kclDirCount = flatProject.filter((f) => f.children !== undefined).length
|
||||
|
||||
return {
|
||||
kclFileCount,
|
||||
kclDirCount,
|
||||
}
|
||||
}
|
||||
|
||||
// Determines if a file or directory is relevant to the project
|
||||
// i.e. not a hidden file or directory, and is a relevant file type
|
||||
// or contains at least one relevant file (even if it's nested)
|
||||
// or is a completely empty directory
|
||||
export function isRelevantFileOrDir(fileOrDir: FileEntry) {
|
||||
let isRelevantDir = false
|
||||
if ('children' in fileOrDir && fileOrDir.children !== undefined) {
|
||||
isRelevantDir =
|
||||
!isHidden(fileOrDir) &&
|
||||
(fileOrDir.children.some(isRelevantFileOrDir) ||
|
||||
fileOrDir.children.length === 0)
|
||||
}
|
||||
const isRelevantFile =
|
||||
!isHidden(fileOrDir) &&
|
||||
RELEVANT_FILE_TYPES.some((ext) => fileOrDir.name?.endsWith(ext))
|
||||
|
||||
return (
|
||||
(isDir(fileOrDir) && isRelevantDir) || (!isDir(fileOrDir) && isRelevantFile)
|
||||
)
|
||||
}
|
||||
|
||||
// Deeply sort the files and directories in a project like VS Code does:
|
||||
// The main.kcl file is always first, then files, then directories
|
||||
// Files and directories are sorted alphabetically
|
||||
@ -279,47 +53,6 @@ export function sortProject(project: FileEntry[]): FileEntry[] {
|
||||
})
|
||||
}
|
||||
|
||||
// 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,
|
||||
initCode = ''
|
||||
): Promise<ProjectWithEntryPointMetadata> {
|
||||
if (!isTauri) {
|
||||
throw new Error('createNewProject() can only be called from a Tauri app')
|
||||
}
|
||||
|
||||
const dirExists = await exists(path)
|
||||
if (!dirExists) {
|
||||
await mkdir(path, { recursive: true }).catch((err) => {
|
||||
console.error('Error creating new directory:', err)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
await writeTextFile(await join(path, PROJECT_ENTRYPOINT), initCode).catch(
|
||||
(err) => {
|
||||
console.error('Error creating new file:', err)
|
||||
throw err
|
||||
}
|
||||
)
|
||||
|
||||
const m = await stat(path)
|
||||
|
||||
return {
|
||||
name: path.slice(path.lastIndexOf(sep()) + 1),
|
||||
path: path,
|
||||
entrypointMetadata: m,
|
||||
children: [
|
||||
{
|
||||
name: PROJECT_ENTRYPOINT,
|
||||
path: await join(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) {
|
||||
@ -373,55 +106,6 @@ function getPaddedIdentifierRegExp() {
|
||||
return new RegExp(`${escapedIdentifier}(${escapedIdentifier.slice(-1)}*)`)
|
||||
}
|
||||
|
||||
export async function getUserSettingsFilePath(
|
||||
filename: string = SETTINGS_FILE_EXT
|
||||
) {
|
||||
const dir = await appConfigDir()
|
||||
return await join(dir, filename)
|
||||
}
|
||||
|
||||
export async function readSettingsFile(
|
||||
path: string
|
||||
): Promise<Partial<SaveSettingsPayload>> {
|
||||
const dir = path.slice(0, path.lastIndexOf(sep()))
|
||||
|
||||
const dirExists = await exists(dir)
|
||||
if (!dirExists) {
|
||||
await mkdir(dir, { recursive: true })
|
||||
}
|
||||
|
||||
const settingsExist = dirExists ? await exists(path) : false
|
||||
|
||||
if (!settingsExist) {
|
||||
console.log(`Settings file does not exist at ${path}`)
|
||||
return {}
|
||||
}
|
||||
|
||||
try {
|
||||
await initPromise
|
||||
const settings = await readTextFile(path)
|
||||
// We expect the settings to be under a top-level [settings] key
|
||||
return tomlParse(settings).settings as Partial<SaveSettingsPayload>
|
||||
} catch (e) {
|
||||
console.error('Error reading settings file:', e)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSettingsFilePaths(
|
||||
projectPath?: string
|
||||
): Promise<Partial<Record<SettingsLevel, string>>> {
|
||||
const { user, project } = await getSettingsFolderPaths(projectPath)
|
||||
|
||||
return {
|
||||
user: user + 'user' + SETTINGS_FILE_EXT,
|
||||
project:
|
||||
project !== undefined
|
||||
? project + (isTauri() ? sep() : '/') + 'project' + SETTINGS_FILE_EXT
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSettingsFolderPaths(projectPath?: string) {
|
||||
const user = isTauri() ? await appConfigDir() : '/'
|
||||
const project = projectPath !== undefined ? projectPath : undefined
|
||||
@ -433,18 +117,15 @@ export async function getSettingsFolderPaths(projectPath?: string) {
|
||||
}
|
||||
|
||||
export async function createAndOpenNewProject(
|
||||
projectDirectory: string,
|
||||
navigate: (path: string) => void
|
||||
) {
|
||||
const projects = await getProjectsInDir(projectDirectory)
|
||||
const nextIndex = await getNextProjectIndex(ONBOARDING_PROJECT_NAME, projects)
|
||||
const configuration = await readAppSettingsFile()
|
||||
const projects = await listProjects(configuration)
|
||||
const nextIndex = getNextProjectIndex(ONBOARDING_PROJECT_NAME, projects)
|
||||
const name = interpolateProjectNameWithIndex(
|
||||
ONBOARDING_PROJECT_NAME,
|
||||
nextIndex
|
||||
)
|
||||
const newFile = await createNewProject(
|
||||
await join(projectDirectory, name),
|
||||
bracket
|
||||
)
|
||||
const newFile = await createNewProjectDirectory(name, bracket, configuration)
|
||||
navigate(`${paths.FILE}/${encodeURIComponent(newFile.path)}`)
|
||||
}
|
||||
|
Reference in New Issue
Block a user