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:
Jess Frazelle
2024-04-25 00:13:09 -07:00
committed by GitHub
parent dcbed4f06f
commit 1afed68dd7
79 changed files with 3966 additions and 1923 deletions

View File

@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
dir: ['src/wasm-lib']
dir: ['src/wasm-lib', 'src-tauri']
steps:
- uses: actions/checkout@v4
- name: Install latest rust
@ -31,9 +31,22 @@ jobs:
- name: install dependencies
if: matrix.dir == 'src-tauri'
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
sudo apt-get install -y \
libgtk-3-dev \
libayatana-appindicator3-dev \
webkit2gtk-driver \
libsoup-3.0-dev \
libjavascriptcoregtk-4.1-dev \
libwebkit2gtk-4.1-dev \
at-spi2-core \
xvfb
yarn install
yarn build:wasm
yarn build:local
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1

57
.github/workflows/cargo-test-tauri.yml vendored Normal file
View File

@ -0,0 +1,57 @@
on:
push:
branches:
- main
paths:
- 'src-tauri/**.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- .github/workflows/cargo-test-tauri.yml
pull_request:
paths:
- 'src-tauri/**.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- .github/workflows/cargo-test-tauri.yml
workflow_dispatch:
permissions: read-all
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
name: cargo test of tauri
jobs:
cargotest:
name: cargo test
runs-on: ubuntu-latest-8-cores
strategy:
matrix:
dir: ['src-tauri']
steps:
- uses: actions/checkout@v4
- name: Install latest rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: install dependencies
if: matrix.dir == 'src-tauri'
run: |
sudo apt-get update
sudo apt-get install -y \
libgtk-3-dev \
libayatana-appindicator3-dev \
webkit2gtk-driver \
libsoup-3.0-dev \
libjavascriptcoregtk-4.1-dev \
libwebkit2gtk-4.1-dev \
at-spi2-core \
xvfb
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1
- name: cargo test
shell: bash
run: |-
cd "${{ matrix.dir }}"
cargo test --all

View File

@ -147,16 +147,16 @@ jobs:
- name: Install ubuntu system dependencies
if: matrix.os == 'ubuntu-latest'
run: >
sudo apt-get update &&
sudo apt-get install -y
libgtk-3-dev
libayatana-appindicator3-dev
webkit2gtk-driver
libsoup-3.0-dev
libjavascriptcoregtk-4.1-dev
libwebkit2gtk-4.1-dev
at-spi2-core
run: |
sudo apt-get update
sudo apt-get install -y \
libgtk-3-dev \
libayatana-appindicator3-dev \
webkit2gtk-driver \
libsoup-3.0-dev \
libjavascriptcoregtk-4.1-dev \
libwebkit2gtk-4.1-dev \
at-spi2-core \
xvfb
- name: Sync node version and setup cache

View File

@ -596,13 +596,12 @@ test('Auto complete works', async ({ page }) => {
test('Stored settings are validated and fall back to defaults', async ({
page,
context,
}) => {
const u = getUtils(page)
// Override beforeEach test setup
// with corrupted settings
await context.addInitScript(
await page.addInitScript(
async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
},
@ -619,18 +618,18 @@ test('Stored settings are validated and fall back to defaults', async ({
// Check the settings were reset
const storedSettings = TOML.parse(
await page.evaluate(
({ settingsKey }) => localStorage.getItem(settingsKey) || '{}',
({ settingsKey }) => localStorage.getItem(settingsKey) || '',
{ settingsKey: TEST_SETTINGS_KEY }
)
) as { settings: SaveSettingsPayload }
expect(storedSettings.settings.app?.theme).toBe('dark')
expect(storedSettings.settings?.app?.theme).toBe(undefined)
// Check that the invalid settings were removed
expect(storedSettings.settings.modeling?.defaultUnit).toBe(undefined)
expect(storedSettings.settings.modeling?.mouseControls).toBe(undefined)
expect(storedSettings.settings.app?.projectDirectory).toBe(undefined)
expect(storedSettings.settings.projects?.defaultProjectName).toBe(undefined)
expect(storedSettings.settings?.modeling?.defaultUnit).toBe(undefined)
expect(storedSettings.settings?.modeling?.mouseControls).toBe(undefined)
expect(storedSettings.settings?.app?.projectDirectory).toBe(undefined)
expect(storedSettings.settings?.projects?.defaultProjectName).toBe(undefined)
})
test('Project settings can be set and override user settings', async ({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -1,7 +1,7 @@
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { Themes } from 'lib/theme'
export const TEST_SETTINGS_KEY = '/user.toml'
export const TEST_SETTINGS_KEY = '/settings.toml'
export const TEST_SETTINGS = {
app: {
theme: Themes.Dark,
@ -24,7 +24,7 @@ export const TEST_SETTINGS = {
export const TEST_SETTINGS_ONBOARDING = {
...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export ' },
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export' },
} satisfies Partial<SaveSettingsPayload>
export const TEST_SETTINGS_CORRUPTED = {

View File

@ -71,7 +71,7 @@ describe('ZMA (Tauri, Linux)', () => {
// Now should be signed in
const newFileButton = await $('[data-testid="home-new-file"]')
expect(await newFileButton.getText()).toEqual('New file')
expect(await newFileButton.getText()).toEqual('New project')
})
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {

View File

@ -18,7 +18,7 @@ export default defineConfig({
/* Retry on CI only */
retries: process.env.CI ? 3 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 2 : 1,
workers: process.env.CI ? 1 : 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */

2288
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,6 @@ repository = "https://github.com/KittyCAD/modeling-app"
default-run = "app"
edition = "2021"
rust-version = "1.70"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
@ -16,9 +15,9 @@ tauri-build = { version = "2.0.0-beta.13", features = [] }
[dependencies]
anyhow = "1"
kcl-lib = { version = "0.1.51", path = "../src/wasm-lib/kcl" }
kittycad = "0.3.0"
oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
tauri-plugin-dialog = { version = "2.0.0-beta.6" }
@ -28,7 +27,7 @@ tauri-plugin-os = { version = "2.0.0-beta.2" }
tauri-plugin-process = { version = "2.0.0-beta.2" }
tauri-plugin-shell = { version = "2.0.0-beta.2" }
tauri-plugin-updater = { version = "2.0.0-beta.4" }
tokio = { version = "1.37.0", features = ["time"] }
tokio = { version = "1.37.0", features = ["time", "fs"] }
toml = "0.8.2"
[features]

6
src-tauri/rustfmt.toml Normal file
View File

@ -0,0 +1,6 @@
max_width = 120
edition = "2018"
format_code_in_doc_comments = true
format_strings = false
imports_granularity = "Crate"
group_imports = "StdExternalCrate"

View File

@ -1,91 +1,189 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::env;
use std::fs;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use std::{
env,
path::{Path, PathBuf},
process::Command,
};
use anyhow::Result;
use kcl_lib::settings::types::{
file::{FileEntry, Project},
project::ProjectConfiguration,
Configuration,
};
use oauth2::TokenResponse;
use serde::Serialize;
use std::process::Command;
use tauri::ipc::InvokeError;
use tauri::{ipc::InvokeError, Manager};
use tauri_plugin_shell::ShellExt;
const DEFAULT_HOST: &str = "https://api.kittycad.io";
/// This command returns the a json string parse from a toml file at the path.
const DEFAULT_HOST: &str = "https://api.zoo.dev";
const SETTINGS_FILE_NAME: &str = "settings.toml";
const PROJECT_SETTINGS_FILE_NAME: &str = "project.toml";
const PROJECT_FOLDER: &str = "zoo-modeling-app-projects";
#[tauri::command]
fn read_toml(path: &str) -> Result<String, InvokeError> {
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
let mut contents = String::new();
file.read_to_string(&mut contents)
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
let value =
toml::from_str::<toml::Value>(&contents).map_err(|e| InvokeError::from_anyhow(e.into()))?;
let value = serde_json::to_string(&value).map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(value)
fn get_initial_default_dir(app: tauri::AppHandle) -> Result<PathBuf, InvokeError> {
let dir = match app.path().document_dir() {
Ok(dir) => dir,
Err(_) => {
// for headless Linux (eg. Github Actions)
let home_dir = app.path().home_dir()?;
home_dir.join("Documents")
}
};
Ok(dir.join(PROJECT_FOLDER))
}
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
/// Removed from tauri v2
#[derive(Debug, Serialize)]
pub struct DiskEntry {
/// The path to the entry.
pub path: PathBuf,
/// The name of the entry (file name with extension or directory name).
pub name: Option<String>,
/// The children of this entry if it's a directory.
#[serde(skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<DiskEntry>>,
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))
}
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
/// Removed from tauri v2
fn is_dir<P: AsRef<Path>>(path: P) -> Result<bool> {
std::fs::metadata(path)
.map(|md| md.is_dir())
.map_err(Into::into)
}
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
/// Removed from tauri v2
#[tauri::command]
fn read_dir_recursive(path: &str) -> Result<Vec<DiskEntry>, InvokeError> {
let mut files_and_dirs: Vec<DiskEntry> = vec![];
// let path = path.as_ref();
for entry in fs::read_dir(path).map_err(|e| InvokeError::from_anyhow(e.into()))? {
let path = entry
.map_err(|e| InvokeError::from_anyhow(e.into()))?
.path();
async fn read_app_settings_file(app: tauri::AppHandle) -> Result<Configuration, InvokeError> {
let mut settings_path = get_app_settings_file_path(&app)?;
let mut needs_migration = false;
if let Ok(flag) = is_dir(&path) {
files_and_dirs.push(DiskEntry {
path: path.clone(),
children: if flag {
Some(read_dir_recursive(path.to_str().expect("No path"))?)
} else {
None
},
name: path
.file_name()
.map(|name| name.to_string_lossy())
.map(|name| name.to_string()),
});
// Check if this file exists.
if !settings_path.exists() {
// Try the backwards compatible path.
// TODO: Remove this after a few releases.
let app_config_dir = app.path().app_config_dir()?;
settings_path = format!(
"{}user.toml",
app_config_dir.display().to_string().trim_end_matches('/')
)
.into();
needs_migration = true;
// Check if this path exists.
if !settings_path.exists() {
let mut default = Configuration::default();
default.settings.project.directory = get_initial_default_dir(app.clone())?;
// Return the default configuration.
return Ok(default);
}
}
Ok(files_and_dirs)
let contents = tokio::fs::read_to_string(&settings_path)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
let mut parsed = Configuration::backwards_compatible_toml_parse(&contents).map_err(InvokeError::from_anyhow)?;
if parsed.settings.project.directory == PathBuf::new() {
parsed.settings.project.directory = get_initial_default_dir(app.clone())?;
}
// TODO: Remove this after a few releases.
if needs_migration {
write_app_settings_file(app, parsed.clone()).await?;
// Delete the old file.
tokio::fs::remove_file(settings_path)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
Ok(parsed)
}
/// This command returns a string that is the contents of a file at the path.
#[tauri::command]
fn read_txt_file(path: &str) -> Result<String, InvokeError> {
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
let mut contents = String::new();
file.read_to_string(&mut contents)
async fn write_app_settings_file(app: tauri::AppHandle, configuration: Configuration) -> Result<(), InvokeError> {
let settings_path = get_app_settings_file_path(&app)?;
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
tokio::fs::write(settings_path, contents.as_bytes())
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(contents)
Ok(())
}
fn get_project_settings_file_path(app_settings: Configuration, project_name: &str) -> Result<PathBuf, InvokeError> {
Ok(app_settings
.settings
.project
.directory
.join(project_name)
.join(PROJECT_SETTINGS_FILE_NAME))
}
#[tauri::command]
async fn read_project_settings_file(
app_settings: Configuration,
project_name: &str,
) -> Result<ProjectConfiguration, InvokeError> {
let settings_path = get_project_settings_file_path(app_settings, project_name)?;
// Check if this file exists.
if !settings_path.exists() {
// Return the default configuration.
return Ok(ProjectConfiguration::default());
}
let contents = tokio::fs::read_to_string(&settings_path)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
let parsed = ProjectConfiguration::backwards_compatible_toml_parse(&contents).map_err(InvokeError::from_anyhow)?;
Ok(parsed)
}
#[tauri::command]
async fn write_project_settings_file(
app_settings: Configuration,
project_name: &str,
configuration: ProjectConfiguration,
) -> Result<(), InvokeError> {
let settings_path = get_project_settings_file_path(app_settings, project_name)?;
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
tokio::fs::write(settings_path, contents.as_bytes())
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(())
}
/// Initialize the directory that holds all the projects.
#[tauri::command]
async fn initialize_project_directory(configuration: Configuration) -> Result<PathBuf, InvokeError> {
configuration
.ensure_project_directory_exists()
.await
.map_err(InvokeError::from_anyhow)
}
/// Create a new project directory.
#[tauri::command]
async fn create_new_project_directory(
configuration: Configuration,
project_name: &str,
initial_code: Option<&str>,
) -> Result<Project, InvokeError> {
configuration
.create_new_project_directory(project_name, initial_code)
.await
.map_err(InvokeError::from_anyhow)
}
/// List all the projects in the project directory.
#[tauri::command]
async fn list_projects(configuration: Configuration) -> Result<Vec<Project>, InvokeError> {
configuration.list_projects().await.map_err(InvokeError::from_anyhow)
}
/// Get information about a project.
#[tauri::command]
async fn get_project_info(configuration: Configuration, project_name: &str) -> Result<Project, InvokeError> {
configuration
.get_project_info(project_name)
.await
.map_err(InvokeError::from_anyhow)
}
#[tauri::command]
async fn read_dir_recursive(path: &str) -> Result<FileEntry, InvokeError> {
kcl_lib::settings::utils::walk_dir(&Path::new(path).to_path_buf())
.await
.map_err(InvokeError::from_anyhow)
}
/// This command instantiates a new window with auth.
@ -103,8 +201,7 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
let auth_client = oauth2::basic::BasicClient::new(
oauth2::ClientId::new(client_id),
None,
oauth2::AuthUrl::new(format!("{host}/authorize"))
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
oauth2::AuthUrl::new(format!("{host}/authorize")).map_err(|e| InvokeError::from_anyhow(e.into()))?,
Some(
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
@ -132,12 +229,10 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
// and bypass the shell::open call as it fails on GitHub Actions.
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
if e2e_tauri_enabled {
println!(
"E2E_TAURI_ENABLED is set, won't open {} externally",
auth_uri.secret()
);
fs::write("/tmp/kittycad_user_code", details.user_code().secret())
.expect("Unable to write /tmp/kittycad_user_code file");
println!("E2E_TAURI_ENABLED is set, won't open {} externally", auth_uri.secret());
tokio::fs::write("/tmp/kittycad_user_code", details.user_code().secret())
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
} else {
app.shell()
.open(auth_uri.secret(), None)
@ -160,10 +255,7 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
///This command returns the KittyCAD user info given a token.
/// The string returned from this method is the user info as a json string.
#[tauri::command]
async fn get_user(
token: Option<String>,
hostname: &str,
) -> Result<kittycad::types::User, InvokeError> {
async fn get_user(token: &str, hostname: &str) -> Result<kittycad::types::User, InvokeError> {
// Use the host passed in if it's set.
// Otherwise, use the default host.
let host = if hostname.is_empty() {
@ -183,7 +275,7 @@ async fn get_user(
println!("Getting user info...");
// use kittycad library to fetch the user info from /user/me
let mut client = kittycad::Client::new(token.unwrap());
let mut client = kittycad::Client::new(token);
if baseurl != DEFAULT_HOST {
client.set_base_url(&baseurl);
@ -202,43 +294,53 @@ async fn get_user(
/// From this GitHub comment: https://github.com/tauri-apps/tauri/issues/4062#issuecomment-1338048169
/// But with the Linux support removed since we don't need it for now.
#[tauri::command]
fn show_in_folder(path: String) {
#[cfg(target_os = "windows")]
fn show_in_folder(path: &str) -> Result<(), InvokeError> {
#[cfg(not(unix))]
{
Command::new("explorer")
.args(["/select,", &path]) // The comma after select is not a typo
.spawn()
.unwrap();
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
#[cfg(target_os = "macos")]
#[cfg(unix)]
{
Command::new("open").args(["-R", &path]).spawn().unwrap();
Command::new("open")
.args(["-R", &path])
.spawn()
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
Ok(())
}
fn main() {
fn main() -> Result<()> {
tauri::Builder::default()
.setup(|_app| {
#[cfg(debug_assertions)]
{
use tauri::Manager;
_app.get_webview("main").unwrap().open_devtools();
}
#[cfg(not(debug_assertions))]
{
_app.handle()
.plugin(tauri_plugin_updater::Builder::new().build())?;
_app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
get_initial_default_dir,
initialize_project_directory,
create_new_project_directory,
list_projects,
get_project_info,
get_user,
login,
read_toml,
read_txt_file,
read_dir_recursive,
show_in_folder,
read_app_settings_file,
write_app_settings_file,
read_project_settings_file,
write_project_settings_file,
])
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
@ -246,6 +348,7 @@ fn main() {
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_shell::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
.run(tauri::generate_context!())?;
Ok(())
}

View File

@ -15,10 +15,10 @@ import {
import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine'
import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs'
import { readProject } from 'lib/tauriFS'
import { isTauri } from 'lib/isTauri'
import { join, sep } from '@tauri-apps/api/path'
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
import { getProjectInfo } from 'lib/tauri'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -62,7 +62,7 @@ export const FileMachineProvider = ({
services: {
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
const newFiles = isTauri()
? await readProject(context.project.path)
? (await getProjectInfo(context.project.name)).children
: []
return {
...context.project,

View File

@ -94,10 +94,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
if (isInProject) {
navigate('onboarding')
} else {
createAndOpenNewProject(
settings.context.app.projectDirectory.current,
navigate
)
createAndOpenNewProject(navigate)
}
}}
>

View File

@ -1,5 +1,4 @@
import { FormEvent, useEffect, useRef, useState } from 'react'
import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { paths } from 'lib/paths'
import { Link } from 'react-router-dom'
import { ActionButton } from './ActionButton'
@ -9,11 +8,11 @@ import {
faTrashAlt,
faX,
} from '@fortawesome/free-solid-svg-icons'
import { getPartsCount, readProject } from '../lib/tauriFS'
import { FILE_EXT } from 'lib/constants'
import { Dialog } from '@headlessui/react'
import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from './Tooltip'
import { Project } from 'wasm-lib/kcl/bindings/Project'
function ProjectCard({
project,
@ -21,17 +20,17 @@ function ProjectCard({
handleDeleteProject,
...props
}: {
project: ProjectWithEntryPointMetadata
project: Project
handleRenameProject: (
e: FormEvent<HTMLFormElement>,
f: ProjectWithEntryPointMetadata
f: Project
) => Promise<void>
handleDeleteProject: (f: ProjectWithEntryPointMetadata) => Promise<void>
handleDeleteProject: (f: Project) => Promise<void>
}) {
useHotkeys('esc', () => setIsEditing(false))
const [isEditing, setIsEditing] = useState(false)
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
const [numberOfParts, setNumberOfParts] = useState(1)
const [numberOfFiles, setNumberOfFiles] = useState(1)
const [numberOfFolders, setNumberOfFolders] = useState(0)
let inputRef = useRef<HTMLInputElement>(null)
@ -41,7 +40,8 @@ function ProjectCard({
void handleRenameProject(e, project).then(() => setIsEditing(false))
}
function getDisplayedTime(date: Date) {
function getDisplayedTime(dateStr: string) {
const date = new Date(dateStr)
const startOfToday = new Date()
startOfToday.setHours(0, 0, 0, 0)
return date.getTime() < startOfToday.getTime()
@ -50,15 +50,12 @@ function ProjectCard({
}
useEffect(() => {
async function getNumberOfParts() {
const { kclFileCount, kclDirCount } = getPartsCount(
await readProject(project.path)
)
setNumberOfParts(kclFileCount)
setNumberOfFolders(kclDirCount)
async function getNumberOfFiles() {
setNumberOfFiles(project.kcl_file_count)
setNumberOfFolders(project.directory_count)
}
void getNumberOfParts()
}, [project.path])
void getNumberOfFiles()
}, [project.kcl_file_count, project.directory_count])
useEffect(() => {
if (inputRef.current) {
@ -129,7 +126,7 @@ function ProjectCard({
{project.name?.replace(FILE_EXT, '')}
</Link>
<span className="text-chalkboard-60 text-xs">
{numberOfParts} part{numberOfParts === 1 ? '' : 's'}{' '}
{numberOfFiles} file{numberOfFiles === 1 ? '' : 's'}{' '}
{numberOfFolders > 0 &&
`/ ${numberOfFolders} folder${
numberOfFolders === 1 ? '' : 's'
@ -137,8 +134,8 @@ function ProjectCard({
</span>
<span className="text-chalkboard-60 text-xs">
Edited{' '}
{project.entrypointMetadata.mtime
? getDisplayedTime(project.entrypointMetadata.mtime)
{project.metadata && project.metadata?.modified
? getDisplayedTime(project.metadata.modified)
: 'never'}
</span>
<div className="absolute z-10 bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">

View File

@ -1,10 +1,10 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
import { APP_NAME } from 'lib/constants'
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
import { Project } from 'wasm-lib/kcl/bindings/Project'
const now = new Date()
const projectWellFormed = {
@ -14,29 +14,17 @@ const projectWellFormed = {
{
name: 'main.kcl',
path: '/some/path/Simple Box/main.kcl',
children: [],
},
],
entrypointMetadata: {
atime: now,
blksize: 32,
blocks: 32,
birthtime: now,
dev: 1,
gid: 1,
ino: 1,
isDirectory: false,
isFile: true,
isSymlink: false,
mode: 1,
mtime: now,
nlink: 1,
readonly: false,
rdev: 1,
metadata: {
created: now.toISOString(),
modified: now.toISOString(),
size: 32,
uid: 1,
fileAttributes: null,
},
} satisfies ProjectWithEntryPointMetadata
kcl_file_count: 1,
directory_count: 0,
} satisfies Project
describe('ProjectSidebarMenu tests', () => {
test('Renders the project name', () => {

View File

@ -133,13 +133,13 @@ function ProjectMenuPopover({
<p className="m-0 text-mono" data-testid="projectName">
{project?.name ? project.name : APP_NAME}
</p>
{project?.entrypointMetadata && (
{project?.metadata && project.metadata.created && (
<p
className="m-0 text-xs text-chalkboard-80 dark:text-chalkboard-40"
data-testid="createdAt"
>
Created{' '}
{project.entrypointMetadata.birthtime?.toLocaleDateString()}
{new Date(project.metadata.created).toLocaleDateString()}
</p>
)}
</div>

View File

@ -168,7 +168,7 @@ export const SettingsAuthProviderBase = ({
},
'Execute AST': () => kclManager.executeCode(true),
persistSettings: (context) =>
saveSettings(context, loadedProject?.project?.path),
saveSettings(context, loadedProject?.project?.name),
},
}
)

View File

@ -1,8 +1,7 @@
import { readFile, exists as tauriExists } from '@tauri-apps/plugin-fs'
import { isTauri } from 'lib/isTauri'
import { join } from '@tauri-apps/api/path'
import { invoke } from '@tauri-apps/api/core'
import { FileEntry } from 'lib/types'
import { readDirRecursive } from 'lib/tauri'
/// FileSystemManager is a class that provides a way to read files from the local file system.
/// It assumes that you are in a project since it is solely used by the std lib
@ -69,9 +68,7 @@ class FileSystemManager {
throw new Error(`Error joining dir: ${error}`)
})
.then((p) => {
invoke<FileEntry[]>('read_dir_recursive', {
path: p,
})
readDirRecursive(p)
.catch((error) => {
throw new Error(`Error reading dir: ${error}`)
})

View File

@ -10,7 +10,10 @@ import init, {
make_default_planes,
coredump,
toml_stringify,
toml_parse,
default_app_settings,
parse_app_settings,
parse_project_settings,
default_project_settings,
} from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
@ -26,6 +29,8 @@ import { CoreDumpManager } from 'lib/coredump'
import openWindow from 'lib/openWindow'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { TEST } from 'env'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Value } from '../wasm-lib/kcl/bindings/Value'
@ -349,11 +354,38 @@ export function tomlStringify(toml: any): string {
}
}
export function tomlParse(toml: string): any {
export function defaultAppSettings(): Configuration {
try {
const parsed: any = toml_parse(toml)
return parsed
const settings: Configuration = default_app_settings()
return settings
} catch (e: any) {
throw new Error(`Error parsing toml: ${e}`)
throw new Error(`Error getting default app settings: ${e}`)
}
}
export function parseAppSettings(toml: string): Configuration {
try {
const settings: Configuration = parse_app_settings(toml)
return settings
} catch (e: any) {
throw new Error(`Error parsing app settings: ${e}`)
}
}
export function defaultProjectSettings(): ProjectConfiguration {
try {
const settings: ProjectConfiguration = default_project_settings()
return settings
} catch (e: any) {
throw new Error(`Error getting default project settings: ${e}`)
}
}
export function parseProjectSettings(toml: string): ProjectConfiguration {
try {
const settings: ProjectConfiguration = parse_project_settings(toml)
return settings
} catch (e: any) {
throw new Error(`Error parsing project settings: ${e}`)
}
}

View File

@ -1,3 +1,5 @@
import { MouseControlType } from 'wasm-lib/kcl/bindings/MouseControlType'
const noModifiersPressed = (e: React.MouseEvent) =>
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
@ -20,6 +22,29 @@ export const cameraSystems: CameraSystem[] = [
'AutoCAD',
]
export function mouseControlsToCameraSystem(
mouseControl: MouseControlType | undefined
): CameraSystem | undefined {
switch (mouseControl) {
case 'kitty_cad':
return 'KittyCAD'
case 'on_shape':
return 'OnShape'
case 'trackpad_friendly':
return 'Trackpad Friendly'
case 'solidworks':
return 'Solidworks'
case 'nx':
return 'NX'
case 'creo':
return 'Creo'
case 'auto_cad':
return 'AutoCAD'
default:
return undefined
}
}
interface MouseGuardHandler {
description: string
callback: (e: React.MouseEvent) => boolean

View File

@ -8,8 +8,6 @@ export const MAX_PADDING = 7
* This is available for users to edit as a setting.
*/
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
/** The file name for settings files, both at the user and project level */
export const SETTINGS_FILE_EXT = '.toml'
/** Name given the temporary "project" in the browser version of the app */
export const BROWSER_PROJECT_NAME = 'browser'
/** Name given the temporary file in the browser version of the app */

View File

@ -1,10 +1,5 @@
import { ActionFunction, LoaderFunction, redirect } from 'react-router-dom'
import {
FileEntry,
FileLoaderData,
HomeLoaderData,
IndexLoaderData,
} from './types'
import { FileLoaderData, HomeLoaderData, IndexLoaderData } from './types'
import { isTauri } from './isTauri'
import { getProjectMetaByRouteId, paths } from './paths'
import { BROWSER_PATH } from 'lib/paths'
@ -14,24 +9,24 @@ import {
PROJECT_ENTRYPOINT,
} from 'lib/constants'
import { loadAndValidateSettings } from './settings/settingsUtils'
import {
getInitialDefaultDir,
getProjectsInDir,
initializeProjectDirectory,
} from './tauriFS'
import makeUrlPathRelative from './makeUrlPathRelative'
import { join, sep } from '@tauri-apps/api/path'
import { readTextFile, stat } from '@tauri-apps/plugin-fs'
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 { invoke } from '@tauri-apps/api/core'
import {
getProjectInfo,
initializeProjectDirectory,
listProjects,
} from './tauri'
import { createSettings } from './settings/initialSettings'
// The root loader simply resolves the settings and any errors that
// occurred during the settings load
export const settingsLoader: LoaderFunction = async ({
params,
}): ReturnType<typeof loadAndValidateSettings> => {
let settings = await loadAndValidateSettings()
}): Promise<ReturnType<typeof createSettings>> => {
let { settings } = await loadAndValidateSettings()
// I don't love that we have to read the settings again here,
// but we need to get the project path to load the project settings
@ -39,8 +34,9 @@ export const settingsLoader: LoaderFunction = async ({
const defaultDir = settings.app.projectDirectory.current || ''
const projectPathData = getProjectMetaByRouteId(params.id, defaultDir)
if (projectPathData) {
const { projectPath } = projectPathData
settings = await loadAndValidateSettings(projectPath)
const { projectName } = projectPathData
const { settings: s } = await loadAndValidateSettings(projectName)
settings = s
}
}
@ -49,7 +45,7 @@ export const settingsLoader: LoaderFunction = async ({
// Redirect users to the appropriate onboarding page if they haven't completed it
export const onboardingRedirectLoader: ActionFunction = async (args) => {
const settings = await loadAndValidateSettings()
const { settings } = await loadAndValidateSettings()
const onboardingStatus = settings.app.onboardingStatus.current || ''
const notEnRouteToOnboarding = !args.request.url.includes(
paths.ONBOARDING.INDEX
@ -73,7 +69,7 @@ export const onboardingRedirectLoader: ActionFunction = async (args) => {
export const fileLoader: LoaderFunction = async ({
params,
}): Promise<FileLoaderData | Response> => {
let settings = await loadAndValidateSettings()
let { settings } = await loadAndValidateSettings()
const defaultDir = settings.app.projectDirectory.current || '/'
const projectPathData = getProjectMetaByRouteId(params.id, defaultDir)
@ -94,12 +90,7 @@ export const fileLoader: LoaderFunction = async ({
// TODO: PROJECT_ENTRYPOINT is hardcoded
// until we support setting a project's entrypoint file
const code = await readTextFile(currentFilePath)
const entrypointMetadata = await stat(
await join(projectPath, PROJECT_ENTRYPOINT)
)
const children = await invoke<FileEntry[]>('read_dir_recursive', {
path: projectPath,
})
// Update both the state and the editor's code.
// We explicitly do not write to the file here since we are loading from
// the file system and not the editor.
@ -113,15 +104,19 @@ export const fileLoader: LoaderFunction = async ({
const projectData: IndexLoaderData = {
code,
project: {
project: isTauri()
? await getProjectInfo(projectName)
: {
name: projectName,
path: projectPath,
children,
entrypointMetadata,
children: [],
kcl_file_count: 0,
directory_count: 0,
},
file: {
name: currentFileName,
path: currentFilePath,
children: [],
},
}
@ -140,6 +135,7 @@ export const fileLoader: LoaderFunction = async ({
file: {
name: BROWSER_FILE_NAME,
path: decodeURIComponent(BROWSER_PATH),
children: [],
},
}
}
@ -152,14 +148,12 @@ export const homeLoader: LoaderFunction = async (): Promise<
if (!isTauri()) {
return redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME)
}
const settings = await loadAndValidateSettings()
const { configuration } = await loadAndValidateSettings()
const projectDir = await initializeProjectDirectory(
settings.app.projectDirectory.current || (await getInitialDefaultDir())
)
const projectDir = await initializeProjectDirectory(configuration)
if (projectDir.path) {
const projects = await getProjectsInDir(projectDir.path)
if (projectDir) {
const projects = await listProjects(configuration)
return {
projects,

View File

@ -1,100 +1,228 @@
import {
getInitialDefaultDir,
getSettingsFilePaths,
readSettingsFile,
} from '../tauriFS'
import { Setting, createSettings, settings } from 'lib/settings/initialSettings'
import { SaveSettingsPayload, SettingsLevel } from './settingsTypes'
import { isTauri } from 'lib/isTauri'
import { remove, writeTextFile, exists } from '@tauri-apps/plugin-fs'
import { initPromise, tomlParse, tomlStringify } from 'lang/wasm'
import {
defaultAppSettings,
defaultProjectSettings,
initPromise,
parseAppSettings,
parseProjectSettings,
tomlStringify,
} from 'lang/wasm'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { mouseControlsToCameraSystem } from 'lib/cameraControls'
import { appThemeToTheme } from 'lib/theme'
import {
readAppSettingsFile,
readProjectSettingsFile,
writeAppSettingsFile,
writeProjectSettingsFile,
} from 'lib/tauri'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { BROWSER_PROJECT_NAME } from 'lib/constants'
/**
* We expect the settings to be stored in a TOML file
* or TOML-formatted string in localStorage
* under a top-level [settings] key.
* @param path
* @returns
*/
function getSettingsFromStorage(path: string) {
return isTauri()
? readSettingsFile(path)
: (tomlParse(localStorage.getItem(path) ?? '')
.settings as Partial<SaveSettingsPayload>)
* Convert from a rust settings struct into the JS settings struct.
* We do this because the JS settings type has all the fancy shit
* for hiding and showing settings.
**/
function configurationToSettingsPayload(
configuration: Configuration
): Partial<SaveSettingsPayload> {
return {
app: {
theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
themeColor: configuration?.settings?.app?.appearance?.color
? configuration?.settings?.app?.appearance?.color.toString()
: undefined,
onboardingStatus: configuration?.settings?.app?.onboarding_status,
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
projectDirectory: configuration?.settings?.project?.directory,
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
},
modeling: {
defaultUnit: configuration?.settings?.modeling?.base_unit,
mouseControls: mouseControlsToCameraSystem(
configuration?.settings?.modeling?.mouse_controls
),
highlightEdges: configuration?.settings?.modeling?.highlight_edges,
showDebugPanel: configuration?.settings?.modeling?.show_debug_panel,
},
textEditor: {
textWrapping: configuration?.settings?.text_editor?.text_wrapping,
blinkingCursor: configuration?.settings?.text_editor?.blinking_cursor,
},
projects: {
defaultProjectName:
configuration?.settings?.project?.default_project_name,
},
commandBar: {
includeSettings: configuration?.settings?.command_bar?.include_settings,
},
}
}
export async function loadAndValidateSettings(projectPath?: string) {
const settings = createSettings()
settings.app.projectDirectory.default = await getInitialDefaultDir()
// First, get the settings data at the user and project level
const settingsFilePaths = await getSettingsFilePaths(projectPath)
function projectConfigurationToSettingsPayload(
configuration: ProjectConfiguration
): Partial<SaveSettingsPayload> {
return {
app: {
theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
themeColor: configuration?.settings?.app?.appearance?.color
? configuration?.settings?.app?.appearance?.color.toString()
: undefined,
onboardingStatus: configuration?.settings?.app?.onboarding_status,
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
},
modeling: {
defaultUnit: configuration?.settings?.modeling?.base_unit,
mouseControls: mouseControlsToCameraSystem(
configuration?.settings?.modeling?.mouse_controls
),
highlightEdges: configuration?.settings?.modeling?.highlight_edges,
showDebugPanel: configuration?.settings?.modeling?.show_debug_panel,
},
textEditor: {
textWrapping: configuration?.settings?.text_editor?.text_wrapping,
blinkingCursor: configuration?.settings?.text_editor?.blinking_cursor,
},
commandBar: {
includeSettings: configuration?.settings?.command_bar?.include_settings,
},
}
}
// Load the settings from the files
if (settingsFilePaths.user) {
function localStorageAppSettingsPath() {
return '/settings.toml'
}
function localStorageProjectSettingsPath() {
return '/' + BROWSER_PROJECT_NAME + '/project.toml'
}
function readLocalStorageAppSettingsFile(): Configuration {
// TODO: Remove backwards compatibility after a few releases.
let stored =
localStorage.getItem(localStorageAppSettingsPath()) ??
localStorage.getItem('/user.toml') ??
''
if (stored === '') {
return defaultAppSettings()
}
try {
return parseAppSettings(stored)
} catch (e) {
const settings = defaultAppSettings()
localStorage.setItem(localStorageAppSettingsPath(), tomlStringify(settings))
return settings
}
}
function readLocalStorageProjectSettingsFile(): ProjectConfiguration {
// TODO: Remove backwards compatibility after a few releases.
let stored = localStorage.getItem(localStorageProjectSettingsPath()) ?? ''
if (stored === '') {
return defaultProjectSettings()
}
try {
return parseProjectSettings(stored)
} catch (e) {
const settings = defaultProjectSettings()
localStorage.setItem(
localStorageProjectSettingsPath(),
tomlStringify(settings)
)
return settings
}
}
export interface AppSettings {
settings: ReturnType<typeof createSettings>
configuration: Configuration
}
export async function loadAndValidateSettings(
projectName?: string
): Promise<AppSettings> {
const settings = createSettings()
const inTauri = isTauri()
if (!inTauri) {
// Make sure we have wasm initialized.
await initPromise
const userSettings = await getSettingsFromStorage(settingsFilePaths.user)
if (userSettings) {
setSettingsAtLevel(settings, 'user', userSettings)
}
}
// Load the app settings from the file system or localStorage.
const appSettings = inTauri
? await readAppSettingsFile()
: readLocalStorageAppSettingsFile()
// Convert the app settings to the JS settings format.
const appSettingsPayload = configurationToSettingsPayload(appSettings)
setSettingsAtLevel(settings, 'user', appSettingsPayload)
// Load the project settings if they exist
if (settingsFilePaths.project) {
const projectSettings = await getSettingsFromStorage(
settingsFilePaths.project
)
if (projectSettings) {
setSettingsAtLevel(settings, 'project', projectSettings)
}
if (projectName) {
const projectSettings = inTauri
? await readProjectSettingsFile(appSettings, projectName)
: readLocalStorageProjectSettingsFile()
const projectSettingsPayload =
projectConfigurationToSettingsPayload(projectSettings)
setSettingsAtLevel(settings, 'project', projectSettingsPayload)
}
// Return the settings object
return settings
return { settings, configuration: appSettings }
}
export async function saveSettings(
allSettings: typeof settings,
projectPath?: string
) {
const settingsFilePaths = await getSettingsFilePaths(projectPath)
if (settingsFilePaths.user) {
const changedSettings = getChangedSettingsAtLevel(allSettings, 'user')
await writeOrClearPersistedSettings(settingsFilePaths.user, changedSettings)
}
if (settingsFilePaths.project) {
const changedSettings = getChangedSettingsAtLevel(allSettings, 'project')
await writeOrClearPersistedSettings(
settingsFilePaths.project,
changedSettings
)
}
}
async function writeOrClearPersistedSettings(
settingsFilePath: string,
changedSettings: Partial<SaveSettingsPayload>
projectName?: string
) {
// Make sure we have wasm initialized.
await initPromise
if (changedSettings && Object.keys(changedSettings).length) {
if (isTauri()) {
await writeTextFile(
settingsFilePath,
tomlStringify({ settings: changedSettings })
)
}
localStorage.setItem(
settingsFilePath,
tomlStringify({ settings: changedSettings })
)
const inTauri = isTauri()
// Get the user settings.
const jsAppSettings = getChangedSettingsAtLevel(allSettings, 'user')
const tomlString = tomlStringify({ settings: jsAppSettings })
// Parse this as a Configuration.
const appSettings = parseAppSettings(tomlString)
// Write the app settings.
if (inTauri) {
await writeAppSettingsFile(appSettings)
} else {
if (isTauri() && (await exists(settingsFilePath))) {
await remove(settingsFilePath)
localStorage.setItem(
localStorageAppSettingsPath(),
tomlStringify(appSettings)
)
}
localStorage.removeItem(settingsFilePath)
if (!projectName) {
// If we're not saving project settings, we're done.
return
}
// Get the project settings.
const jsProjectSettings = getChangedSettingsAtLevel(allSettings, 'project')
const projectTomlString = tomlStringify({ settings: jsProjectSettings })
// Parse this as a Configuration.
const projectSettings = parseProjectSettings(projectTomlString)
// Write the project settings.
if (inTauri) {
await writeProjectSettingsFile(appSettings, projectName, projectSettings)
} else {
localStorage.setItem(
localStorageProjectSettingsPath(),
tomlStringify(projectSettings)
)
}
}

View File

@ -3,7 +3,7 @@ import {
faArrowUp,
faCircle,
} from '@fortawesome/free-solid-svg-icons'
import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { Project } from 'wasm-lib/kcl/bindings/Project'
const DESC = ':desc'
@ -27,10 +27,7 @@ export function getNextSearchParams(currentSort: string, newSort: string) {
}
export function getSortFunction(sortBy: string) {
const sortByName = (
a: ProjectWithEntryPointMetadata,
b: ProjectWithEntryPointMetadata
) => {
const sortByName = (a: Project, b: Project) => {
if (a.name && b.name) {
return sortBy.includes('desc')
? a.name.localeCompare(b.name)
@ -39,16 +36,13 @@ export function getSortFunction(sortBy: string) {
return 0
}
const sortByModified = (
a: ProjectWithEntryPointMetadata,
b: ProjectWithEntryPointMetadata
) => {
if (a.entrypointMetadata?.mtime && b.entrypointMetadata?.mtime) {
const sortByModified = (a: Project, b: Project) => {
if (a.metadata?.modified && b.metadata?.modified) {
const aDate = new Date(a.metadata.modified)
const bDate = new Date(b.metadata.modified)
return !sortBy || sortBy.includes('desc')
? b.entrypointMetadata.mtime.getTime() -
a.entrypointMetadata.mtime.getTime()
: a.entrypointMetadata.mtime.getTime() -
b.entrypointMetadata.mtime.getTime()
? bDate.getTime() - aDate.getTime()
: aDate.getTime() - bDate.getTime()
}
return 0
}

128
src/lib/tauri.ts Normal file
View File

@ -0,0 +1,128 @@
// This file contains wrappers around the tauri commands we define in rust code.
import { Models } from '@kittycad/lib/dist/types/src'
import { invoke } from '@tauri-apps/api/core'
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'
// Get the initial default dir for holding all projects.
export async function getInitialDefaultDir(): Promise<string> {
return invoke<string>('get_initial_default_dir')
}
export async function showInFolder(path: string | undefined): Promise<void> {
if (!path) {
console.error('path is undefined cannot call tauri showInFolder')
return
}
return await invoke('show_in_folder', { path })
}
export async function initializeProjectDirectory(
settings: Configuration
): Promise<string> {
return await invoke<string>('initialize_project_directory', {
configuration: settings,
})
}
export async function createNewProjectDirectory(
projectName: string,
initialCode?: string,
configuration?: Configuration
): Promise<Project> {
if (!configuration) {
configuration = await readAppSettingsFile()
}
return await invoke<Project>('create_new_project_directory', {
configuration,
projectName,
initialCode,
})
}
export async function listProjects(
configuration?: Configuration
): Promise<Project[]> {
if (!configuration) {
configuration = await readAppSettingsFile()
}
return await invoke<Project[]>('list_projects', { configuration })
}
export async function getProjectInfo(
projectName: string,
configuration?: Configuration
): Promise<Project> {
if (!configuration) {
configuration = await readAppSettingsFile()
}
return await invoke<Project>('get_project_info', {
configuration,
projectName,
})
}
export async function login(host: string): Promise<string> {
return await invoke('login', { host })
}
export async function getUser(
token: string | undefined,
host: string
): Promise<Models['User_type'] | Record<'error_code', unknown> | void> {
if (!token) {
console.error('token is undefined cannot call tauri getUser')
return
}
return await invoke<Models['User_type'] | Record<'error_code', unknown>>(
'get_user',
{
token: token,
hostname: host,
}
).catch((err) => console.error('error from Tauri getUser', err))
}
export async function readDirRecursive(path: string): Promise<FileEntry[]> {
return await invoke<FileEntry[]>('read_dir_recursive', { path })
}
// Read the contents of the app settings.
export async function readAppSettingsFile(): Promise<Configuration> {
return await invoke<Configuration>('read_app_settings_file')
}
// Write the contents of the app settings.
export async function writeAppSettingsFile(
settings: Configuration
): Promise<void> {
return await invoke('write_app_settings_file', { configuration: settings })
}
// Read project settings file.
export async function readProjectSettingsFile(
appSettings: Configuration,
projectName: string
): Promise<ProjectConfiguration> {
return await invoke<ProjectConfiguration>('read_project_settings_file', {
appSettings,
projectName,
})
}
// Write project settings file.
export async function writeProjectSettingsFile(
appSettings: Configuration,
projectName: string,
settings: ProjectConfiguration
): Promise<void> {
return await invoke('write_project_settings_file', {
appSettings,
projectName,
configuration: settings,
})
}

View File

@ -1,11 +1,4 @@
import {
deepFileFilter,
getNextProjectIndex,
getPartsCount,
interpolateProjectNameWithIndex,
isRelevantFileOrDir,
} from './tauriFS'
import type { FileEntry } from './types'
import { getNextProjectIndex, interpolateProjectNameWithIndex } from './tauriFS'
import { MAX_PADDING } from './constants'
describe('Test project name utility functions', () => {
@ -31,18 +24,22 @@ describe('Test project name utility functions', () => {
{
name: 'new-project-04.kcl',
path: '/projects/new-project-04.kcl',
children: [],
},
{
name: 'new-project-007.kcl',
path: '/projects/new-project-007.kcl',
children: [],
},
{
name: 'new-project-05.kcl',
path: '/projects/new-project-05.kcl',
children: [],
},
{
name: 'new-project-0.kcl',
path: '/projects/new-project-0.kcl',
children: [],
},
]
@ -50,101 +47,3 @@ describe('Test project name utility functions', () => {
expect(getNextProjectIndex('new-project-$n', testFiles)).toBe(8)
})
})
describe('Test file tree utility functions', () => {
const baseFiles: FileEntry[] = [
{
name: 'show-me.kcl',
path: '/projects/show-me.kcl',
},
{
name: 'hide-me.jpg',
path: '/projects/hide-me.jpg',
},
{
name: '.gitignore',
path: '/projects/.gitignore',
},
]
const filteredBaseFiles: FileEntry[] = [
{
name: 'show-me.kcl',
path: '/projects/show-me.kcl',
},
]
it('Only includes files relevant to the project in a flat directory', () => {
expect(deepFileFilter(baseFiles, isRelevantFileOrDir)).toEqual(
filteredBaseFiles
)
})
const nestedFiles: FileEntry[] = [
...baseFiles,
{
name: 'show-me',
path: '/projects/show-me',
children: [
{
name: 'show-me-nested',
path: '/projects/show-me/show-me-nested',
children: baseFiles,
},
{
name: 'hide-me',
path: '/projects/show-me/hide-me',
children: baseFiles.filter((file) => file.name !== 'show-me.kcl'),
},
],
},
{
name: 'hide-me',
path: '/projects/hide-me',
children: baseFiles.filter((file) => file.name !== 'show-me.kcl'),
},
]
const filteredNestedFiles: FileEntry[] = [
...filteredBaseFiles,
{
name: 'show-me',
path: '/projects/show-me',
children: [
{
name: 'show-me-nested',
path: '/projects/show-me/show-me-nested',
children: filteredBaseFiles,
},
],
},
]
it('Only includes directories that include files relevant to the project in a nested directory', () => {
expect(deepFileFilter(nestedFiles, isRelevantFileOrDir)).toEqual(
filteredNestedFiles
)
})
const withHiddenDir: FileEntry[] = [
...baseFiles,
{
name: '.hide-me',
path: '/projects/.hide-me',
children: baseFiles,
},
]
it(`Hides folders that begin with a ".", even if they contain relevant files`, () => {
expect(deepFileFilter(withHiddenDir, isRelevantFileOrDir)).toEqual(
filteredBaseFiles
)
})
it(`Properly counts the number of relevant files and directories in a project`, () => {
expect(getPartsCount(nestedFiles)).toEqual({
kclFileCount: 2,
kclDirCount: 2,
})
})
})

View File

@ -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)}`)
}

View File

@ -1,9 +1,26 @@
import { AppTheme } from 'wasm-lib/kcl/bindings/AppTheme'
export enum Themes {
Light = 'light',
Dark = 'dark',
System = 'system',
}
export function appThemeToTheme(
theme: AppTheme | undefined
): Themes | undefined {
switch (theme) {
case 'light':
return Themes.Light
case 'dark':
return Themes.Dark
case 'system':
return Themes.System
default:
return undefined
}
}
// Get the theme from the system settings manually
export function getSystemTheme(): Exclude<Themes, 'system'> {
return typeof globalThis.window !== 'undefined' &&

View File

@ -1,35 +1,22 @@
import { type FileInfo } from '@tauri-apps/plugin-fs'
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
import { Project } from 'wasm-lib/kcl/bindings/Project'
export type { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
export type IndexLoaderData = {
code: string | null
project?: ProjectWithEntryPointMetadata
project?: Project
file?: FileEntry
}
export type FileLoaderData = {
code: string | null
project?: FileEntry | ProjectWithEntryPointMetadata
project?: FileEntry | Project
file?: FileEntry
}
export type ProjectWithEntryPointMetadata = FileEntry & {
entrypointMetadata: FileInfo
}
export type HomeLoaderData = {
projects: ProjectWithEntryPointMetadata[]
}
// From https://github.com/tauri-apps/tauri/blob/1.x/tooling/api/src/fs.ts#L159
// Removed from tauri v2
export interface FileEntry {
path: string
/**
* Name of the directory/file
* can be null if the path terminates with `..`
*/
name?: string
/** Children of this entry if it's a directory; null otherwise */
children?: FileEntry[]
projects: Project[]
}
// From the very helpful @jcalz on StackOverflow: https://stackoverflow.com/a/58436959/22753272

View File

@ -2,8 +2,8 @@ import { createMachine, assign } from 'xstate'
import { Models } from '@kittycad/lib'
import withBaseURL from '../lib/withBaseURL'
import { isTauri } from 'lib/isTauri'
import { invoke } from '@tauri-apps/api/core'
import { VITE_KC_API_BASE_URL } from 'env'
import { getUser as getUserTauri } from 'lib/tauri'
const SKIP_AUTH =
import.meta.env.VITE_KC_SKIP_AUTH === 'true' && import.meta.env.DEV
@ -129,10 +129,7 @@ async function getUser(context: UserContext) {
})
.then((res) => res.json())
.catch((err) => console.error('error from Browser getUser', err))
: invoke<Models['User_type'] | Record<'error_code', unknown>>('get_user', {
token: context.token,
hostname: VITE_KC_API_BASE_URL,
}).catch((err) => console.error('error from Tauri getUser', err))
: getUserTauri(context.token, VITE_KC_API_BASE_URL)
const user = await userPromise

View File

@ -1,5 +1,6 @@
import { assign, createMachine } from 'xstate'
import type { FileEntry, ProjectWithEntryPointMetadata } from 'lib/types'
import type { FileEntry } from 'lib/types'
import { Project } from 'wasm-lib/kcl/bindings/Project'
export const fileMachine = createMachine(
{
@ -9,7 +10,7 @@ export const fileMachine = createMachine(
initial: 'Reading files',
context: {
project: {} as ProjectWithEntryPointMetadata,
project: {} as Project,
selectedDirectory: {} as FileEntry,
},
@ -154,7 +155,7 @@ export const fileMachine = createMachine(
| { type: 'navigate'; data: { name: string } }
| {
type: 'done.invoke.read-files'
data: ProjectWithEntryPointMetadata
data: Project
}
| { type: 'assign'; data: { [key: string]: any } }
| { type: 'Refresh' },

View File

@ -1,6 +1,6 @@
import { assign, createMachine } from 'xstate'
import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
import { Project } from 'wasm-lib/kcl/bindings/Project'
export const homeMachine = createMachine(
{
@ -10,7 +10,7 @@ export const homeMachine = createMachine(
initial: 'Reading projects',
context: {
projects: [] as ProjectWithEntryPointMetadata[],
projects: [] as Project[],
defaultProjectName: '',
defaultDirectory: '',
},
@ -145,7 +145,7 @@ export const homeMachine = createMachine(
| { type: 'navigate'; data: { name: string } }
| {
type: 'done.invoke.read-projects'
data: ProjectWithEntryPointMetadata[]
data: Project[]
}
| { type: 'assign'; data: { [key: string]: any } },
},
@ -157,7 +157,7 @@ export const homeMachine = createMachine(
{
actions: {
setProjects: assign((_, event) => {
return { projects: event.data as ProjectWithEntryPointMetadata[] }
return { projects: event.data as Project[] }
}),
},
}

View File

@ -1,11 +1,9 @@
import { FormEvent, useEffect } from 'react'
import { remove, rename } from '@tauri-apps/plugin-fs'
import {
createNewProject,
getNextProjectIndex,
interpolateProjectNameWithIndex,
doesProjectNameNeedInterpolated,
getProjectsInDir,
} from '../lib/tauriFS'
import { ActionButton } from '../components/ActionButton'
import { faArrowDown, faPlus } from '@fortawesome/free-solid-svg-icons'
@ -14,10 +12,7 @@ import { AppHeader } from '../components/AppHeader'
import ProjectCard from '../components/ProjectCard'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { Link } from 'react-router-dom'
import {
type ProjectWithEntryPointMetadata,
type HomeLoaderData,
} from 'lib/types'
import { type HomeLoaderData } from 'lib/types'
import Loading from '../components/Loading'
import { useMachine } from '@xstate/react'
import { homeMachine } from '../machines/homeMachine'
@ -39,6 +34,8 @@ import { kclManager } from 'lib/singletons'
import { useLspContext } from 'components/LspProvider'
import { useRefreshSettings } from 'hooks/useRefreshSettings'
import { LowerRightControls } from 'components/LowerRightControls'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { createNewProjectDirectory, listProjects } from 'lib/tauri'
// This route only opens in the Tauri desktop context for now,
// as defined in Router.tsx, so we can use the Tauri APIs and types.
@ -94,7 +91,7 @@ const Home = () => {
},
services: {
readProjects: async (context: ContextFrom<typeof homeMachine>) =>
getProjectsInDir(context.defaultDirectory),
listProjects(),
createProject: async (
context: ContextFrom<typeof homeMachine>,
event: EventFrom<typeof homeMachine, 'Create project'>
@ -110,7 +107,7 @@ const Home = () => {
name = interpolateProjectNameWithIndex(name, nextIndex)
}
await createNewProject(await join(context.defaultDirectory, name))
await createNewProjectDirectory(name)
return `Successfully created "${name}"`
},
@ -181,7 +178,7 @@ const Home = () => {
async function handleRenameProject(
e: FormEvent<HTMLFormElement>,
project: ProjectWithEntryPointMetadata
project: Project
) {
const { newProjectName } = Object.fromEntries(
new FormData(e.target as HTMLFormElement)
@ -192,7 +189,7 @@ const Home = () => {
})
}
async function handleDeleteProject(project: ProjectWithEntryPointMetadata) {
async function handleDeleteProject(project: Project) {
send('Delete project', { data: { name: project.name || '' } })
}
@ -284,7 +281,7 @@ const Home = () => {
icon={{ icon: faPlus, iconClassName: 'p-1 w-4' }}
data-testid="home-new-file"
>
New file
New project
</ActionButton>
</>
)}

View File

@ -4,9 +4,7 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Themes, getSystemTheme } from 'lib/theme'
import { bracket } from 'lib/exampleKcl'
import {
createNewProject,
getNextProjectIndex,
getProjectsInDir,
interpolateProjectNameWithIndex,
} from 'lib/tauriFS'
import { isTauri } from 'lib/isTauri'
@ -20,33 +18,21 @@ import {
ONBOARDING_PROJECT_NAME,
PROJECT_ENTRYPOINT,
} from 'lib/constants'
import { createNewProjectDirectory, listProjects } from 'lib/tauri'
function OnboardingWithNewFile() {
const navigate = useNavigate()
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.INDEX)
const {
settings: {
context: {
app: { projectDirectory },
},
},
} = useSettingsAuthContext()
async function createAndOpenNewProject() {
const projects = await getProjectsInDir(projectDirectory.current)
const nextIndex = await getNextProjectIndex(
ONBOARDING_PROJECT_NAME,
projects
)
const projects = await listProjects()
const nextIndex = getNextProjectIndex(ONBOARDING_PROJECT_NAME, projects)
const name = interpolateProjectNameWithIndex(
ONBOARDING_PROJECT_NAME,
nextIndex
)
const newFile = await createNewProject(
await join(projectDirectory.current, name),
bracket
)
const newFile = await createNewProjectDirectory(name, bracket)
navigate(
`${paths.FILE}/${encodeURIComponent(
await join(newFile.path, PROJECT_ENTRYPOINT)

View File

@ -10,15 +10,10 @@ import { useHotkeys } from 'react-hotkeys-hook'
import { paths } from 'lib/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useDotDotSlash } from 'hooks/useDotDotSlash'
import {
createAndOpenNewProject,
getInitialDefaultDir,
getSettingsFolderPaths,
} from 'lib/tauriFS'
import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/tauriFS'
import { sep } from '@tauri-apps/api/path'
import { isTauri } from 'lib/isTauri'
import toast from 'react-hot-toast'
import { invoke } from '@tauri-apps/api/core'
import React, { Fragment, useMemo, useRef, useState } from 'react'
import { Setting } from 'lib/settings/initialSettings'
import decamelize from 'decamelize'
@ -31,6 +26,7 @@ import {
shouldHideSetting,
shouldShowSettingInput,
} from 'lib/settings/settingsUtils'
import { getInitialDefaultDir, showInFolder } from 'lib/tauri'
export const APP_VERSION = import.meta.env.PACKAGE_VERSION || 'unknown'
@ -70,7 +66,7 @@ export const Settings = () => {
if (isFileSettings) {
navigate(dotDotSlash(1) + paths.ONBOARDING.INDEX)
} else {
createAndOpenNewProject(context.app.projectDirectory.current, navigate)
createAndOpenNewProject(navigate)
}
}
@ -302,9 +298,7 @@ export const Settings = () => {
? decodeURIComponent(projectPath)
: undefined
)
void invoke('show_in_folder', {
path: paths[settingsLevel],
})
showInFolder(paths[settingsLevel])
}}
icon={{
icon: 'folder',

View File

@ -1,11 +1,11 @@
import { ActionButton } from '../components/ActionButton'
import { isTauri } from '../lib/isTauri'
import { invoke } from '@tauri-apps/api/core'
import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env'
import { Themes, getSystemTheme } from '../lib/theme'
import { paths } from 'lib/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { APP_NAME } from 'lib/constants'
import { login } from 'lib/tauri'
const SignIn = () => {
const {
@ -28,9 +28,7 @@ const SignIn = () => {
const signInTauri = async () => {
// We want to invoke our command to login via device auth.
try {
const token: string = await invoke('login', {
host: VITE_KC_API_BASE_URL,
})
const token: string = await login(VITE_KC_API_BASE_URL)
send({ type: 'Log in', token })
} catch (error) {
console.error('Error with login button', error)

View File

@ -582,7 +582,7 @@ dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
"strsim 0.11.0",
"unicase",
"unicode-width",
]
@ -849,6 +849,41 @@ dependencies = [
"syn 2.0.60",
]
[[package]]
name = "darling"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.60",
]
[[package]]
name = "darling_macro"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core",
"quote",
"syn 2.0.60",
]
[[package]]
name = "dashmap"
version = "5.5.3"
@ -927,7 +962,7 @@ dependencies = [
[[package]]
name = "derive-docs"
version = "0.1.16"
version = "0.1.17"
dependencies = [
"Inflector",
"anyhow",
@ -1664,6 +1699,12 @@ dependencies = [
"cc",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.5.0"
@ -1854,7 +1895,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.1.50"
version = "0.1.51"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1895,11 +1936,13 @@ dependencies = [
"thiserror",
"tokio",
"tokio-tungstenite",
"toml",
"tower-lsp",
"ts-rs",
"twenty-twenty",
"url",
"uuid",
"validator",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
@ -3721,6 +3764,12 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9557cb6521e8d009c51a8666f09356f4b817ba9ba0981a305bd86aee47bd35c"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.0"
@ -4337,6 +4386,7 @@ version = "7.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2cae1fc5d05d47aa24b64f9a4f7cba24cdc9187a2084dd97ac57bef5eccae6"
dependencies = [
"chrono",
"thiserror",
"ts-rs-macros",
"url",
@ -4521,6 +4571,36 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "validator"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db79c75af171630a3148bd3e6d7c4f42b6a9a014c2945bc5ed0020cbb8d9478e"
dependencies = [
"idna",
"once_cell",
"regex",
"serde",
"serde_derive",
"serde_json",
"url",
"validator_derive",
]
[[package]]
name = "validator_derive"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55591299b7007f551ed1eb79a684af7672c19c3193fb9e0a31936987bb2438ec"
dependencies = [
"darling",
"once_cell",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.60",
]
[[package]]
name = "valuable"
version = "0.1.0"

View File

@ -1,7 +1,7 @@
[package]
name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.16"
version = "0.1.17"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -783,8 +783,7 @@ fn generate_code_block_test(
let tokens = crate::token::lexer(#code_block).unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone()).await.unwrap();
let ctx = crate::executor::ExecutorContext::new(ws, Default::default()).await.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -28,8 +28,7 @@ mod test_examples_show {
let tokens = crate::token::lexer("This is another code block.\nyes sirrr.\nshow").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(ws, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();
@ -105,8 +104,7 @@ mod test_examples_show {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(ws, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -28,8 +28,7 @@ mod test_examples_show {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(ws, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -29,8 +29,7 @@ mod test_examples_my_func {
crate::token::lexer("This is another code block.\nyes sirrr.\nmyFunc").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(ws, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();
@ -106,8 +105,7 @@ mod test_examples_my_func {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nmyFunc").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(ws, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -30,8 +30,7 @@ mod test_examples_import {
crate::token::lexer("This is another code block.\nyes sirrr.\nimport").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(ws, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();
@ -108,8 +107,7 @@ mod test_examples_import {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nimport").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(ws, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -29,8 +29,7 @@ mod test_examples_line_to {
crate::token::lexer("This is another code block.\nyes sirrr.\nlineTo").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(ws, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();
@ -106,8 +105,7 @@ mod test_examples_line_to {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nlineTo").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(ws, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -28,8 +28,7 @@ mod test_examples_min {
let tokens = crate::token::lexer("This is another code block.\nyes sirrr.\nmin").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(ws, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();
@ -105,8 +104,7 @@ mod test_examples_min {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nmin").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(ws, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -28,8 +28,7 @@ mod test_examples_show {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(ws, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -28,8 +28,7 @@ mod test_examples_import {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nimport").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(ws, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -28,8 +28,7 @@ mod test_examples_import {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nimport").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(ws, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -28,8 +28,7 @@ mod test_examples_import {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nimport").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(ws, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -28,8 +28,7 @@ mod test_examples_show {
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(ws, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.1.50"
version = "0.1.51"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -19,7 +19,7 @@ chrono = "0.4.38"
clap = { version = "4.5.4", features = ["cargo", "derive", "env", "unicode"], optional = true }
dashmap = "5.5.3"
databake = { version = "0.1.7", features = ["derive"] }
derive-docs = { version = "0.1.16", path = "../derive-docs" }
derive-docs = { version = "0.1.17", path = "../derive-docs" }
form_urlencoded = "1.2.1"
futures = { version = "0.3.30" }
git_rev = "0.1.0"
@ -37,9 +37,11 @@ serde = { version = "1.0.198", features = ["derive"] }
serde_json = "1.0.116"
sha2 = "0.10.8"
thiserror = "1.0.59"
ts-rs = { version = "7.1.1", features = ["uuid-impl", "url-impl"] }
toml = "0.8.12"
ts-rs = { version = "7.1.1", features = ["uuid-impl", "url-impl", "chrono-impl"] }
url = { version = "2.5.0", features = ["serde"] }
uuid = { version = "1.8.0", features = ["v4", "js", "serde"] }
validator = { version = "0.18.1", features = ["derive"] }
winnow = "0.5.40"
zip = { version = "0.6.6", default-features = false }

View File

@ -1000,21 +1000,39 @@ pub struct ExecutorContext {
pub engine: Arc<Box<dyn EngineManager>>,
pub fs: Arc<FileManager>,
pub stdlib: Arc<StdLib>,
pub units: kittycad::types::UnitLength,
pub settings: ExecutorSettings,
/// Mock mode is only for the modeling app when they just want to mock engine calls and not
/// actually make them.
pub is_mock: bool,
}
/// The executor settings.
#[derive(Debug, Clone)]
pub struct ExecutorSettings {
/// The unit to use in modeling dimensions.
pub units: crate::settings::types::UnitLength,
/// Highlight edges of 3D objects?
pub highlight_edges: bool,
}
impl Default for ExecutorSettings {
fn default() -> Self {
Self {
units: Default::default(),
highlight_edges: true,
}
}
}
impl ExecutorContext {
/// Create a new default executor context.
#[cfg(not(target_arch = "wasm32"))]
pub async fn new(ws: reqwest::Upgraded, units: kittycad::types::UnitLength) -> Result<Self> {
pub async fn new(ws: reqwest::Upgraded, settings: ExecutorSettings) -> Result<Self> {
Ok(Self {
engine: Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?)),
fs: Arc::new(FileManager::new()),
stdlib: Arc::new(StdLib::new()),
units,
settings,
is_mock: false,
})
}
@ -1033,7 +1051,7 @@ impl ExecutorContext {
uuid::Uuid::new_v4(),
SourceRange::default(),
kittycad::types::ModelingCmd::SetSceneUnits {
unit: self.units.clone(),
unit: self.settings.units.clone().into(),
},
)
.await?;
@ -1266,8 +1284,8 @@ impl ExecutorContext {
}
/// Update the units for the executor.
pub fn update_units(&mut self, units: kittycad::types::UnitLength) {
self.units = units;
pub fn update_units(&mut self, units: crate::settings::types::UnitLength) {
self.settings.units = units;
}
}
@ -1343,7 +1361,7 @@ mod tests {
engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await?)),
fs: Arc::new(crate::fs::FileManager::new()),
stdlib: Arc::new(crate::std::StdLib::new()),
units: kittycad::types::UnitLength::Mm,
settings: Default::default(),
is_mock: false,
};
let memory = ctx.run(program, None).await?;

View File

@ -13,6 +13,7 @@ pub mod executor;
pub mod fs;
pub mod lsp;
pub mod parser;
pub mod settings;
pub mod std;
pub mod thread;
pub mod token;

View File

@ -3,6 +3,8 @@
use serde::{Deserialize, Serialize};
use tower_lsp::lsp_types::notification::Notification;
use crate::settings::types::UnitLength;
/// A notification that the AST has changed.
#[derive(Debug)]
pub enum AstUpdated {}
@ -30,56 +32,6 @@ pub struct TextDocumentIdentifier {
pub uri: url::Url,
}
/// The valid types of length units.
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize, ts_rs::TS)]
#[ts(export)]
pub enum UnitLength {
/// Centimeters <https://en.wikipedia.org/wiki/Centimeter>
#[serde(rename = "cm")]
Cm,
/// Feet <https://en.wikipedia.org/wiki/Foot_(unit)>
#[serde(rename = "ft")]
Ft,
/// Inches <https://en.wikipedia.org/wiki/Inch>
#[serde(rename = "in")]
In,
/// Meters <https://en.wikipedia.org/wiki/Meter>
#[serde(rename = "m")]
M,
/// Millimeters <https://en.wikipedia.org/wiki/Millimeter>
#[serde(rename = "mm")]
Mm,
/// Yards <https://en.wikipedia.org/wiki/Yard>
#[serde(rename = "yd")]
Yd,
}
impl From<kittycad::types::UnitLength> for UnitLength {
fn from(unit: kittycad::types::UnitLength) -> Self {
match unit {
kittycad::types::UnitLength::Cm => UnitLength::Cm,
kittycad::types::UnitLength::Ft => UnitLength::Ft,
kittycad::types::UnitLength::In => UnitLength::In,
kittycad::types::UnitLength::M => UnitLength::M,
kittycad::types::UnitLength::Mm => UnitLength::Mm,
kittycad::types::UnitLength::Yd => UnitLength::Yd,
}
}
}
impl From<UnitLength> for kittycad::types::UnitLength {
fn from(unit: UnitLength) -> Self {
match unit {
UnitLength::Cm => kittycad::types::UnitLength::Cm,
UnitLength::Ft => kittycad::types::UnitLength::Ft,
UnitLength::In => kittycad::types::UnitLength::In,
UnitLength::M => kittycad::types::UnitLength::M,
UnitLength::Mm => kittycad::types::UnitLength::Mm,
UnitLength::Yd => kittycad::types::UnitLength::Yd,
}
}
}
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]

View File

@ -575,8 +575,7 @@ impl Backend {
false
};
let units: kittycad::types::UnitLength = params.units.into();
if executor_ctx.units == units
if executor_ctx.settings.units == params.units
&& !self.has_diagnostics(params.text_document.uri.as_ref()).await
&& has_memory
{
@ -585,7 +584,7 @@ impl Backend {
}
// Set the engine units.
executor_ctx.update_units(units);
executor_ctx.update_units(params.units);
// Update the locked executor context.
self.set_executor_ctx(executor_ctx.clone()).await;

View File

@ -53,7 +53,7 @@ async fn kcl_lsp_server(execute: bool) -> Result<crate::lsp::kcl::Backend> {
.modeling()
.commands_ws(None, None, None, None, None, None, Some(false))
.await?;
Some(crate::executor::ExecutorContext::new(ws, kittycad::types::UnitLength::Mm).await?)
Some(crate::executor::ExecutorContext::new(ws, Default::default()).await?)
} else {
None
};
@ -1654,8 +1654,8 @@ const part001 = cube([0,0], 20)
// Make sure the memory is the same.
assert_eq!(memory, server.memory_map.get("file:///test.kcl").await.unwrap().clone());
let units = server.executor_ctx.read().await.clone().unwrap().units;
assert_eq!(units, kittycad::types::UnitLength::Mm);
let units = server.executor_ctx.read().await.clone().unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
// Update the units.
server
@ -1663,15 +1663,15 @@ const part001 = cube([0,0], 20)
text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
units: crate::lsp::kcl::custom_notifications::UnitLength::M,
units: crate::settings::types::UnitLength::M,
text: same_text.clone(),
})
.await
.unwrap();
server.wait_on_handle().await;
let units = server.executor_ctx().await.unwrap().units;
assert_eq!(units, kittycad::types::UnitLength::M);
let units = server.executor_ctx().await.unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::M);
// Make sure it forced a memory update.
assert!(memory != server.memory_map.get("file:///test.kcl").await.unwrap().clone());
@ -2210,8 +2210,8 @@ async fn serial_test_kcl_lsp_code_and_ast_units_unchanged_but_has_diagnostics_re
let memory = server.memory_map.get("file:///test.kcl").await.unwrap().clone();
assert_eq!(memory, ProgramMemory::default());
let units = server.executor_ctx().await.unwrap().units;
assert_eq!(units, kittycad::types::UnitLength::Mm);
let units = server.executor_ctx().await.unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
// Update the units to the _same_ units.
server
@ -2219,15 +2219,15 @@ async fn serial_test_kcl_lsp_code_and_ast_units_unchanged_but_has_diagnostics_re
text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
units: crate::lsp::kcl::custom_notifications::UnitLength::Mm,
units: crate::settings::types::UnitLength::Mm,
text: code.to_string(),
})
.await
.unwrap();
server.wait_on_handle().await;
let units = server.executor_ctx().await.unwrap().units;
assert_eq!(units, kittycad::types::UnitLength::Mm);
let units = server.executor_ctx().await.unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").await.unwrap().clone();
@ -2295,8 +2295,8 @@ async fn serial_test_kcl_lsp_code_and_ast_units_unchanged_but_has_memory_reexecu
let memory = server.memory_map.get("file:///test.kcl").await.unwrap().clone();
assert_eq!(memory, ProgramMemory::default());
let units = server.executor_ctx().await.unwrap().units;
assert_eq!(units, kittycad::types::UnitLength::Mm);
let units = server.executor_ctx().await.unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
// Update the units to the _same_ units.
server
@ -2304,15 +2304,15 @@ async fn serial_test_kcl_lsp_code_and_ast_units_unchanged_but_has_memory_reexecu
text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
units: crate::lsp::kcl::custom_notifications::UnitLength::Mm,
units: crate::settings::types::UnitLength::Mm,
text: code.to_string(),
})
.await
.unwrap();
server.wait_on_handle().await;
let units = server.executor_ctx().await.unwrap().units;
assert_eq!(units, kittycad::types::UnitLength::Mm);
let units = server.executor_ctx().await.unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").await.unwrap().clone();
@ -2381,21 +2381,21 @@ async fn serial_test_kcl_lsp_cant_execute_set() {
assert_eq!(memory, ProgramMemory::default());
// Update the units to the _same_ units.
let units = server.executor_ctx().await.unwrap().units;
assert_eq!(units, kittycad::types::UnitLength::Mm);
let units = server.executor_ctx().await.unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
server
.update_units(crate::lsp::kcl::custom_notifications::UpdateUnitsParams {
text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
units: crate::lsp::kcl::custom_notifications::UnitLength::Mm,
units: crate::settings::types::UnitLength::Mm,
text: code.to_string(),
})
.await
.unwrap();
server.wait_on_handle().await;
let units = server.executor_ctx().await.unwrap().units;
assert_eq!(units, kittycad::types::UnitLength::Mm);
let units = server.executor_ctx().await.unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").await.unwrap().clone();
@ -2431,21 +2431,21 @@ async fn serial_test_kcl_lsp_cant_execute_set() {
assert_eq!(server.can_execute().await, false);
// Update the units to the _same_ units.
let units = server.executor_ctx().await.unwrap().units;
assert_eq!(units, kittycad::types::UnitLength::Mm);
let units = server.executor_ctx().await.unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
server
.update_units(crate::lsp::kcl::custom_notifications::UpdateUnitsParams {
text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
units: crate::lsp::kcl::custom_notifications::UnitLength::Mm,
units: crate::settings::types::UnitLength::Mm,
text: code.to_string(),
})
.await
.unwrap();
server.wait_on_handle().await;
let units = server.executor_ctx().await.unwrap().units;
assert_eq!(units, kittycad::types::UnitLength::Mm);
let units = server.executor_ctx().await.unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").await.unwrap().clone();
@ -2472,21 +2472,21 @@ async fn serial_test_kcl_lsp_cant_execute_set() {
assert_eq!(server.can_execute().await, true);
// Update the units to the _same_ units.
let units = server.executor_ctx.read().await.clone().unwrap().units;
assert_eq!(units, kittycad::types::UnitLength::Mm);
let units = server.executor_ctx.read().await.clone().unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
server
.update_units(crate::lsp::kcl::custom_notifications::UpdateUnitsParams {
text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
units: crate::lsp::kcl::custom_notifications::UnitLength::Mm,
units: crate::settings::types::UnitLength::Mm,
text: code.to_string(),
})
.await
.unwrap();
server.wait_on_handle().await;
let units = server.executor_ctx.read().await.clone().unwrap().units;
assert_eq!(units, kittycad::types::UnitLength::Mm);
let units = server.executor_ctx.read().await.clone().unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").await.unwrap().clone();

View File

@ -0,0 +1,5 @@
//! This module contains settings for kcl projects as well as the modeling app.
pub mod types;
#[cfg(not(target_arch = "wasm32"))]
pub mod utils;

View File

@ -0,0 +1,198 @@
//! Types for interacting with files in projects.
use anyhow::Result;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// Information about project.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct Project {
#[serde(flatten)]
pub file: FileEntry,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<FileMetadata>,
#[serde(default)]
#[ts(type = "number")]
pub kcl_file_count: u64,
#[serde(default)]
#[ts(type = "number")]
pub directory_count: u64,
}
impl Project {
/// Populate the number of KCL files in the project.
pub fn populate_kcl_file_count(&mut self) -> Result<()> {
let mut count = 0;
if let Some(children) = &self.file.children {
for entry in children.iter() {
if entry.name.ends_with(".kcl") {
count += 1;
} else {
count += entry.kcl_file_count();
}
}
}
self.kcl_file_count = count;
Ok(())
}
/// Populate the number of directories in the project.
pub fn populate_directory_count(&mut self) -> Result<()> {
let mut count = 0;
if let Some(children) = &self.file.children {
for entry in children.iter() {
count += entry.directory_count();
}
}
self.directory_count = count;
Ok(())
}
}
/// Information about a file or directory.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct FileEntry {
pub path: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<FileEntry>>,
}
impl FileEntry {
/// Recursively get the number of kcl files in the file entry.
pub fn kcl_file_count(&self) -> u64 {
let mut count = 0;
if let Some(children) = &self.children {
for entry in children.iter() {
if entry.name.ends_with(".kcl") {
count += 1;
} else {
count += entry.kcl_file_count();
}
}
}
count
}
/// Recursively get the number of directories in the file entry.
pub fn directory_count(&self) -> u64 {
let mut count = 0;
if let Some(children) = &self.children {
for entry in children.iter() {
if entry.children.is_some() {
count += 1;
}
}
}
count
}
}
/// Metadata about a file or directory.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct FileMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub accessed: Option<chrono::DateTime<chrono::Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created: Option<chrono::DateTime<chrono::Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub r#type: Option<FileType>,
#[serde(default)]
#[ts(type = "number")]
pub size: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub modified: Option<chrono::DateTime<chrono::Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permission: Option<FilePermission>,
}
/// The type of a file.
#[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, Display, FromStr, ts_rs::TS, PartialEq, Eq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
#[display(style = "snake_case")]
pub enum FileType {
/// A file.
File,
/// A directory.
Directory,
/// A symbolic link.
Symlink,
}
/// The permissions of a file.
#[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, Display, FromStr, ts_rs::TS, PartialEq, Eq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
#[display(style = "snake_case")]
pub enum FilePermission {
/// Read permission.
Read,
/// Write permission.
Write,
/// Execute permission.
Execute,
}
impl From<std::fs::FileType> for FileType {
fn from(file_type: std::fs::FileType) -> Self {
if file_type.is_file() {
FileType::File
} else if file_type.is_dir() {
FileType::Directory
} else if file_type.is_symlink() {
FileType::Symlink
} else {
unreachable!()
}
}
}
impl From<std::fs::Permissions> for FilePermission {
fn from(permissions: std::fs::Permissions) -> Self {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = permissions.mode();
if mode & 0o400 != 0 {
FilePermission::Read
} else if mode & 0o200 != 0 {
FilePermission::Write
} else if mode & 0o100 != 0 {
FilePermission::Execute
} else {
unreachable!()
}
}
#[cfg(not(unix))]
{
if permissions.readonly() {
FilePermission::Read
} else {
FilePermission::Write
}
}
}
}
impl From<std::fs::Metadata> for FileMetadata {
fn from(metadata: std::fs::Metadata) -> Self {
Self {
accessed: metadata.accessed().ok().map(|t| t.into()),
created: metadata.created().ok().map(|t| t.into()),
r#type: Some(metadata.file_type().into()),
size: metadata.len(),
modified: metadata.modified().ok().map(|t| t.into()),
permission: Some(metadata.permissions().into()),
}
}
}

View File

@ -0,0 +1,930 @@
//! Types for kcl project and modeling-app settings.
pub mod file;
pub mod project;
use anyhow::Result;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use validator::{Validate, ValidateRange};
const DEFAULT_THEME_COLOR: f64 = 264.5;
const DEFAULT_PROJECT_KCL_FILE: &str = "main.kcl";
const DEFAULT_PROJECT_NAME_TEMPLATE: &str = "project-$nnn";
/// High level configuration.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct Configuration {
/// The settings for the modeling app.
#[serde(default, skip_serializing_if = "is_default")]
#[validate(nested)]
pub settings: Settings,
}
impl Configuration {
// TODO: remove this when we remove backwards compatibility with the old settings file.
pub fn backwards_compatible_toml_parse(toml_str: &str) -> Result<Self> {
let mut settings = toml::from_str::<Self>(toml_str)?;
if let Some(project_directory) = &settings.settings.app.project_directory {
if settings.settings.project.directory.to_string_lossy().is_empty() {
settings.settings.project.directory = project_directory.clone();
settings.settings.app.project_directory = None;
}
}
if let Some(theme) = &settings.settings.app.theme {
if settings.settings.app.appearance.theme == AppTheme::default() {
settings.settings.app.appearance.theme = *theme;
settings.settings.app.theme = None;
}
}
if let Some(theme_color) = &settings.settings.app.theme_color {
if settings.settings.app.appearance.color == AppColor::default() {
settings.settings.app.appearance.color = theme_color.clone().into();
settings.settings.app.theme_color = None;
}
}
if let Some(enable_ssao) = settings.settings.app.enable_ssao {
if settings.settings.modeling.enable_ssao.into() {
settings.settings.modeling.enable_ssao = enable_ssao.into();
settings.settings.app.enable_ssao = None;
}
}
settings.validate()?;
Ok(settings)
}
#[cfg(not(target_arch = "wasm32"))]
/// Initialize the project directory.
pub async fn ensure_project_directory_exists(&self) -> Result<std::path::PathBuf> {
let project_dir = &self.settings.project.directory;
// Check if the directory exists.
if !project_dir.exists() {
// Create the directory.
tokio::fs::create_dir_all(project_dir).await?;
}
Ok(project_dir.clone())
}
#[cfg(not(target_arch = "wasm32"))]
/// Create a new project directory.
pub async fn create_new_project_directory(
&self,
project_name: &str,
initial_code: Option<&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."));
}
// Create the project directory.
let project_dir = main_dir.join(project_name);
// Create the directory.
if !project_dir.exists() {
tokio::fs::create_dir_all(&project_dir).await?;
}
// Write the initial project file.
let project_file = project_dir.join(DEFAULT_PROJECT_KCL_FILE);
tokio::fs::write(&project_file, initial_code.unwrap_or_default()).await?;
Ok(crate::settings::types::file::Project {
file: crate::settings::types::file::FileEntry {
path: project_dir.to_string_lossy().to_string(),
name: project_name.to_string(),
// We don't need to recursively get all files in the project directory.
// Because we just created it and it's empty.
children: None,
},
metadata: Some(tokio::fs::metadata(&project_dir).await?.into()),
kcl_file_count: 1,
directory_count: 0,
})
}
#[cfg(not(target_arch = "wasm32"))]
/// List all the projects for the configuration.
pub async fn list_projects(&self) -> Result<Vec<crate::settings::types::file::Project>> {
// Get all the top level directories in the project directory.
let main_dir = &self.ensure_project_directory_exists().await?;
let mut projects = vec![];
let mut entries = tokio::fs::read_dir(main_dir).await?;
while let Some(e) = entries.next_entry().await? {
if !e.file_type().await?.is_dir() {
// We don't care it's not a directory.
continue;
}
projects.push(self.get_project_info(&e.file_name().to_string_lossy()).await?);
}
Ok(projects)
}
#[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."));
}
// Check the directory.
let project_dir = main_dir.join(project_name);
if !project_dir.exists() {
return Err(anyhow::anyhow!("Project directory does not exist."));
}
let mut project = crate::settings::types::file::Project {
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,
};
// Populate the number of KCL files in the project.
project.populate_kcl_file_count()?;
//Populate the number of directories in the project.
project.populate_directory_count()?;
Ok(project)
}
}
/// High level settings.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct Settings {
/// The settings for the modeling app.
#[serde(default, skip_serializing_if = "is_default")]
#[validate(nested)]
pub app: AppSettings,
/// Settings that affect the behavior while modeling.
#[serde(default, skip_serializing_if = "is_default")]
#[validate(nested)]
pub modeling: ModelingSettings,
/// Settings that affect the behavior of the KCL text editor.
#[serde(default, alias = "textEditor", skip_serializing_if = "is_default")]
#[validate(nested)]
pub text_editor: TextEditorSettings,
/// Settings that affect the behavior of project management.
#[serde(default, alias = "projects", skip_serializing_if = "is_default")]
#[validate(nested)]
pub project: ProjectSettings,
/// Settings that affect the behavior of the command bar.
#[serde(default, alias = "commandBar", skip_serializing_if = "is_default")]
#[validate(nested)]
pub command_bar: CommandBarSettings,
}
/// Application wide settings.
// TODO: When we remove backwards compatibility with the old settings file, we can remove the
// aliases to camelCase (and projects plural) from everywhere.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct AppSettings {
/// The settings for the appearance of the app.
#[serde(default, skip_serializing_if = "is_default")]
#[validate(nested)]
pub appearance: AppearanceSettings,
/// The onboarding status of the app.
#[serde(default, alias = "onboardingStatus", skip_serializing_if = "is_default")]
pub onboarding_status: OnboardingStatus,
/// Backwards compatible project directory setting.
#[serde(default, alias = "projectDirectory", skip_serializing_if = "Option::is_none")]
pub project_directory: Option<std::path::PathBuf>,
/// Backwards compatible theme setting.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub theme: Option<AppTheme>,
/// The hue of the primary theme color for the app.
#[serde(default, skip_serializing_if = "Option::is_none", alias = "themeColor")]
pub theme_color: Option<FloatOrInt>,
/// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
#[serde(default, alias = "enableSSAO", skip_serializing_if = "Option::is_none")]
pub enable_ssao: Option<bool>,
/// Permanently dismiss the banner warning to download the desktop app.
/// This setting only applies to the web app. And is temporary until we have Linux support.
#[serde(default, alias = "dismissWebBanner", skip_serializing_if = "is_default")]
pub dismiss_web_banner: bool,
}
// TODO: When we remove backwards compatibility with the old settings file, we can remove this.
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(untagged)]
pub enum FloatOrInt {
String(String),
Float(f64),
Int(i64),
}
impl From<FloatOrInt> for f64 {
fn from(float_or_int: FloatOrInt) -> Self {
match float_or_int {
FloatOrInt::String(s) => s.parse().unwrap(),
FloatOrInt::Float(f) => f,
FloatOrInt::Int(i) => i as f64,
}
}
}
impl From<FloatOrInt> for AppColor {
fn from(float_or_int: FloatOrInt) -> Self {
match float_or_int {
FloatOrInt::String(s) => s.parse::<f64>().unwrap().into(),
FloatOrInt::Float(f) => f.into(),
FloatOrInt::Int(i) => (i as f64).into(),
}
}
}
/// The settings for the theme of the app.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct AppearanceSettings {
/// The overall theme of the app.
#[serde(default, skip_serializing_if = "is_default")]
pub theme: AppTheme,
/// The hue of the primary theme color for the app.
#[serde(default, skip_serializing_if = "is_default")]
#[validate(nested)]
pub color: AppColor,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(transparent)]
pub struct AppColor(pub f64);
impl Default for AppColor {
fn default() -> Self {
Self(DEFAULT_THEME_COLOR)
}
}
impl From<AppColor> for f64 {
fn from(color: AppColor) -> Self {
color.0
}
}
impl From<f64> for AppColor {
fn from(color: f64) -> Self {
Self(color)
}
}
impl Validate for AppColor {
fn validate(&self) -> Result<(), validator::ValidationErrors> {
if !self.0.validate_range(Some(0.0), None, None, Some(360.0)) {
let mut errors = validator::ValidationErrors::new();
let mut err = validator::ValidationError::new("color");
err.add_param(std::borrow::Cow::from("min"), &0.0);
err.add_param(std::borrow::Cow::from("exclusive_max"), &360.0);
errors.add("color", err);
return Err(errors);
}
Ok(())
}
}
/// The overall appearance of the app.
#[derive(
Debug, Default, Copy, Clone, Deserialize, Serialize, JsonSchema, Display, FromStr, ts_rs::TS, PartialEq, Eq,
)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
#[display(style = "snake_case")]
pub enum AppTheme {
/// A light theme.
Light,
/// A dark theme.
Dark,
/// Use the system theme.
/// This will use dark theme if the system theme is dark, and light theme if the system theme is light.
#[default]
System,
}
impl From<AppTheme> for kittycad::types::Color {
fn from(theme: AppTheme) -> Self {
match theme {
AppTheme::Light => kittycad::types::Color {
r: 249.0 / 255.0,
g: 249.0 / 255.0,
b: 249.0 / 255.0,
a: 1.0,
},
AppTheme::Dark => kittycad::types::Color {
r: 28.0 / 255.0,
g: 28.0 / 255.0,
b: 28.0 / 255.0,
a: 1.0,
},
AppTheme::System => {
// TODO: Check the system setting for the user.
todo!()
}
}
}
}
/// Settings that affect the behavior while modeling.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
#[serde(rename_all = "snake_case")]
#[ts(export)]
pub struct ModelingSettings {
/// The default unit to use in modeling dimensions.
#[serde(default, alias = "defaultUnit", skip_serializing_if = "is_default")]
pub base_unit: UnitLength,
/// The controls for how to navigate the 3D view.
#[serde(default, alias = "mouseControls", skip_serializing_if = "is_default")]
pub mouse_controls: MouseControlType,
/// Highlight edges of 3D objects?
#[serde(default, alias = "highlightEdges", skip_serializing_if = "is_default")]
pub highlight_edges: DefaultTrue,
/// Whether to show the debug panel, which lets you see various states
/// of the app to aid in development.
#[serde(default, alias = "showDebugPanel", skip_serializing_if = "is_default")]
pub show_debug_panel: bool,
/// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
#[serde(default, skip_serializing_if = "is_default")]
pub enable_ssao: DefaultTrue,
}
#[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq)]
#[ts(export)]
#[serde(transparent)]
pub struct DefaultTrue(pub bool);
impl Default for DefaultTrue {
fn default() -> Self {
Self(true)
}
}
impl From<DefaultTrue> for bool {
fn from(default_true: DefaultTrue) -> Self {
default_true.0
}
}
impl From<bool> for DefaultTrue {
fn from(b: bool) -> Self {
Self(b)
}
}
/// The valid types of length units.
#[derive(Debug, Default, Eq, PartialEq, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, Display, FromStr)]
#[ts(export)]
#[serde(rename_all = "lowercase")]
#[display(style = "lowercase")]
pub enum UnitLength {
/// Centimeters <https://en.wikipedia.org/wiki/Centimeter>
Cm,
/// Feet <https://en.wikipedia.org/wiki/Foot_(unit)>
Ft,
/// Inches <https://en.wikipedia.org/wiki/Inch>
In,
/// Meters <https://en.wikipedia.org/wiki/Meter>
M,
/// Millimeters <https://en.wikipedia.org/wiki/Millimeter>
#[default]
Mm,
/// Yards <https://en.wikipedia.org/wiki/Yard>
Yd,
}
impl From<kittycad::types::UnitLength> for UnitLength {
fn from(unit: kittycad::types::UnitLength) -> Self {
match unit {
kittycad::types::UnitLength::Cm => UnitLength::Cm,
kittycad::types::UnitLength::Ft => UnitLength::Ft,
kittycad::types::UnitLength::In => UnitLength::In,
kittycad::types::UnitLength::M => UnitLength::M,
kittycad::types::UnitLength::Mm => UnitLength::Mm,
kittycad::types::UnitLength::Yd => UnitLength::Yd,
}
}
}
impl From<UnitLength> for kittycad::types::UnitLength {
fn from(unit: UnitLength) -> Self {
match unit {
UnitLength::Cm => kittycad::types::UnitLength::Cm,
UnitLength::Ft => kittycad::types::UnitLength::Ft,
UnitLength::In => kittycad::types::UnitLength::In,
UnitLength::M => kittycad::types::UnitLength::M,
UnitLength::Mm => kittycad::types::UnitLength::Mm,
UnitLength::Yd => kittycad::types::UnitLength::Yd,
}
}
}
/// The types of controls for how to navigate the 3D view.
#[derive(Debug, Default, Eq, PartialEq, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, Display, FromStr)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
#[display(style = "snake_case")]
pub enum MouseControlType {
#[default]
#[display("kittycad")]
#[serde(rename = "kittycad", alias = "KittyCAD")]
KittyCad,
#[display("onshape")]
#[serde(rename = "onshape", alias = "OnShape")]
OnShape,
#[serde(alias = "Trackpad Friendly")]
TrackpadFriendly,
#[serde(alias = "Solidworks")]
Solidworks,
#[serde(alias = "NX")]
Nx,
#[serde(alias = "Creo")]
Creo,
#[display("autocad")]
#[serde(rename = "autocad", alias = "AutoCAD")]
AutoCad,
}
/// Settings that affect the behavior of the KCL text editor.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
#[serde(rename_all = "snake_case")]
#[ts(export)]
pub struct TextEditorSettings {
/// Whether to wrap text in the editor or overflow with scroll.
#[serde(default, alias = "textWrapping", skip_serializing_if = "is_default")]
pub text_wrapping: DefaultTrue,
/// Whether to make the cursor blink in the editor.
#[serde(default, alias = "blinkingCursor", skip_serializing_if = "is_default")]
pub blinking_cursor: DefaultTrue,
}
/// Settings that affect the behavior of project management.
#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
#[serde(rename_all = "snake_case")]
#[ts(export)]
pub struct ProjectSettings {
/// The directory to save and load projects from.
#[serde(default, skip_serializing_if = "is_default")]
pub directory: std::path::PathBuf,
/// The default project name to use when creating a new project.
#[serde(default, alias = "defaultProjectName", skip_serializing_if = "is_default")]
pub default_project_name: ProjectNameTemplate,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq)]
#[ts(export)]
#[serde(transparent)]
pub struct ProjectNameTemplate(pub String);
impl Default for ProjectNameTemplate {
fn default() -> Self {
Self(DEFAULT_PROJECT_NAME_TEMPLATE.to_string())
}
}
impl From<ProjectNameTemplate> for String {
fn from(project_name: ProjectNameTemplate) -> Self {
project_name.0
}
}
impl From<String> for ProjectNameTemplate {
fn from(s: String) -> Self {
Self(s)
}
}
/// Settings that affect the behavior of the command bar.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
#[serde(rename_all = "snake_case")]
#[ts(export)]
pub struct CommandBarSettings {
/// Whether to include settings in the command bar.
#[serde(default, alias = "includeSettings", skip_serializing_if = "is_default")]
pub include_settings: DefaultTrue,
}
/// The types of onboarding status.
#[derive(Debug, Default, Eq, PartialEq, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, Display, FromStr)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
#[display(style = "snake_case")]
pub enum OnboardingStatus {
/// The user has completed onboarding.
Completed,
/// The user has not completed onboarding.
#[default]
Incomplete,
/// The user has dismissed onboarding.
Dismissed,
// Routes
#[serde(rename = "/")]
#[display("/")]
Index,
#[serde(rename = "/camera")]
#[display("/camera")]
Camera,
#[serde(rename = "/streaming")]
#[display("/streaming")]
Streaming,
#[serde(rename = "/editor")]
#[display("/editor")]
Editor,
#[serde(rename = "/parametric-modeling")]
#[display("/parametric-modeling")]
ParametricModeling,
#[serde(rename = "/interactive-numbers")]
#[display("/interactive-numbers")]
InteractiveNumbers,
#[serde(rename = "/command-k")]
#[display("/command-k")]
CommandK,
#[serde(rename = "/user-menu")]
#[display("/user-menu")]
UserMenu,
#[serde(rename = "/project-menu")]
#[display("/project-menu")]
ProjectMenu,
#[serde(rename = "/export")]
#[display("/export")]
Export,
#[serde(rename = "/move")]
#[display("/move")]
Move,
#[serde(rename = "/sketching")]
#[display("/sketching")]
Sketching,
#[serde(rename = "/future-work")]
#[display("/future-work")]
FutureWork,
}
fn is_default<T: Default + PartialEq>(t: &T) -> bool {
t == &T::default()
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use validator::Validate;
use super::{
AppColor, AppSettings, AppTheme, AppearanceSettings, CommandBarSettings, Configuration, ModelingSettings,
OnboardingStatus, ProjectSettings, Settings, TextEditorSettings, UnitLength,
};
#[test]
// Test that we can deserialize a project file from the old format.
// TODO: We can remove this functionality after a few versions.
fn test_backwards_compatible_project_settings_file_pw() {
let old_project_file = r#"[settings.app]
theme = "dark"
onboardingStatus = "dismissed"
projectDirectory = ""
enableSSAO = false
[settings.modeling]
defaultUnit = "in"
mouseControls = "KittyCAD"
showDebugPanel = true
[settings.projects]
defaultProjectName = "project-$nnn"
[settings.textEditor]
textWrapping = true
#"#;
//let parsed = toml::from_str::<Configuration(old_project_file).unwrap();
let parsed = Configuration::backwards_compatible_toml_parse(old_project_file).unwrap();
assert_eq!(
parsed,
Configuration {
settings: Settings {
app: AppSettings {
appearance: AppearanceSettings {
theme: AppTheme::Dark,
color: Default::default(),
},
onboarding_status: OnboardingStatus::Dismissed,
project_directory: None,
theme: None,
theme_color: None,
dismiss_web_banner: false,
enable_ssao: None,
},
modeling: ModelingSettings {
base_unit: UnitLength::In,
mouse_controls: Default::default(),
highlight_edges: Default::default(),
show_debug_panel: true,
enable_ssao: false.into(),
},
text_editor: TextEditorSettings {
text_wrapping: true.into(),
blinking_cursor: true.into(),
},
project: Default::default(),
command_bar: CommandBarSettings {
include_settings: true.into(),
},
}
}
);
}
#[test]
// Test that we can deserialize a project file from the old format.
// TODO: We can remove this functionality after a few versions.
fn test_backwards_compatible_project_settings_file() {
let old_project_file = r#"[settings.app]
theme = "dark"
themeColor = "138"
[settings.modeling]
defaultUnit = "yd"
showDebugPanel = true
[settings.textEditor]
textWrapping = false
blinkingCursor = false
[settings.commandBar]
includeSettings = false
#"#;
//let parsed = toml::from_str::<Configuration(old_project_file).unwrap();
let parsed = Configuration::backwards_compatible_toml_parse(old_project_file).unwrap();
assert_eq!(
parsed,
Configuration {
settings: Settings {
app: AppSettings {
appearance: AppearanceSettings {
theme: AppTheme::Dark,
color: 138.0.into(),
},
onboarding_status: Default::default(),
project_directory: None,
theme: None,
theme_color: None,
dismiss_web_banner: false,
enable_ssao: None,
},
modeling: ModelingSettings {
base_unit: UnitLength::Yd,
mouse_controls: Default::default(),
highlight_edges: Default::default(),
show_debug_panel: true,
enable_ssao: true.into(),
},
text_editor: TextEditorSettings {
text_wrapping: false.into(),
blinking_cursor: false.into(),
},
project: Default::default(),
command_bar: CommandBarSettings {
include_settings: false.into(),
},
}
}
);
}
#[test]
// Test that we can deserialize a app settings file from the old format.
// TODO: We can remove this functionality after a few versions.
fn test_backwards_compatible_app_settings_file() {
let old_app_settings_file = r#"[settings.app]
onboardingStatus = "dismissed"
projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects"
theme = "dark"
themeColor = "138"
[settings.modeling]
defaultUnit = "yd"
showDebugPanel = true
[settings.textEditor]
textWrapping = false
blinkingCursor = false
[settings.commandBar]
includeSettings = false
[settings.projects]
defaultProjectName = "projects-$nnn"
#"#;
//let parsed = toml::from_str::<Configuration>(old_app_settings_file).unwrap();
let parsed = Configuration::backwards_compatible_toml_parse(old_app_settings_file).unwrap();
assert_eq!(
parsed,
Configuration {
settings: Settings {
app: AppSettings {
appearance: AppearanceSettings {
theme: AppTheme::Dark,
color: 138.0.into(),
},
onboarding_status: OnboardingStatus::Dismissed,
project_directory: None,
theme: None,
theme_color: None,
dismiss_web_banner: false,
enable_ssao: None,
},
modeling: ModelingSettings {
base_unit: UnitLength::Yd,
mouse_controls: Default::default(),
highlight_edges: Default::default(),
show_debug_panel: true,
enable_ssao: true.into(),
},
text_editor: TextEditorSettings {
text_wrapping: false.into(),
blinking_cursor: false.into(),
},
project: ProjectSettings {
directory: "/Users/macinatormax/Documents/kittycad-modeling-projects".into(),
default_project_name: "projects-$nnn".to_string().into(),
},
command_bar: CommandBarSettings {
include_settings: false.into(),
},
}
}
);
// Write the file back out.
let serialized = toml::to_string(&parsed).unwrap();
assert_eq!(
serialized,
r#"[settings.app]
onboarding_status = "dismissed"
[settings.app.appearance]
theme = "dark"
color = 138.0
[settings.modeling]
base_unit = "yd"
show_debug_panel = true
[settings.text_editor]
text_wrapping = false
blinking_cursor = false
[settings.project]
directory = "/Users/macinatormax/Documents/kittycad-modeling-projects"
default_project_name = "projects-$nnn"
[settings.command_bar]
include_settings = false
"#
);
}
#[test]
fn test_settings_backwards_compat_partial() {
let partial_settings_file = r#"[settings.app]
onboardingStatus = "dismissed"
projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
//let parsed = toml::from_str::<Configuration>(partial_settings_file).unwrap();
let parsed = Configuration::backwards_compatible_toml_parse(partial_settings_file).unwrap();
assert_eq!(
parsed,
Configuration {
settings: Settings {
app: AppSettings {
appearance: AppearanceSettings {
theme: AppTheme::System,
color: Default::default(),
},
onboarding_status: OnboardingStatus::Dismissed,
project_directory: None,
theme: None,
theme_color: None,
dismiss_web_banner: false,
enable_ssao: None,
},
modeling: ModelingSettings {
base_unit: UnitLength::Mm,
mouse_controls: Default::default(),
highlight_edges: true.into(),
show_debug_panel: false,
enable_ssao: true.into(),
},
text_editor: TextEditorSettings {
text_wrapping: true.into(),
blinking_cursor: true.into(),
},
project: ProjectSettings {
directory: "/Users/macinatormax/Documents/kittycad-modeling-projects".into(),
default_project_name: "project-$nnn".to_string().into(),
},
command_bar: CommandBarSettings {
include_settings: true.into()
},
}
}
);
// Write the file back out.
let serialized = toml::to_string(&parsed).unwrap();
assert_eq!(
serialized,
r#"[settings.app]
onboarding_status = "dismissed"
[settings.project]
directory = "/Users/macinatormax/Documents/kittycad-modeling-projects"
"#
);
}
#[test]
fn test_settings_empty_file_parses() {
let empty_settings_file = r#""#;
let parsed = toml::from_str::<Configuration>(empty_settings_file).unwrap();
assert_eq!(parsed, Configuration::default());
// Write the file back out.
let serialized = toml::to_string(&parsed).unwrap();
assert_eq!(serialized, r#""#);
let parsed = Configuration::backwards_compatible_toml_parse(empty_settings_file).unwrap();
assert_eq!(parsed, Configuration::default());
}
#[test]
fn test_color_validation() {
let color = AppColor(360.0);
let result = color.validate();
if let Ok(r) = result {
panic!("Expected an error, but got success: {:?}", r);
}
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("color: Validation error: color"));
let appearance = AppearanceSettings {
theme: AppTheme::System,
color: AppColor(361.5),
};
let result = appearance.validate();
if let Ok(r) = result {
panic!("Expected an error, but got success: {:?}", r);
}
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("color: Validation error: color"));
}
#[test]
fn test_settings_color_validation_error() {
let settings_file = r#"[settings.app.appearance]
color = 1567.4"#;
let result = Configuration::backwards_compatible_toml_parse(settings_file);
if let Ok(r) = result {
panic!("Expected an error, but got success: {:?}", r);
}
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("color: Validation error: color"));
}
}

View File

@ -0,0 +1,187 @@
//! Types specific for modeling-app projects.
use anyhow::Result;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use validator::Validate;
use crate::settings::types::{
AppColor, AppSettings, AppTheme, CommandBarSettings, ModelingSettings, TextEditorSettings,
};
/// High level project configuration.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct ProjectConfiguration {
/// The settings for the project.
#[serde(default)]
#[validate(nested)]
pub settings: PerProjectSettings,
}
impl ProjectConfiguration {
// TODO: remove this when we remove backwards compatibility with the old settings file.
pub fn backwards_compatible_toml_parse(toml_str: &str) -> Result<Self> {
let mut settings = toml::from_str::<Self>(toml_str)?;
settings.settings.app.project_directory = None;
if let Some(theme) = &settings.settings.app.theme {
if settings.settings.app.appearance.theme == AppTheme::default() {
settings.settings.app.appearance.theme = *theme;
settings.settings.app.theme = None;
}
}
if let Some(theme_color) = &settings.settings.app.theme_color {
if settings.settings.app.appearance.color == AppColor::default() {
settings.settings.app.appearance.color = theme_color.clone().into();
settings.settings.app.theme_color = None;
}
}
if let Some(enable_ssao) = settings.settings.app.enable_ssao {
if settings.settings.modeling.enable_ssao.into() {
settings.settings.modeling.enable_ssao = enable_ssao.into();
settings.settings.app.enable_ssao = None;
}
}
settings.validate()?;
Ok(settings)
}
}
/// High level project settings.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct PerProjectSettings {
/// The settings for the modeling app.
#[serde(default)]
#[validate(nested)]
pub app: AppSettings,
/// Settings that affect the behavior while modeling.
#[serde(default)]
#[validate(nested)]
pub modeling: ModelingSettings,
/// Settings that affect the behavior of the KCL text editor.
#[serde(default, alias = "textEditor")]
#[validate(nested)]
pub text_editor: TextEditorSettings,
/// Settings that affect the behavior of the command bar.
#[serde(default, alias = "commandBar")]
#[validate(nested)]
pub command_bar: CommandBarSettings,
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::{
AppSettings, AppTheme, CommandBarSettings, ModelingSettings, PerProjectSettings, ProjectConfiguration,
TextEditorSettings,
};
use crate::settings::types::{AppearanceSettings, UnitLength};
#[test]
// Test that we can deserialize a project file from the old format.
// TODO: We can remove this functionality after a few versions.
fn test_backwards_compatible_project_settings_file() {
let old_project_file = r#"[settings.app]
theme = "dark"
themeColor = "138"
[settings.modeling]
defaultUnit = "yd"
showDebugPanel = true
[settings.textEditor]
textWrapping = false
blinkingCursor = false
[settings.commandBar]
includeSettings = false
#"#;
//let parsed = toml::from_str::<ProjectConfiguration(old_project_file).unwrap();
let parsed = ProjectConfiguration::backwards_compatible_toml_parse(old_project_file).unwrap();
assert_eq!(
parsed,
ProjectConfiguration {
settings: PerProjectSettings {
app: AppSettings {
appearance: AppearanceSettings {
theme: AppTheme::Dark,
color: 138.0.into(),
},
onboarding_status: Default::default(),
project_directory: None,
theme: None,
theme_color: None,
dismiss_web_banner: false,
enable_ssao: None,
},
modeling: ModelingSettings {
base_unit: UnitLength::Yd,
mouse_controls: Default::default(),
highlight_edges: Default::default(),
show_debug_panel: true,
enable_ssao: true.into(),
},
text_editor: TextEditorSettings {
text_wrapping: false.into(),
blinking_cursor: false.into(),
},
command_bar: CommandBarSettings {
include_settings: false.into(),
},
}
}
);
}
#[test]
fn test_project_settings_empty_file_parses() {
let empty_settings_file = r#""#;
let parsed = toml::from_str::<ProjectConfiguration>(empty_settings_file).unwrap();
assert_eq!(parsed, ProjectConfiguration::default());
// Write the file back out.
let serialized = toml::to_string(&parsed).unwrap();
assert_eq!(
serialized,
r#"[settings.app]
[settings.modeling]
[settings.text_editor]
[settings.command_bar]
"#
);
let parsed = ProjectConfiguration::backwards_compatible_toml_parse(empty_settings_file).unwrap();
assert_eq!(parsed, ProjectConfiguration::default());
}
#[test]
fn test_project_settings_color_validation_error() {
let settings_file = r#"[settings.app.appearance]
color = 1567.4"#;
let result = ProjectConfiguration::backwards_compatible_toml_parse(settings_file);
if let Ok(r) = result {
panic!("Expected an error, but got success: {:?}", r);
}
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("color: Validation error: color"));
}
}

View File

@ -0,0 +1,42 @@
//! Utility functions for settings.
use std::path::PathBuf;
use anyhow::Result;
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> {
let mut entry = FileEntry {
name: dir
.file_name()
.ok_or_else(|| anyhow::anyhow!("No file name"))?
.to_string_lossy()
.to_string(),
path: dir.display().to_string(),
children: None,
};
let mut children = vec![];
let mut entries = tokio::fs::read_dir(&dir).await?;
while let Some(e) = entries.next_entry().await? {
if e.file_type().await?.is_dir() {
children.push(walk_dir(&e.path()).await?);
} else {
children.push(FileEntry {
name: e.file_name().to_string_lossy().to_string(),
path: e.path().display().to_string(),
children: None,
});
}
}
if !children.is_empty() {
entry.children = Some(children);
}
Ok(entry)
}

View File

@ -1,19 +1,7 @@
//! Functions for interacting with TOML files.
//! We do this in rust because the Javascript TOML libraries are actual trash.
use gloo_utils::format::JsValueSerdeExt;
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
#[wasm_bindgen]
pub fn toml_parse(s: &str) -> Result<JsValue, String> {
console_error_panic_hook::set_once();
let value: toml::Value = toml::from_str(s).map_err(|e| e.to_string())?;
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead.
JsValue::from_serde(&value).map_err(|e| e.to_string())
}
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn toml_stringify(json: &str) -> Result<String, String> {

View File

@ -7,7 +7,7 @@ use std::{
use futures::stream::TryStreamExt;
use gloo_utils::format::JsValueSerdeExt;
use kcl_lib::{coredump::CoreDump, engine::EngineManager};
use kcl_lib::{coredump::CoreDump, engine::EngineManager, executor::ExecutorSettings};
use tower_lsp::{LspService, Server};
use wasm_bindgen::prelude::*;
@ -26,7 +26,7 @@ pub async fn execute_wasm(
let program: kcl_lib::ast::types::Program = serde_json::from_str(program_str).map_err(|e| e.to_string())?;
let memory: kcl_lib::executor::ProgramMemory = serde_json::from_str(memory_str).map_err(|e| e.to_string())?;
let units = kittycad::types::UnitLength::from_str(units).map_err(|e| e.to_string())?;
let units = kcl_lib::settings::types::UnitLength::from_str(units).map_err(|e| e.to_string())?;
let engine = kcl_lib::engine::conn_wasm::EngineConnection::new(engine_manager)
.await
@ -36,7 +36,10 @@ pub async fn execute_wasm(
engine: Arc::new(Box::new(engine)),
fs,
stdlib: std::sync::Arc::new(kcl_lib::std::StdLib::new()),
settings: ExecutorSettings {
units,
..Default::default()
},
is_mock,
};
@ -220,7 +223,7 @@ pub async fn kcl_lsp_run(
let file_manager = Arc::new(kcl_lib::fs::FileManager::new(fs));
let executor_ctx = if let Some(engine_manager) = engine_manager {
let units = kittycad::types::UnitLength::from_str(units).map_err(|e| e.to_string())?;
let units = kcl_lib::settings::types::UnitLength::from_str(units).map_err(|e| e.to_string())?;
let engine = kcl_lib::engine::conn_wasm::EngineConnection::new(engine_manager)
.await
.map_err(|e| format!("{:?}", e))?;
@ -228,7 +231,10 @@ pub async fn kcl_lsp_run(
engine: Arc::new(Box::new(engine)),
fs: file_manager.clone(),
stdlib: std::sync::Arc::new(stdlib),
settings: ExecutorSettings {
units,
..Default::default()
},
is_mock: false,
})
} else {
@ -455,3 +461,53 @@ pub async fn coredump(core_dump_manager: kcl_lib::coredump::wasm::CoreDumpManage
// gloo-serialize crate instead.
JsValue::from_serde(&dump).map_err(|e| e.to_string())
}
/// Get the default app settings.
#[wasm_bindgen]
pub fn default_app_settings() -> Result<JsValue, String> {
console_error_panic_hook::set_once();
let settings = kcl_lib::settings::types::Configuration::default();
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead.
JsValue::from_serde(&settings).map_err(|e| e.to_string())
}
/// Parse the app settings.
#[wasm_bindgen]
pub fn parse_app_settings(toml_str: &str) -> Result<JsValue, String> {
console_error_panic_hook::set_once();
let settings = kcl_lib::settings::types::Configuration::backwards_compatible_toml_parse(&toml_str)
.map_err(|e| e.to_string())?;
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead.
JsValue::from_serde(&settings).map_err(|e| e.to_string())
}
/// Get the default project settings.
#[wasm_bindgen]
pub fn default_project_settings() -> Result<JsValue, String> {
console_error_panic_hook::set_once();
let settings = kcl_lib::settings::types::project::ProjectConfiguration::default();
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead.
JsValue::from_serde(&settings).map_err(|e| e.to_string())
}
/// Parse the project settings.
#[wasm_bindgen]
pub fn parse_project_settings(toml_str: &str) -> Result<JsValue, String> {
console_error_panic_hook::set_once();
let settings = kcl_lib::settings::types::project::ProjectConfiguration::backwards_compatible_toml_parse(&toml_str)
.map_err(|e| e.to_string())?;
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead.
JsValue::from_serde(&settings).map_err(|e| e.to_string())
}

View File

@ -1,8 +1,9 @@
use anyhow::Result;
use kcl_lib::executor::ExecutorSettings;
/// Executes a kcl program and takes a snapshot of the result.
/// This returns the bytes of the snapshot.
async fn execute_and_snapshot(code: &str, units: kittycad::types::UnitLength) -> Result<image::DynamicImage> {
async fn execute_and_snapshot(code: &str, units: kcl_lib::settings::types::UnitLength) -> Result<image::DynamicImage> {
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
let http_client = reqwest::Client::builder()
.user_agent(user_agent)
@ -38,7 +39,14 @@ async fn execute_and_snapshot(code: &str, units: kittycad::types::UnitLength) ->
let tokens = kcl_lib::token::lexer(code)?;
let parser = kcl_lib::parser::Parser::new(tokens);
let program = parser.ast()?;
let ctx = kcl_lib::executor::ExecutorContext::new(ws, units.clone()).await?;
let ctx = kcl_lib::executor::ExecutorContext::new(
ws,
ExecutorSettings {
units,
..Default::default()
},
)
.await?;
let _ = ctx.run(program, None).await?;
@ -100,7 +108,7 @@ const part002 = startSketchOn(part001, "here")
|> extrude(5, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face.png", &result, 0.999);
@ -109,7 +117,7 @@ const part002 = startSketchOn(part001, "here")
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_riddle_small() {
let code = include_str!("inputs/riddle_small.kcl");
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/riddle_small.png", &result, 0.999);
@ -118,7 +126,7 @@ async fn serial_test_riddle_small() {
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_lego() {
let code = include_str!("inputs/lego.kcl");
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/lego.png", &result, 0.999);
@ -127,7 +135,7 @@ async fn serial_test_lego() {
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_pentagon_fillet_desugar() {
let code = include_str!("inputs/pentagon_fillet_desugar.kcl");
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Cm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Cm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/pentagon_fillet_desugar.png", &result, 0.999);
@ -136,7 +144,7 @@ async fn serial_test_pentagon_fillet_desugar() {
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_pentagon_fillet_sugar() {
let code = include_str!("inputs/pentagon_fillet_sugar.kcl");
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Cm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Cm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/pentagon_fillet_sugar.png", &result, 0.999);
@ -166,7 +174,7 @@ const part002 = startSketchOn(part001, "start")
|> extrude(5, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_start.png", &result, 0.999);
@ -175,7 +183,7 @@ const part002 = startSketchOn(part001, "start")
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_mike_stress_lines() {
let code = include_str!("inputs/mike_stress_test.kcl");
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/mike_stress_test.png", &result, 0.999);
@ -205,7 +213,7 @@ const part002 = startSketchOn(part001, "END")
|> extrude(5, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_end.png", &result, 0.999);
@ -235,7 +243,7 @@ const part002 = startSketchOn(part001, "END")
|> extrude(-5, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image(
@ -257,7 +265,7 @@ async fn serial_test_fillet_duplicate_tags() {
|> fillet({radius: 0.5, tags: ["thing", "thing"]}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm).await;
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm).await;
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
@ -277,7 +285,7 @@ async fn serial_test_basic_fillet_cube_start() {
|> fillet({radius: 2, tags: ["thing", "thing2"]}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/basic_fillet_cube_start.png", &result, 0.999);
@ -296,7 +304,7 @@ async fn serial_test_basic_fillet_cube_end() {
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/basic_fillet_cube_end.png", &result, 0.999);
@ -315,7 +323,7 @@ async fn serial_test_basic_fillet_cube_close_opposite() {
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image(
@ -337,7 +345,7 @@ async fn serial_test_basic_fillet_cube_next_adjacent() {
|> fillet({radius: 2, tags: [getNextAdjacentEdge("thing3", %)]}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image(
@ -359,7 +367,7 @@ async fn serial_test_basic_fillet_cube_previous_adjacent() {
|> fillet({radius: 2, tags: [getPreviousAdjacentEdge("thing3", %)]}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image(
@ -386,7 +394,7 @@ async fn serial_test_execute_with_function_sketch() {
const fnBox = box(3, 6, 10)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/function_sketch.png", &result, 0.999);
@ -408,7 +416,7 @@ async fn serial_test_execute_with_function_sketch_with_position() {
const thing = box([0,0], 3, 6, 10)"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image(
@ -431,7 +439,7 @@ async fn serial_test_execute_with_angled_line() {
|> extrude(4, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/angled_line.png", &result, 0.999);
@ -459,7 +467,7 @@ const bracket = startSketchOn('XY')
|> extrude(width, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/parametric.png", &result, 0.999);
@ -495,7 +503,7 @@ const bracket = startSketchAt([0, 0])
|> extrude(width, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/parametric_with_tan_arc.png", &result, 0.999);
@ -512,7 +520,7 @@ async fn serial_test_execute_engine_error_return() {
|> extrude(4, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm).await;
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm).await;
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
@ -525,7 +533,7 @@ async fn serial_test_execute_i_shape() {
// This is some code from lee that starts a pipe expression with a variable.
let code = include_str!("inputs/i_shape.kcl");
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/i_shape.png", &result, 0.999);
@ -536,7 +544,7 @@ async fn serial_test_execute_i_shape() {
async fn serial_test_execute_pipes_on_pipes() {
let code = include_str!("inputs/pipes_on_pipes.kcl");
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/pipes_on_pipes.png", &result, 0.999);
@ -546,7 +554,7 @@ async fn serial_test_execute_pipes_on_pipes() {
async fn serial_test_execute_cylinder() {
let code = include_str!("inputs/cylinder.kcl");
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cylinder.png", &result, 0.999);
@ -556,7 +564,7 @@ async fn serial_test_execute_cylinder() {
async fn serial_test_execute_kittycad_svg() {
let code = include_str!("inputs/kittycad_svg.kcl");
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/kittycad_svg.png", &result, 0.999);
@ -581,7 +589,7 @@ const pt1 = b1.value[0]
const pt2 = b2.value[0]
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image(
@ -599,7 +607,7 @@ async fn serial_test_helix_defaults() {
|> helix({revolutions: 16, angle_start: 0}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/helix_defaults.png", &result, 1.0);
@ -613,7 +621,7 @@ async fn serial_test_helix_defaults_negative_extrude() {
|> helix({revolutions: 16, angle_start: 0}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image(
@ -631,7 +639,7 @@ async fn serial_test_helix_ccw() {
|> helix({revolutions: 16, angle_start: 0, ccw: true}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/helix_ccw.png", &result, 1.0);
@ -645,7 +653,7 @@ async fn serial_test_helix_with_length() {
|> helix({revolutions: 16, angle_start: 0, length: 3}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/helix_with_length.png", &result, 1.0);
@ -661,7 +669,7 @@ async fn serial_test_dimensions_match() {
|> close(%)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/dimensions_match.png", &result, 1.0);
@ -680,7 +688,7 @@ const body = startSketchOn('XY')
|> extrude(height, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/close_arc.png", &result, 0.999);
@ -708,7 +716,7 @@ box(10, 23, 8)
let thing = box(-12, -15, 10)
box(-20, -5, 10)"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/negative_args.png", &result, 0.999);
@ -723,7 +731,7 @@ async fn serial_test_basic_tangential_arc() {
|> extrude(10, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/tangential_arc.png", &result, 0.999);
@ -738,7 +746,7 @@ async fn serial_test_basic_tangential_arc_with_point() {
|> extrude(10, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/tangential_arc_with_point.png", &result, 0.999);
@ -753,7 +761,7 @@ async fn serial_test_basic_tangential_arc_to() {
|> extrude(10, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/tangential_arc_to.png", &result, 0.999);
@ -782,7 +790,7 @@ box(30, 43, 18, '-xy')
let thing = box(-12, -15, 10, 'yz')
box(-20, -5, 10, 'xy')"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image(
@ -846,7 +854,7 @@ const part004 = startSketchOn('YZ')
|> close(%)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/lots_of_planes.png", &result, 0.999);
@ -865,7 +873,7 @@ async fn serial_test_holes() {
|> extrude(2, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/holes.png", &result, 0.999);
@ -885,7 +893,7 @@ async fn optional_params() {
const thing = other_circle([2, 2], 20)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/optional_params.png", &result, 0.999);
@ -923,7 +931,7 @@ const part = roundedRectangle([0, 0], 20, 20, 4)
|> extrude(2, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/rounded_with_holes.png", &result, 0.999);
@ -933,7 +941,7 @@ const part = roundedRectangle([0, 0], 20, 20, 4)
async fn serial_test_top_level_expression() {
let code = r#"startSketchOn('XY') |> circle([0,0], 22, %) |> extrude(14, %)"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/top_level_expression.png", &result, 0.999);
@ -949,7 +957,7 @@ const part = startSketchOn('XY')
|> extrude(1, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image(
@ -967,7 +975,7 @@ async fn serial_test_patterns_linear_basic() {
|> extrude(1, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic.png", &result, 0.999);
@ -985,7 +993,7 @@ async fn serial_test_patterns_linear_basic_3d() {
|> patternLinear3d({axis: [1, 0, 1], repetitions: 3, distance: 6}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic_3d.png", &result, 0.999);
@ -998,7 +1006,7 @@ async fn serial_test_patterns_linear_basic_negative_distance() {
|> patternLinear2d({axis: [0,1], repetitions: 12, distance: -2}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image(
@ -1015,7 +1023,7 @@ async fn serial_test_patterns_linear_basic_negative_axis() {
|> patternLinear2d({axis: [0,-1], repetitions: 12, distance: 2}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image(
@ -1041,7 +1049,7 @@ const rectangle = startSketchOn('XY')
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic_holes.png", &result, 0.999);
@ -1054,7 +1062,7 @@ async fn serial_test_patterns_circular_basic_2d() {
|> patternCircular2d({center: [20, 20], repetitions: 12, arcDegrees: 210, rotateDuplicates: true}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_circular_basic_2d.png", &result, 0.999);
@ -1072,7 +1080,7 @@ async fn serial_test_patterns_circular_basic_3d() {
|> patternCircular3d({axis: [0,0, 1], center: [-20, -20, -20], repetitions: 40, arcDegrees: 360, rotateDuplicates: false}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_circular_basic_3d.png", &result, 0.999);
@ -1090,7 +1098,7 @@ async fn serial_test_patterns_circular_3d_tilted_axis() {
|> patternCircular3d({axis: [1,1,0], center: [10, 0, 10], repetitions: 10, arcDegrees: 360, rotateDuplicates: true}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image(
@ -1104,7 +1112,7 @@ async fn serial_test_patterns_circular_3d_tilted_axis() {
async fn serial_test_import_file_doesnt_exist() {
let code = r#"const model = import("thing.obj")"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm).await;
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm).await;
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
@ -1116,7 +1124,7 @@ async fn serial_test_import_file_doesnt_exist() {
async fn serial_test_import_obj_with_mtl() {
let code = r#"const model = import("tests/executor/inputs/cube.obj")"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_obj_with_mtl.png", &result, 0.999);
@ -1126,7 +1134,7 @@ async fn serial_test_import_obj_with_mtl() {
async fn serial_test_import_obj_with_mtl_units() {
let code = r#"const model = import("tests/executor/inputs/cube.obj", {type: "obj", units: "m"})"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_obj_with_mtl_units.png", &result, 0.999);
@ -1136,7 +1144,7 @@ async fn serial_test_import_obj_with_mtl_units() {
async fn serial_test_import_gltf_with_bin() {
let code = r#"const model = import("tests/executor/inputs/cube.gltf")"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_gltf_with_bin.png", &result, 0.999);
@ -1146,7 +1154,7 @@ async fn serial_test_import_gltf_with_bin() {
async fn serial_test_import_gltf_embedded() {
let code = r#"const model = import("tests/executor/inputs/cube-embedded.gltf")"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_gltf_embedded.png", &result, 0.999);
@ -1156,7 +1164,7 @@ async fn serial_test_import_gltf_embedded() {
async fn serial_test_import_glb() {
let code = r#"const model = import("tests/executor/inputs/cube.glb")"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_glb.png", &result, 0.999);
@ -1166,7 +1174,7 @@ async fn serial_test_import_glb() {
async fn serial_test_import_glb_no_assign() {
let code = r#"import("tests/executor/inputs/cube.glb")"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_glb_no_assign.png", &result, 0.999);
@ -1176,7 +1184,7 @@ async fn serial_test_import_glb_no_assign() {
async fn serial_test_import_ext_doesnt_match() {
let code = r#"const model = import("tests/executor/inputs/cube.gltf", {type: "obj", units: "m"})"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm).await;
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm).await;
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
@ -1201,7 +1209,7 @@ async fn serial_test_cube_mm() {
const myCube = cube([0,0], 10)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_mm.png", &result, 1.0);
@ -1224,7 +1232,7 @@ async fn serial_test_cube_cm() {
const myCube = cube([0,0], 10)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Cm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Cm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_cm.png", &result, 1.0);
@ -1247,7 +1255,7 @@ async fn serial_test_cube_m() {
const myCube = cube([0,0], 10)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::M)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::M)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_m.png", &result, 1.0);
@ -1270,7 +1278,7 @@ async fn serial_test_cube_in() {
const myCube = cube([0,0], 10)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::In)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::In)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_in.png", &result, 1.0);
@ -1293,7 +1301,7 @@ async fn serial_test_cube_ft() {
const myCube = cube([0,0], 10)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Ft)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Ft)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_ft.png", &result, 1.0);
@ -1316,7 +1324,7 @@ async fn serial_test_cube_yd() {
const myCube = cube([0,0], 10)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Yd)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Yd)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/cube_yd.png", &result, 1.0);
@ -1346,7 +1354,7 @@ const part002 = startSketchOn(part001, "here")
|> extrude(1, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm).await;
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm).await;
assert!(result.is_err());
assert_eq!(
@ -1387,7 +1395,7 @@ const part003 = startSketchOn(part002, "end")
|> extrude(5, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_of_face.png", &result, 1.0);
@ -1406,7 +1414,7 @@ async fn serial_test_stdlib_kcl_error_right_code_path() {
|> extrude(2, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm).await;
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm).await;
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
@ -1434,7 +1442,7 @@ const part002 = startSketchOn(part001, "end")
|> extrude(5, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_circle.png", &result, 1.0);
@ -1460,7 +1468,7 @@ const part002 = startSketchOn(part001, "end")
|> extrude(5, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_circle_tagged.png", &result, 1.0);
@ -1504,7 +1512,7 @@ const part = rectShape([0, 0], 20, 20)
}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm).await;
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm).await;
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
@ -1525,7 +1533,7 @@ async fn serial_test_big_number_angle_to_match_length_x() {
|> extrude(10, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image(
@ -1548,7 +1556,7 @@ async fn serial_test_big_number_angle_to_match_length_y() {
|> extrude(10, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image(
@ -1574,7 +1582,7 @@ async fn serial_test_simple_revolve() {
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve.png", &result, 1.0);
@ -1596,7 +1604,7 @@ async fn serial_test_simple_revolve_uppercase() {
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_uppercase.png", &result, 1.0);
@ -1618,7 +1626,7 @@ async fn serial_test_simple_revolve_negative() {
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_negative.png", &result, 1.0);
@ -1640,7 +1648,7 @@ async fn serial_test_revolve_bad_angle_low() {
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm).await;
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm).await;
assert!(result.is_err());
assert_eq!(
@ -1665,7 +1673,7 @@ async fn serial_test_revolve_bad_angle_high() {
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm).await;
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm).await;
assert!(result.is_err());
assert_eq!(
@ -1690,7 +1698,7 @@ async fn serial_test_simple_revolve_custom_angle() {
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_custom_angle.png", &result, 1.0);
@ -1712,7 +1720,7 @@ async fn serial_test_simple_revolve_custom_axis() {
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_custom_axis.png", &result, 1.0);
@ -1738,7 +1746,7 @@ const sketch001 = startSketchOn(box, "end")
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/revolve_on_edge.png", &result, 1.0);
@ -1764,7 +1772,7 @@ const sketch001 = startSketchOn(box, "revolveAxis")
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm).await;
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm).await;
assert!(result.is_err());
assert_eq!(
@ -1791,7 +1799,7 @@ const sketch001 = startSketchOn(box, "END")
}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/revolve_on_face_circle_edge.png", &result, 1.0);
@ -1815,7 +1823,7 @@ const sketch001 = startSketchOn(box, "END")
}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/revolve_on_face_circle.png", &result, 1.0);
@ -1843,7 +1851,7 @@ const sketch001 = startSketchOn(box, "end")
}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/revolve_on_face.png", &result, 1.0);
@ -1859,7 +1867,7 @@ async fn serial_test_basic_revolve_circle() {
}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/basic_revolve_circle.png", &result, 1.0);
@ -1888,7 +1896,7 @@ const part002 = startSketchOn(part001, 'end')
|> extrude(5, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/simple_revolve_sketch_on_edge.png", &result, 1.0);
@ -1951,7 +1959,7 @@ const plumbus0 = make_circle(p, 'a', [0, 0], 2.5)
// }, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/plumbus_fillets.png", &result, 1.0);
@ -1961,7 +1969,7 @@ const plumbus0 = make_circle(p, 'a', [0, 0], 2.5)
async fn serial_test_empty_file_is_ok() {
let code = r#""#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm).await;
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm).await;
assert!(result.is_ok());
}
@ -1991,7 +1999,7 @@ async fn serial_test_member_expression_in_params() {
capScrew([0, 0.5, 0], 50, 37.5, 50, 25)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/member_expression_in_params.png", &result, 1.0);
@ -2038,7 +2046,7 @@ const bracket = startSketchOn('XY')
}, %)
"#;
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm).await;
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm).await;
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),

View File

@ -39,7 +39,7 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, uuid
let tokens = kcl_lib::token::lexer(code)?;
let parser = kcl_lib::parser::Parser::new(tokens);
let program = parser.ast()?;
let ctx = kcl_lib::executor::ExecutorContext::new(ws, kittycad::types::UnitLength::Mm).await?;
let ctx = kcl_lib::executor::ExecutorContext::new(ws, Default::default()).await?;
let memory = ctx.run(program.clone(), None).await?;
// We need to get the sketch ID.