diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8f477b8de..ff9bf59a9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -130,8 +130,9 @@ jobs:
matrix:
os: [macos-14, ubuntu-latest, windows-latest]
env:
- TAURI_ARGS_MACOS: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }}
- TAURI_ARGS_UBUNTU: ${{ matrix.os == 'ubuntu-latest' && '--bundles' || '' }}
+ TAURI_ARGS_MACOS: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin --config src-tauri/tauri.release-macos.conf.json' || '' }}
+ TAURI_ARGS_UBUNTU: ${{ matrix.os == 'ubuntu-latest' && '--bundles --config src-tauri/tauri.release.conf.json' || '' }}
+ TAURI_ARGS_WINDOWS: ${{ matrix.os == 'windows-latest' && '--bundles --config src-tauri\\tauri.release.conf.json' || '' }}
steps:
- uses: actions/checkout@v4
@@ -235,7 +236,7 @@ jobs:
with:
includeRelease: false
includeDebug: true
- args: "${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
+ args: "${{ env.TAURI_ARGS_WINDOWS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
- name: Mac App Store
if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'macos-14' }}
@@ -276,7 +277,7 @@ jobs:
rm src-tauri/Cargo.toml.bu
git diff src-tauri/Cargo.toml
- yarn tauri build --target "${target}" --verbose --config src-tauri/tauri.appstore.conf.json
+ yarn tauri build --target "${target}" --verbose --config src-tauri/tauri.app-store.conf.json
app_path="src-tauri/target/${target}/release/bundle/macos/Zoo Modeling App.app"
build_name="src-tauri/target/${target}/release/bundle/macos/Zoo Modeling App.pkg"
@@ -336,9 +337,8 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
with:
- args: "${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
+ args: "${{ env.TAURI_ARGS_WINDOWS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
- uses: actions/upload-artifact@v3
if: matrix.os != 'ubuntu-latest'
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index b4a426303..c105d3f95 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -169,6 +169,7 @@ dependencies = [
"tauri-plugin-updater",
"tokio",
"toml 0.8.12",
+ "url",
]
[[package]]
@@ -4198,9 +4199,9 @@ dependencies = [
[[package]]
name = "schemars"
-version = "0.8.16"
+version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29"
+checksum = "7f55c82c700538496bdc329bb4918a81f87cc8888811bd123cf325a0f2f8d309"
dependencies = [
"bigdecimal",
"bytes",
@@ -4216,14 +4217,14 @@ dependencies = [
[[package]]
name = "schemars_derive"
-version = "0.8.16"
+version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967"
+checksum = "83263746fe5e32097f06356968a077f96089739c927a61450efa069905eec108"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
- "syn 1.0.109",
+ "syn 2.0.60",
]
[[package]]
@@ -4302,9 +4303,9 @@ dependencies = [
[[package]]
name = "serde"
-version = "1.0.198"
+version = "1.0.199"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
+checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a"
dependencies = [
"serde_derive",
]
@@ -4320,9 +4321,9 @@ dependencies = [
[[package]]
name = "serde_derive"
-version = "1.0.198"
+version = "1.0.199"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
+checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc"
dependencies = [
"proc-macro2",
"quote",
@@ -4331,13 +4332,13 @@ dependencies = [
[[package]]
name = "serde_derive_internals"
-version = "0.26.0"
+version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c"
+checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "syn 2.0.60",
]
[[package]]
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 2958e13a8..ca7dfcebf 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -31,6 +31,7 @@ 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", "fs", "process"] }
toml = "0.8.2"
+url = "2.5.0"
[features]
default = ["updater"]
diff --git a/src-tauri/Info.plist b/src-tauri/Info.plist
index af12f55cf..8a6b8806e 100644
--- a/src-tauri/Info.plist
+++ b/src-tauri/Info.plist
@@ -34,6 +34,161 @@
LSFileQuarantineEnabled
+ CFBundleDocumentTypes
+
+
+ LSItemContentTypes
+
+ dev.zoo.kcl
+
+ CFBundleTypeName
+ KCL
+ CFBundleTypeRole
+ Editor
+ LSTypeIsPackage
+
+ LSHandlerRank
+ Owner
+
+
+ LSItemContentTypes
+
+ dev.zoo.toml
+
+ CFBundleTypeName
+ TOML
+ CFBundleTypeRole
+ Editor
+ LSTypeIsPackage
+
+ LSHandlerRank
+ Default
+
+
+ LSItemContentTypes
+
+ dev.zoo.gltf
+
+ CFBundleTypeName
+ glTF
+ CFBundleTypeRole
+ Editor
+ LSTypeIsPackage
+
+ LSHandlerRank
+ Default
+
+
+ LSItemContentTypes
+
+ dev.zoo.glb
+
+ CFBundleTypeName
+ glb
+ CFBundleTypeRole
+ Editor
+ LSTypeIsPackage
+
+ LSHandlerRank
+ Default
+
+
+ LSItemContentTypes
+
+ dev.zoo.step
+
+ CFBundleTypeName
+ STEP
+ CFBundleTypeRole
+ Editor
+ LSTypeIsPackage
+
+ LSHandlerRank
+ Default
+
+
+ LSItemContentTypes
+
+ dev.zoo.fbx
+
+ CFBundleTypeName
+ FBX
+ CFBundleTypeRole
+ Editor
+ LSTypeIsPackage
+
+ LSHandlerRank
+ Default
+
+
+ LSItemContentTypes
+
+ dev.zoo.sldprt
+
+ CFBundleTypeName
+ Solidworks Part
+ CFBundleTypeRole
+ Viewer
+ LSTypeIsPackage
+
+ LSHandlerRank
+ Default
+
+
+ LSItemContentTypes
+
+ public.geometry-definition-format
+
+ CFBundleTypeName
+ OBJ
+ CFBundleTypeRole
+ Editor
+ LSTypeIsPackage
+
+ LSHandlerRank
+ Default
+
+
+ LSItemContentTypes
+
+ public.polygon-file-format
+
+ CFBundleTypeName
+ PLY
+ CFBundleTypeRole
+ Editor
+ LSTypeIsPackage
+
+ LSHandlerRank
+ Default
+
+
+ LSItemContentTypes
+
+ public.standard-tesselated-geometry-format
+
+ CFBundleTypeName
+ STL
+ CFBundleTypeRole
+ Editor
+ LSTypeIsPackage
+
+ LSHandlerRank
+ Default
+
+
+ LSItemContentTypes
+
+ public.folder
+
+ CFBundleTypeName
+ Folders
+ CFBundleTypeRole
+ Viewer
+ LSHandlerRank
+ Alternate
+
+
UTExportedTypeDeclarations
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index a50e6c51e..d7882dab1 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -12,7 +12,7 @@ use anyhow::Result;
use kcl_lib::settings::types::{
file::{FileEntry, Project, ProjectRoute, ProjectState},
project::ProjectConfiguration,
- Configuration, DEFAULT_PROJECT_KCL_FILE,
+ Configuration,
};
use oauth2::TokenResponse;
use tauri::{ipc::InvokeError, Manager};
@@ -350,6 +350,31 @@ fn show_in_folder(path: &str) -> Result<(), InvokeError> {
Ok(())
}
+#[allow(dead_code)]
+fn open_url_sync(app: &tauri::AppHandle, url: &url::Url) {
+ println!("Opening URL: {:?}", url);
+ let cloned_url = url.clone();
+ let runner: tauri::async_runtime::JoinHandle> = tauri::async_runtime::spawn(async move {
+ let url_str = cloned_url.to_string();
+ let path = Path::new(url_str.as_str());
+ ProjectState::new_from_path(path.to_path_buf()).await
+ });
+
+ // Block on the handle.
+ match tauri::async_runtime::block_on(runner) {
+ Ok(Ok(store)) => {
+ // Create a state object to hold the project.
+ app.manage(state::Store::new(store));
+ }
+ Err(e) => {
+ println!("Error opening URL:{} {:?}", url, e);
+ }
+ Ok(Err(e)) => {
+ println!("Error opening URL:{} {:?}", url, e);
+ }
+ }
+}
+
fn main() -> Result<()> {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
@@ -410,6 +435,7 @@ fn main() -> Result<()> {
if let Some(source_arg) = matches.args.get("source") {
// We don't do an else here because this can be null.
if let Some(value) = source_arg.value.as_str() {
+ println!("Got path in cli argument: {}", value);
source_path = Some(Path::new(value).to_path_buf());
}
}
@@ -419,6 +445,10 @@ fn main() -> Result<()> {
}
}
+ if verbose {
+ println!("Verbose mode enabled.");
+ }
+
// If we have a source path to open, make sure it exists.
let Some(source_path) = source_path else {
// The user didn't provide a source path to open.
@@ -436,74 +466,7 @@ fn main() -> Result<()> {
}
let runner: tauri::async_runtime::JoinHandle> =
- tauri::async_runtime::spawn(async move {
- // Fix for "." path, which is the current directory.
- let source_path = if source_path == Path::new(".") {
- std::env::current_dir()
- .map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
- } else {
- source_path
- };
-
- // If the path does not start with a slash, it is a relative path.
- // We need to convert it to an absolute path.
- let source_path = if source_path.is_relative() {
- std::env::current_dir()
- .map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
- .join(source_path)
- } else {
- source_path
- };
-
- // If the path is a directory, let's assume it is a project directory.
- if source_path.is_dir() {
- // Load the details about the project from the path.
- let project = Project::from_path(&source_path).await.map_err(|e| {
- anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e)
- })?;
-
- if verbose {
- println!("Project loaded from path: {}", source_path.display());
- }
-
- // Create the default file in the project.
- // Write the initial project file.
- let project_file = source_path.join(DEFAULT_PROJECT_KCL_FILE);
- tokio::fs::write(&project_file, vec![]).await?;
-
- return Ok(ProjectState {
- project,
- current_file: Some(project_file.display().to_string()),
- });
- }
-
- // We were given a file path, not a directory.
- // Let's get the parent directory of the file.
- let parent = source_path.parent().ok_or_else(|| {
- anyhow::anyhow!(
- "Error getting the parent directory of the file: {}",
- source_path.display()
- )
- })?;
-
- // Load the details about the project from the parent directory.
- let project = Project::from_path(&parent).await.map_err(|e| {
- anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e)
- })?;
-
- if verbose {
- println!(
- "Project loaded from path: {}, current file: {}",
- parent.display(),
- source_path.display()
- );
- }
-
- Ok(ProjectState {
- project,
- current_file: Some(source_path.display().to_string()),
- })
- });
+ tauri::async_runtime::spawn(async move { ProjectState::new_from_path(source_path).await });
// Block on the handle.
let store = tauri::async_runtime::block_on(runner)??;
@@ -512,13 +475,30 @@ fn main() -> Result<()> {
app.manage(state::Store::new(store));
// Listen on the deep links.
- app.listen("deep-link://new-url", |url| {
- dbg!(url);
+ app.listen("deep-link://new-url", |event| {
+ println!("got deep-link url: {:?}", event);
+ // TODO: open_url_sync(app.handle(), event.url);
});
Ok(())
})
- .run(tauri::generate_context!())?;
+ .build(tauri::generate_context!())?
+ .run(
+ #[allow(unused_variables)]
+ |app, event| {
+ #[cfg(any(target_os = "macos", target_os = "ios"))]
+ if let tauri::RunEvent::Opened { urls } = event {
+ println!("Opened URLs: {:?}", urls);
+
+ // Handle the first URL.
+ // TODO: do we want to handle more than one URL?
+ // Under what conditions would we even have more than one?
+ if let Some(url) = urls.first() {
+ open_url_sync(app, url);
+ }
+ }
+ },
+ );
Ok(())
}
diff --git a/src-tauri/tauri.appstore.conf.json b/src-tauri/tauri.app-store.conf.json
similarity index 100%
rename from src-tauri/tauri.appstore.conf.json
rename to src-tauri/tauri.app-store.conf.json
diff --git a/src-tauri/tauri.release-macos.conf.json b/src-tauri/tauri.release-macos.conf.json
new file mode 100644
index 000000000..b77568ed2
--- /dev/null
+++ b/src-tauri/tauri.release-macos.conf.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "../node_modules/@tauri-apps/cli/schema.json",
+ "bundle": {
+ "windows": {
+ "certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
+ "digestAlgorithm": "sha256",
+ "timestampUrl": "http://timestamp.digicert.com"
+ }
+ },
+ "plugins": {
+ "updater": {
+ "active": true,
+ "endpoints": [
+ "https://dl.zoo.dev/releases/modeling-app/last_update.json"
+ ],
+ "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
+ }
+ }
+}
diff --git a/src-tauri/tauri.release.conf.json b/src-tauri/tauri.release.conf.json
index b77568ed2..7d88d3201 100644
--- a/src-tauri/tauri.release.conf.json
+++ b/src-tauri/tauri.release.conf.json
@@ -5,7 +5,45 @@
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
"digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.digicert.com"
- }
+ },
+ "fileAssociations": [
+ {
+ "ext": ["kcl"],
+ "mimeType": "text/vnd.zoo.kcl"
+ },
+ {
+ "ext": ["obj"],
+ "mimeType": "model/obj"
+ },
+ {
+ "ext": ["gltf"],
+ "mimeType": "model/gltf+json"
+ },
+ {
+ "ext": ["glb"],
+ "mimeType": "model/gltf+binary"
+ },
+ {
+ "ext": ["fbx", "fbxb"],
+ "mimeType": "model/fbx"
+ },
+ {
+ "ext": ["stl"],
+ "mimeType": "model/stl"
+ },
+ {
+ "ext": ["ply"],
+ "mimeType": "model/ply"
+ },
+ {
+ "ext": ["step", "stp"],
+ "mimeType": "model/step"
+ },
+ {
+ "ext": ["sldprt"],
+ "mimeType": "model/sldprt"
+ }
+ ]
},
"plugins": {
"updater": {
diff --git a/src/wasm-lib/kcl/src/settings/types/file.rs b/src/wasm-lib/kcl/src/settings/types/file.rs
index 819d533c8..fbb63893d 100644
--- a/src/wasm-lib/kcl/src/settings/types/file.rs
+++ b/src/wasm-lib/kcl/src/settings/types/file.rs
@@ -1,11 +1,13 @@
//! Types for interacting with files in projects.
+use std::path::{Path, PathBuf};
+
use anyhow::Result;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use super::Configuration;
+use crate::settings::types::{Configuration, DEFAULT_PROJECT_KCL_FILE};
/// State management for the application.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
@@ -16,6 +18,88 @@ pub struct ProjectState {
pub current_file: Option,
}
+impl ProjectState {
+ /// Create a new project state from a path.
+ #[cfg(not(target_arch = "wasm32"))]
+ pub async fn new_from_path(path: PathBuf) -> Result {
+ // Fix for "." path, which is the current directory.
+
+ let source_path = if path == Path::new(".") {
+ std::env::current_dir().map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
+ } else {
+ path
+ };
+
+ // If the path does not start with a slash, it is a relative path.
+ // We need to convert it to an absolute path.
+ let source_path = if source_path.is_relative() {
+ std::env::current_dir()
+ .map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
+ .join(source_path)
+ } else {
+ source_path
+ };
+
+ // If the path is a directory, let's assume it is a project directory.
+ if source_path.is_dir() {
+ // Load the details about the project from the path.
+ let project = Project::from_path(&source_path)
+ .await
+ .map_err(|e| anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e))?;
+
+ // Check if we have a main.kcl file in the project.
+ let project_file = source_path.join(DEFAULT_PROJECT_KCL_FILE);
+
+ if !project_file.exists() {
+ // Create the default file in the project.
+ // Write the initial project file.
+ tokio::fs::write(&project_file, vec![]).await?;
+ }
+
+ return Ok(ProjectState {
+ project,
+ current_file: Some(project_file.display().to_string()),
+ });
+ }
+
+ // Check if the extension on what we are trying to open is a relevant file type.
+ // Get the extension of the file.
+ let extension = source_path
+ .extension()
+ .ok_or_else(|| anyhow::anyhow!("Error getting the extension of the file: {}", source_path.display()))?;
+ let ext = extension.to_string_lossy().to_string();
+
+ // Check if the extension is a relevant file type.
+ if !crate::settings::utils::RELEVANT_EXTENSIONS.contains(&ext) || ext == "toml" {
+ return Err(anyhow::anyhow!(
+ "File type ({}) cannot be opened with this app: {}, try opening one of the following file types: {}",
+ ext,
+ source_path.display(),
+ crate::settings::utils::RELEVANT_EXTENSIONS.join(", ")
+ ));
+ }
+
+ // We were given a file path, not a directory.
+ // Let's get the parent directory of the file.
+ let parent = source_path.parent().ok_or_else(|| {
+ anyhow::anyhow!(
+ "Error getting the parent directory of the file: {}",
+ source_path.display()
+ )
+ })?;
+
+ // Load the details about the project from the parent directory.
+ let project = Project::from_path(&parent)
+ .await
+ .map_err(|e| anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e))?;
+
+ Ok(ProjectState {
+ project,
+ current_file: Some(source_path.display().to_string()),
+ })
+ }
+}
+
/// Project route information.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
diff --git a/src/wasm-lib/kcl/src/settings/utils.rs b/src/wasm-lib/kcl/src/settings/utils.rs
index b91ff7072..044c40489 100644
--- a/src/wasm-lib/kcl/src/settings/utils.rs
+++ b/src/wasm-lib/kcl/src/settings/utils.rs
@@ -8,8 +8,8 @@ use clap::ValueEnum;
use crate::settings::types::file::FileEntry;
lazy_static::lazy_static! {
- static ref RELEVANT_EXTENSIONS: Vec = {
- let mut relevant_extensions = vec!["kcl".to_string(), "stp".to_string(), "glb".to_string()];
+ pub static ref RELEVANT_EXTENSIONS: Vec = {
+ let mut relevant_extensions = vec!["kcl".to_string(), "stp".to_string(), "glb".to_string(), "fbxb".to_string()];
let named_extensions = kittycad::types::FileImportFormat::value_variants()
.iter()
.map(|x| format!("{}", x))