* add print button Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup Signed-off-by: Jess Frazelle <github@jessfraz.com> * generate more types Signed-off-by: Jess Frazelle <github@jessfraz.com> * add a github action to generate machine api-types Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix Signed-off-by: Jess Frazelle <github@jessfraz.com> * New machine-api types * actually print on the real machine Signed-off-by: Jess Frazelle <github@jessfraz.com> * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * add more Signed-off-by: Jess Frazelle <github@jessfraz.com> * New machine-api types * get the current machine Signed-off-by: Jess Frazelle <github@jessfraz.com> * New machine-api types * know when error 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> * fmt Signed-off-by: Jess Frazelle <github@jessfraz.com> * add fmt Signed-off-by: Jess Frazelle <github@jessfraz.com> * New machine-api types * empty * empty * update machine api Signed-off-by: Jess Frazelle <github@jessfraz.com> * New machine-api types * empty * New machine-api types * emptuy * no circular deps Signed-off-by: Jess Frazelle <github@jessfraz.com> * New machine-api types * remove recursive dep Signed-off-by: Jess Frazelle <github@jessfraz.com> --------- Signed-off-by: Jess Frazelle <github@jessfraz.com> Co-authored-by: Jess Frazelle <github@jessfraz.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
604 lines
21 KiB
Rust
604 lines
21 KiB
Rust
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
|
|
pub(crate) mod state;
|
|
|
|
use std::{
|
|
env,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use anyhow::Result;
|
|
use kcl_lib::settings::types::{
|
|
file::{FileEntry, Project, ProjectRoute, ProjectState},
|
|
project::ProjectConfiguration,
|
|
Configuration,
|
|
};
|
|
use oauth2::TokenResponse;
|
|
use tauri::{ipc::InvokeError, Manager};
|
|
use tauri_plugin_cli::CliExt;
|
|
use tauri_plugin_shell::ShellExt;
|
|
|
|
const DEFAULT_HOST: &str = "https://api.zoo.dev";
|
|
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]
|
|
async fn rename_project_directory(project_path: &str, new_name: &str) -> Result<PathBuf, InvokeError> {
|
|
let project_dir = std::path::Path::new(project_path);
|
|
|
|
kcl_lib::settings::types::file::rename_project_directory(project_dir, new_name)
|
|
.await
|
|
.map_err(InvokeError::from_anyhow)
|
|
}
|
|
|
|
#[tauri::command]
|
|
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))
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn get_state(app: tauri::AppHandle) -> Result<Option<ProjectState>, InvokeError> {
|
|
let store = app.state::<state::Store>();
|
|
Ok(store.get().await)
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn set_state(app: tauri::AppHandle, state: Option<ProjectState>) -> Result<(), InvokeError> {
|
|
let store = app.state::<state::Store>();
|
|
store.set(state).await;
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_app_settings_file_path(app: &tauri::AppHandle) -> Result<PathBuf, InvokeError> {
|
|
let app_config_dir = app.path().app_config_dir()?;
|
|
|
|
// Ensure this directory exists.
|
|
if !app_config_dir.exists() {
|
|
tokio::fs::create_dir_all(&app_config_dir)
|
|
.await
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
}
|
|
|
|
Ok(app_config_dir.join(SETTINGS_FILE_NAME))
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn read_app_settings_file(app: tauri::AppHandle) -> Result<Configuration, InvokeError> {
|
|
let mut settings_path = get_app_settings_file_path(&app).await?;
|
|
let mut needs_migration = false;
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn write_app_settings_file(app: tauri::AppHandle, configuration: Configuration) -> Result<(), InvokeError> {
|
|
let settings_path = get_app_settings_file_path(&app).await?;
|
|
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(())
|
|
}
|
|
|
|
async fn get_project_settings_file_path(project_path: &str) -> Result<PathBuf, InvokeError> {
|
|
let project_dir = std::path::Path::new(project_path);
|
|
|
|
if !project_dir.exists() {
|
|
tokio::fs::create_dir_all(&project_dir)
|
|
.await
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
}
|
|
|
|
Ok(project_dir.join(PROJECT_SETTINGS_FILE_NAME))
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn read_project_settings_file(project_path: &str) -> Result<ProjectConfiguration, InvokeError> {
|
|
let settings_path = get_project_settings_file_path(project_path).await?;
|
|
|
|
// 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(
|
|
project_path: &str,
|
|
configuration: ProjectConfiguration,
|
|
) -> Result<(), InvokeError> {
|
|
let settings_path = get_project_settings_file_path(project_path).await?;
|
|
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_path: &str) -> Result<Project, InvokeError> {
|
|
configuration
|
|
.get_project_info(project_path)
|
|
.await
|
|
.map_err(InvokeError::from_anyhow)
|
|
}
|
|
|
|
/// Parse the project route.
|
|
#[tauri::command]
|
|
async fn parse_project_route(configuration: Configuration, route: &str) -> Result<ProjectRoute, InvokeError> {
|
|
ProjectRoute::from_route(&configuration, route).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.
|
|
/// The string returned from this method is the access token.
|
|
#[tauri::command]
|
|
async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError> {
|
|
log::debug!("Logging in...");
|
|
// Do an OAuth 2.0 Device Authorization Grant dance to get a token.
|
|
let device_auth_url = oauth2::DeviceAuthorizationUrl::new(format!("{host}/oauth2/device/auth"))
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
// We can hardcode the client ID.
|
|
// This value is safe to be embedded in version control.
|
|
// This is the client ID of the KittyCAD app.
|
|
let client_id = "2af127fb-e14e-400a-9c57-a9ed08d1a5b7".to_string();
|
|
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()))?,
|
|
Some(
|
|
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
|
),
|
|
)
|
|
.set_auth_type(oauth2::AuthType::RequestBody)
|
|
.set_device_authorization_url(device_auth_url);
|
|
|
|
let details: oauth2::devicecode::StandardDeviceAuthorizationResponse = auth_client
|
|
.exchange_device_code()
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?
|
|
.request_async(oauth2::reqwest::async_http_client)
|
|
.await
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
|
|
let Some(auth_uri) = details.verification_uri_complete() else {
|
|
return Err(InvokeError::from("getting the verification uri failed"));
|
|
};
|
|
|
|
// Open the system browser with the auth_uri.
|
|
// We do this in the browser and not a separate window because we want 1password and
|
|
// other crap to work well.
|
|
// TODO: find a better way to share this value with tauri e2e tests
|
|
// Here we're using an env var to enable the /tmp file (windows not supported for now)
|
|
// 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 {
|
|
log::warn!("E2E_TAURI_ENABLED is set, won't open {} externally", auth_uri.secret());
|
|
let mut temp = String::from("/tmp");
|
|
// Overwrite with Windows variable
|
|
match env::var("TEMP") {
|
|
Ok(val) => temp = val,
|
|
Err(_e) => println!("Fallback to default /tmp"),
|
|
}
|
|
let path = Path::new(&temp).join("kittycad_user_code");
|
|
println!("Writing to {}", path.to_string_lossy());
|
|
tokio::fs::write(path, details.user_code().secret())
|
|
.await
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
} else {
|
|
app.shell()
|
|
.open(auth_uri.secret(), None)
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
}
|
|
|
|
// Wait for the user to login.
|
|
let token = auth_client
|
|
.exchange_device_access_token(&details)
|
|
.request_async(oauth2::reqwest::async_http_client, tokio::time::sleep, None)
|
|
.await
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?
|
|
.access_token()
|
|
.secret()
|
|
.to_string();
|
|
|
|
Ok(token)
|
|
}
|
|
|
|
///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: &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() {
|
|
DEFAULT_HOST.to_string()
|
|
} else {
|
|
hostname.to_string()
|
|
};
|
|
|
|
// Change the baseURL to the one we want.
|
|
let mut baseurl = host.to_string();
|
|
if !host.starts_with("http://") && !host.starts_with("https://") {
|
|
baseurl = format!("https://{host}");
|
|
if host.starts_with("localhost") {
|
|
baseurl = format!("http://{host}")
|
|
}
|
|
}
|
|
log::debug!("Getting user info...");
|
|
|
|
// use kittycad library to fetch the user info from /user/me
|
|
let mut client = kittycad::Client::new(token);
|
|
|
|
if baseurl != DEFAULT_HOST {
|
|
client.set_base_url(&baseurl);
|
|
}
|
|
|
|
let user_info: kittycad::types::User = client
|
|
.users()
|
|
.get_self()
|
|
.await
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
|
|
Ok(user_info)
|
|
}
|
|
|
|
/// Open the selected path in the system file manager.
|
|
/// 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(app: tauri::AppHandle, path: &str) -> Result<(), InvokeError> {
|
|
// Check if the file exists.
|
|
// If it doesn't, return an error.
|
|
if !Path::new(path).exists() {
|
|
return Err(InvokeError::from_anyhow(anyhow::anyhow!(
|
|
"The file `{}` does not exist",
|
|
path
|
|
)));
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
{
|
|
app.shell()
|
|
.command("explorer")
|
|
.args(["/select,", path]) // The comma after select is not a typo
|
|
.spawn()
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
app.shell()
|
|
.command("open")
|
|
.args(["-R", path])
|
|
.spawn()
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
const SERVICE_NAME: &str = "_machine-api._tcp.local.";
|
|
|
|
async fn find_machine_api() -> Result<Option<String>> {
|
|
println!("Looking for machine API...");
|
|
// Timeout if no response is received after 5 seconds.
|
|
let timeout_duration = std::time::Duration::from_secs(5);
|
|
|
|
let mdns = mdns_sd::ServiceDaemon::new()?;
|
|
|
|
// Browse for a service type.
|
|
let receiver = mdns.browse(SERVICE_NAME)?;
|
|
let resp = tokio::time::timeout(
|
|
timeout_duration,
|
|
tokio::spawn(async move {
|
|
while let Ok(event) = receiver.recv() {
|
|
if let mdns_sd::ServiceEvent::ServiceResolved(info) = event {
|
|
if let Some(addr) = info.get_addresses().iter().next() {
|
|
return Some(format!("{}:{}", addr, info.get_port()));
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
// Shut down.
|
|
mdns.shutdown()?;
|
|
|
|
let Ok(Ok(Some(addr))) = resp else {
|
|
return Ok(None);
|
|
};
|
|
|
|
Ok(Some(addr))
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn get_machine_api_ip() -> Result<Option<String>, InvokeError> {
|
|
let machine_api = find_machine_api().await.map_err(InvokeError::from_anyhow)?;
|
|
|
|
Ok(machine_api)
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn list_machines() -> Result<String, InvokeError> {
|
|
let machine_api = find_machine_api().await.map_err(InvokeError::from_anyhow)?;
|
|
|
|
let Some(machine_api) = machine_api else {
|
|
// Empty array.
|
|
return Ok("[]".to_string());
|
|
};
|
|
|
|
let client = reqwest::Client::new();
|
|
let response = client
|
|
.get(format!("http://{}/machines", machine_api))
|
|
.send()
|
|
.await
|
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
|
|
let text = response.text().await.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
Ok(text)
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
fn open_url_sync(app: &tauri::AppHandle, url: &url::Url) {
|
|
log::debug!("Opening URL: {:?}", url);
|
|
let cloned_url = url.clone();
|
|
let runner: tauri::async_runtime::JoinHandle<Result<ProjectState>> = tauri::async_runtime::spawn(async move {
|
|
let url_str = cloned_url.path().to_string();
|
|
|
|
log::debug!("Opening URL path : {}", url_str);
|
|
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) => {
|
|
log::warn!("Error opening URL:{} {:?}", url, e);
|
|
}
|
|
Ok(Err(e)) => {
|
|
log::warn!("Error opening URL:{} {:?}", url, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
tauri::Builder::default()
|
|
.invoke_handler(tauri::generate_handler![
|
|
get_state,
|
|
set_state,
|
|
get_initial_default_dir,
|
|
initialize_project_directory,
|
|
create_new_project_directory,
|
|
list_projects,
|
|
get_project_info,
|
|
parse_project_route,
|
|
get_user,
|
|
login,
|
|
read_dir_recursive,
|
|
show_in_folder,
|
|
read_app_settings_file,
|
|
write_app_settings_file,
|
|
read_project_settings_file,
|
|
write_project_settings_file,
|
|
rename_project_directory,
|
|
get_machine_api_ip,
|
|
list_machines
|
|
])
|
|
.plugin(tauri_plugin_cli::init())
|
|
.plugin(tauri_plugin_deep_link::init())
|
|
.plugin(tauri_plugin_dialog::init())
|
|
.plugin(tauri_plugin_fs::init())
|
|
.plugin(tauri_plugin_http::init())
|
|
.plugin(
|
|
tauri_plugin_log::Builder::new()
|
|
.targets([
|
|
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout),
|
|
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None }),
|
|
])
|
|
.level(log::LevelFilter::Debug)
|
|
.build(),
|
|
)
|
|
.plugin(tauri_plugin_os::init())
|
|
.plugin(tauri_plugin_persisted_scope::init())
|
|
.plugin(tauri_plugin_process::init())
|
|
.plugin(tauri_plugin_shell::init())
|
|
.setup(|app| {
|
|
// Do update things.
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
app.get_webview("main").unwrap().open_devtools();
|
|
}
|
|
#[cfg(not(debug_assertions))]
|
|
#[cfg(feature = "updater")]
|
|
{
|
|
app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
|
|
}
|
|
|
|
let mut verbose = false;
|
|
let mut source_path: Option<PathBuf> = None;
|
|
match app.cli().matches() {
|
|
// `matches` here is a Struct with { args, subcommand }.
|
|
// `args` is `HashMap<String, ArgData>` where `ArgData` is a struct with { value, occurrences }.
|
|
// `subcommand` is `Option<Box<SubcommandMatches>>` where `SubcommandMatches` is a struct with { name, matches }.
|
|
Ok(matches) => {
|
|
if let Some(verbose_flag) = matches.args.get("verbose") {
|
|
let Some(value) = verbose_flag.value.as_bool() else {
|
|
return Err(
|
|
anyhow::anyhow!("Error parsing CLI arguments: verbose flag is not a boolean").into(),
|
|
);
|
|
};
|
|
verbose = value;
|
|
}
|
|
|
|
// Get the path we are trying to open.
|
|
if let Some(source_arg) = matches.args.get("source") {
|
|
// We don't do an else here because this can be null.
|
|
if let Some(value) = source_arg.value.as_str() {
|
|
log::info!("Got path in cli argument: {}", value);
|
|
source_path = Some(Path::new(value).to_path_buf());
|
|
}
|
|
}
|
|
}
|
|
Err(err) => {
|
|
return Err(anyhow::anyhow!("Error parsing CLI arguments: {:?}", err).into());
|
|
}
|
|
}
|
|
|
|
if verbose {
|
|
log::debug!("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.
|
|
// Run the app as normal.
|
|
app.manage(state::Store::default());
|
|
return Ok(());
|
|
};
|
|
|
|
if !source_path.exists() {
|
|
return Err(anyhow::anyhow!(
|
|
"Error: the path `{}` you are trying to open does not exist",
|
|
source_path.display()
|
|
)
|
|
.into());
|
|
}
|
|
|
|
let runner: tauri::async_runtime::JoinHandle<Result<ProjectState>> =
|
|
tauri::async_runtime::spawn(async move { ProjectState::new_from_path(source_path).await });
|
|
|
|
// Block on the handle.
|
|
let store = tauri::async_runtime::block_on(runner)??;
|
|
|
|
// Create a state object to hold the project.
|
|
app.manage(state::Store::new(store));
|
|
|
|
// Listen on the deep links.
|
|
app.listen("deep-link://new-url", |event| {
|
|
log::info!("got deep-link url: {:?}", event);
|
|
// TODO: open_url_sync(app.handle(), event.url);
|
|
});
|
|
|
|
Ok(())
|
|
})
|
|
.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 {
|
|
log::info!("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(())
|
|
}
|