Project state improvements (#2239)

This commit is contained in:
Jess Frazelle
2024-04-25 05:52:08 -07:00
committed by GitHub
parent e123a00d4b
commit 0a96dc6fd2
15 changed files with 379 additions and 37 deletions

113
src-tauri/Cargo.lock generated
View File

@ -90,6 +90,54 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
]
[[package]]
name = "anyhow"
version = "1.0.82"
@ -110,6 +158,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-cli",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-http",
@ -695,6 +744,33 @@ dependencies = [
"inout",
]
[[package]]
name = "clap"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim 0.11.1",
]
[[package]]
name = "clap_lex"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "cocoa"
version = "0.25.0"
@ -725,6 +801,12 @@ dependencies = [
"objc",
]
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "colored"
version = "2.1.0"
@ -915,7 +997,7 @@ dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"strsim 0.10.0",
"syn 2.0.60",
]
@ -2321,7 +2403,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.1.51"
version = "0.1.52"
dependencies = [
"anyhow",
"approx",
@ -4580,6 +4662,12 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "structmeta"
version = "0.2.0"
@ -4958,6 +5046,21 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-cli"
version = "2.0.0-beta.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b079f01e923f7d3bf175e8d31b18861e6580f4b57ce0fdc16fbf69f9acd158c"
dependencies = [
"clap",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.0.0-beta.6"
@ -5820,6 +5923,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.8.0"

View File

@ -15,11 +15,12 @@ tauri-build = { version = "2.0.0-beta.13", features = [] }
[dependencies]
anyhow = "1"
kcl-lib = { version = "0.1.51", path = "../src/wasm-lib/kcl" }
kcl-lib = { version = "0.1.52", path = "../src/wasm-lib/kcl" }
kittycad = "0.3.0"
oauth2 = "4.4.2"
serde_json = "1.0"
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
tauri-plugin-cli = { version = "2.0.0-beta.3" }
tauri-plugin-dialog = { version = "2.0.0-beta.6" }
tauri-plugin-fs = { version = "2.0.0-beta.6" }
tauri-plugin-http = { version = "2.0.0-beta.6" }

View File

@ -7,6 +7,7 @@
"main"
],
"permissions": [
"cli:default",
"path:default",
"event:default",
"window:default",
@ -23,7 +24,6 @@
"fs:allow-copy-file",
"fs:allow-mkdir",
"fs:allow-remove",
"fs:allow-remove",
"fs:allow-rename",
"fs:allow-exists",
"fs:allow-stat",

View File

@ -1,6 +1,8 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
pub(crate) mod state;
use std::{
env,
path::{Path, PathBuf},
@ -9,12 +11,13 @@ use std::{
use anyhow::Result;
use kcl_lib::settings::types::{
file::{FileEntry, Project},
file::{FileEntry, Project, ProjectState},
project::ProjectConfiguration,
Configuration,
Configuration, DEFAULT_PROJECT_KCL_FILE,
};
use oauth2::TokenResponse;
use tauri::{ipc::InvokeError, Manager};
use tauri_plugin_cli::CliExt;
use tauri_plugin_shell::ShellExt;
const DEFAULT_HOST: &str = "https://api.zoo.dev";
@ -36,6 +39,19 @@ fn get_initial_default_dir(app: tauri::AppHandle) -> Result<PathBuf, InvokeError
Ok(dir.join(PROJECT_FOLDER))
}
#[tauri::command]
async fn get_state(app: tauri::AppHandle) -> Result<Option<ProjectState>, InvokeError> {
let store = app.state::<state::Store>();
Ok(store.get().await)
}
#[tauri::command]
async fn set_state(app: tauri::AppHandle, state: Option<ProjectState>) -> Result<(), InvokeError> {
let store = app.state::<state::Store>();
store.set(state).await;
Ok(())
}
fn get_app_settings_file_path(app: &tauri::AppHandle) -> Result<PathBuf, InvokeError> {
let app_config_dir = app.path().app_config_dir()?;
Ok(app_config_dir.join(SETTINGS_FILE_NAME))
@ -172,9 +188,9 @@ async fn list_projects(configuration: Configuration) -> Result<Vec<Project>, Inv
/// Get information about a project.
#[tauri::command]
async fn get_project_info(configuration: Configuration, project_name: &str) -> Result<Project, InvokeError> {
async fn get_project_info(configuration: Configuration, project_path: &str) -> Result<Project, InvokeError> {
configuration
.get_project_info(project_name)
.get_project_info(project_path)
.await
.map_err(InvokeError::from_anyhow)
}
@ -328,6 +344,8 @@ fn main() -> Result<()> {
Ok(())
})
.invoke_handler(tauri::generate_handler![
get_state,
set_state,
get_initial_default_dir,
initialize_project_directory,
create_new_project_directory,
@ -342,6 +360,113 @@ fn main() -> Result<()> {
read_project_settings_file,
write_project_settings_file,
])
.plugin(tauri_plugin_cli::init())
.setup(|app| {
let mut verbose = false;
let mut source_path: Option<PathBuf> = None;
match app.cli().matches() {
// `matches` here is a Struct with { args, subcommand }.
// `args` is `HashMap<String, ArgData>` where `ArgData` is a struct with { value, occurrences }.
// `subcommand` is `Option<Box<SubcommandMatches>>` where `SubcommandMatches` is a struct with { name, matches }.
Ok(matches) => {
if let Some(verbose_flag) = matches.args.get("verbose") {
let Some(value) = verbose_flag.value.as_bool() else {
return Err(
anyhow::anyhow!("Error parsing CLI arguments: verbose flag is not a boolean").into(),
);
};
verbose = value;
}
// Get the path we are trying to open.
if let Some(source_arg) = matches.args.get("source") {
// We don't do an else here because this can be null.
if let Some(value) = source_arg.value.as_str() {
source_path = Some(Path::new(value).to_path_buf());
}
}
}
Err(err) => {
return Err(anyhow::anyhow!("Error parsing CLI arguments: {:?}", err).into());
}
}
// If we have a source path to open, make sure it exists.
let Some(source_path) = source_path else {
// The user didn't provide a source path to open.
// Run the app as normal.
app.manage(state::Store::default());
return Ok(());
};
if !source_path.exists() {
return Err(anyhow::anyhow!(
"Error: the path `{}` you are trying to open does not exist",
source_path.display()
)
.into());
}
let runner: tauri::async_runtime::JoinHandle<Result<ProjectState>> =
tauri::async_runtime::spawn(async move {
// If the path is a directory, let's assume it is a project directory.
if source_path.is_dir() {
// Load the details about the project from the path.
let project = Project::from_path(&source_path).await.map_err(|e| {
anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e)
})?;
if verbose {
println!("Project loaded from path: {}", source_path.display());
}
// Create the default file in the project.
// Write the initial project file.
let project_file = source_path.join(DEFAULT_PROJECT_KCL_FILE);
tokio::fs::write(&project_file, vec![]).await?;
return Ok(ProjectState {
project,
current_file: Some(project_file.display().to_string()),
});
}
// We were given a file path, not a directory.
// Let's get the parent directory of the file.
let parent = source_path.parent().ok_or_else(|| {
anyhow::anyhow!(
"Error getting the parent directory of the file: {}",
source_path.display()
)
})?;
// Load the details about the project from the parent directory.
let project = Project::from_path(&parent).await.map_err(|e| {
anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e)
})?;
if verbose {
println!(
"Project loaded from path: {}, current file: {}",
parent.display(),
source_path.display()
);
}
Ok(ProjectState {
project,
current_file: Some(source_path.display().to_string()),
})
});
// Block on the handle.
let store = tauri::async_runtime::block_on(runner)??;
// Create a state object to hold the project.
app.manage(state::Store::new(store));
Ok(())
})
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_http::init())

21
src-tauri/src/state.rs Normal file
View File

@ -0,0 +1,21 @@
//! State management for the application.
use kcl_lib::settings::types::file::ProjectState;
use tokio::sync::Mutex;
#[derive(Debug, Default)]
pub struct Store(Mutex<Option<ProjectState>>);
impl Store {
pub fn new(p: ProjectState) -> Self {
Self(Mutex::new(Some(p)))
}
pub async fn get(&self) -> Option<ProjectState> {
self.0.lock().await.clone()
}
pub async fn set(&self, p: Option<ProjectState>) {
*self.0.lock().await = p;
}
}

View File

@ -50,6 +50,22 @@
},
"identifier": "dev.zoo.modeling-app",
"plugins": {
"cli": {
"description": "Zoo Modeling App CLI",
"args": [
{
"short": "v",
"name": "verbose",
"description": "Verbosity level"
},
{
"name": "source",
"index": 1,
"takesValue": true
}
],
"subcommands": {}
},
"shell": {
"open": true
}

View File

@ -30,6 +30,7 @@ import SettingsAuthProvider from 'components/SettingsAuthProvider'
import LspProvider from 'components/LspProvider'
import { KclContextProvider } from 'lang/KclProvider'
import { BROWSER_PROJECT_NAME } from 'lib/constants'
import { getState, setState } from 'lib/tauri'
const router = createBrowserRouter([
{
@ -52,10 +53,30 @@ const router = createBrowserRouter([
children: [
{
path: paths.INDEX,
loader: () =>
isTauri()
loader: async () => {
const inTauri = isTauri()
if (inTauri) {
const appState = await getState()
if (appState) {
console.log('appState', appState)
// Reset the state.
// We do this so that we load the initial state from the cli but everything
// else we can ignore.
await setState(undefined)
// Redirect to the file if we have a file path.
if (appState.current_file) {
return redirect(
paths.FILE + '/' + encodeURIComponent(appState.current_file)
)
}
}
}
return inTauri
? redirect(paths.HOME)
: redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME),
: redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME)
},
},
{
loader: fileLoader,

View File

@ -62,7 +62,7 @@ export const FileMachineProvider = ({
services: {
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
const newFiles = isTauri()
? (await getProjectInfo(context.project.name)).children
? (await getProjectInfo(context.project.path)).children
: []
return {
...context.project,

View File

@ -25,7 +25,9 @@ import { createSettings } from './settings/initialSettings'
// occurred during the settings load
export const settingsLoader: LoaderFunction = async ({
params,
}): Promise<ReturnType<typeof createSettings>> => {
}): Promise<
ReturnType<typeof createSettings> | ReturnType<typeof redirect>
> => {
let { settings } = await loadAndValidateSettings()
// I don't love that we have to read the settings again here,
@ -105,7 +107,7 @@ export const fileLoader: LoaderFunction = async ({
const projectData: IndexLoaderData = {
code,
project: isTauri()
? await getProjectInfo(projectName)
? await getProjectInfo(projectPath)
: {
name: projectName,
path: projectPath,

View File

@ -6,6 +6,17 @@ import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
// Get the app state from tauri.
export async function getState(): Promise<ProjectState | undefined> {
return await invoke<ProjectState | undefined>('get_state')
}
// Set the app state in tauri.
export async function setState(state: ProjectState | undefined): Promise<void> {
return await invoke('set_state', { state })
}
// Get the initial default dir for holding all projects.
export async function getInitialDefaultDir(): Promise<string> {
@ -53,7 +64,7 @@ export async function listProjects(
}
export async function getProjectInfo(
projectName: string,
projectPath: string,
configuration?: Configuration
): Promise<Project> {
if (!configuration) {
@ -61,7 +72,7 @@ export async function getProjectInfo(
}
return await invoke<Project>('get_project_info', {
configuration,
projectName,
projectPath,
})
}

View File

@ -1895,7 +1895,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.1.51"
version = "0.1.52"
dependencies = [
"anyhow",
"approx 0.5.1",

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.1.51"
version = "0.1.52"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -5,6 +5,15 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// State management for the application.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct ProjectState {
pub project: Project,
pub current_file: Option<String>,
}
/// Information about project.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
@ -23,6 +32,34 @@ pub struct Project {
}
impl Project {
#[cfg(not(target_arch = "wasm32"))]
/// Populate a project from a path.
pub async fn from_path<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
// Check if they are using '.' as the path.
let path = if path.as_ref() == std::path::Path::new(".") {
std::env::current_dir()?
} else {
path.as_ref().to_path_buf()
};
// Make sure the path exists.
if !path.exists() {
return Err(anyhow::anyhow!("Path does not exist"));
}
let file = crate::settings::utils::walk_dir(&path).await?;
let metadata = std::fs::metadata(path).ok().map(|m| m.into());
let mut project = Self {
file,
metadata,
kcl_file_count: 0,
directory_count: 0,
};
project.populate_kcl_file_count()?;
project.populate_directory_count()?;
Ok(project)
}
/// Populate the number of KCL files in the project.
pub fn populate_kcl_file_count(&mut self) -> Result<()> {
let mut count = 0;

View File

@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
use validator::{Validate, ValidateRange};
const DEFAULT_THEME_COLOR: f64 = 264.5;
const DEFAULT_PROJECT_KCL_FILE: &str = "main.kcl";
pub const DEFAULT_PROJECT_KCL_FILE: &str = "main.kcl";
const DEFAULT_PROJECT_NAME_TEMPLATE: &str = "project-$nnn";
/// High level configuration.
@ -129,7 +129,7 @@ impl Configuration {
continue;
}
projects.push(self.get_project_info(&e.file_name().to_string_lossy()).await?);
projects.push(self.get_project_info(&e.path().display().to_string()).await?);
}
Ok(projects)
@ -137,21 +137,20 @@ impl Configuration {
#[cfg(not(target_arch = "wasm32"))]
/// Get information about a project.
pub async fn get_project_info(&self, project_name: &str) -> Result<crate::settings::types::file::Project> {
let main_dir = &self.ensure_project_directory_exists().await?;
if project_name.is_empty() {
return Err(anyhow::anyhow!("Project name cannot be empty."));
pub async fn get_project_info(&self, project_path: &str) -> Result<crate::settings::types::file::Project> {
// Check the directory.
let project_dir = std::path::Path::new(project_path);
if !project_dir.exists() {
return Err(anyhow::anyhow!("Project directory does not exist: {}", project_path));
}
// Check the directory.
let project_dir = main_dir.join(project_name);
if !project_dir.exists() {
return Err(anyhow::anyhow!("Project directory does not exist."));
// Make sure it is a directory.
if !project_dir.is_dir() {
return Err(anyhow::anyhow!("Project path is not a directory: {}", project_path));
}
let mut project = crate::settings::types::file::Project {
file: crate::settings::utils::walk_dir(&project_dir).await?,
file: crate::settings::utils::walk_dir(project_dir).await?,
metadata: Some(tokio::fs::metadata(&project_dir).await?.into()),
kcl_file_count: 0,
directory_count: 0,

View File

@ -1,6 +1,6 @@
//! Utility functions for settings.
use std::path::PathBuf;
use std::path::Path;
use anyhow::Result;
@ -8,20 +8,21 @@ use crate::settings::types::file::FileEntry;
/// Walk a directory recursively and return a list of all files.
#[async_recursion::async_recursion]
pub async fn walk_dir(dir: &PathBuf) -> Result<FileEntry> {
pub async fn walk_dir<P: AsRef<Path> + Send>(dir: P) -> Result<FileEntry> {
let mut entry = FileEntry {
name: dir
.as_ref()
.file_name()
.ok_or_else(|| anyhow::anyhow!("No file name"))?
.to_string_lossy()
.to_string(),
path: dir.display().to_string(),
path: dir.as_ref().display().to_string(),
children: None,
};
let mut children = vec![];
let mut entries = tokio::fs::read_dir(&dir).await?;
let mut entries = tokio::fs::read_dir(&dir.as_ref()).await?;
while let Some(e) = entries.next_entry().await? {
if e.file_type().await?.is_dir() {
children.push(walk_dir(&e.path()).await?);
@ -34,9 +35,8 @@ pub async fn walk_dir(dir: &PathBuf) -> Result<FileEntry> {
}
}
if !children.is_empty() {
// We don't set this to none if there are no children, because it's a directory.
entry.children = Some(children);
}
Ok(entry)
}