Settings move to rust (for read/write from files) (#2220)
* start of settings types Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * add validator Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * start of settings in rust Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix wasm Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix wasm Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * more tests Signed-off-by: Jess Frazelle <github@jessfraz.com> * derive docs Signed-off-by: Jess Frazelle <github@jessfraz.com> * configuration Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * read and write functions with migration Signed-off-by: Jess Frazelle <github@jessfraz.com> * make more dry Signed-off-by: Jess Frazelle <github@jessfraz.com> * more parsing of app settings Signed-off-by: Jess Frazelle <github@jessfraz.com> * more things Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup Signed-off-by: Jess Frazelle <github@jessfraz.com> * trim end Signed-off-by: Jess Frazelle <github@jessfraz.com> * project settings Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup tauri commands Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * refactor Signed-off-by: Jess Frazelle <github@jessfraz.com> * refactor Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * change to files Signed-off-by: Jess Frazelle <github@jessfraz.com> * better Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup more Signed-off-by: Jess Frazelle <github@jessfraz.com> * get rid of dead code Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixed Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup some more shit Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * add validation Signed-off-by: Jess Frazelle <github@jessfraz.com> * validation Signed-off-by: Jess Frazelle <github@jessfraz.com> * validate Signed-off-by: Jess Frazelle <github@jessfraz.com> * validate Signed-off-by: Jess Frazelle <github@jessfraz.com> * clippuy Signed-off-by: Jess Frazelle <github@jessfraz.com> * clippuy Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix; Signed-off-by: Jess Frazelle <github@jessfraz.com> --------- Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
@ -1,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(())
|
||||
}
|
||||
|
Reference in New Issue
Block a user