aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri
diff options
context:
space:
mode:
Diffstat (limited to 'src-tauri')
-rw-r--r--src-tauri/Cargo.toml1
-rw-r--r--src-tauri/src/core/instance.rs325
-rw-r--r--src-tauri/src/core/mod.rs1
-rw-r--r--src-tauri/src/main.rs260
-rw-r--r--src-tauri/src/utils/mod.rs2
-rw-r--r--src-tauri/src/utils/path.rs22
6 files changed, 527 insertions, 84 deletions
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 407da5a..663a5b1 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -30,6 +30,7 @@ serde_urlencoded = "0.7.1"
tauri-plugin-dialog = "2.6.0"
tauri-plugin-fs = "2.4.5"
bytes = "1.11.0"
+chrono = "0.4"
[build-dependencies]
tauri-build = { version = "2.0", features = [] }
diff --git a/src-tauri/src/core/instance.rs b/src-tauri/src/core/instance.rs
new file mode 100644
index 0000000..90ec34e
--- /dev/null
+++ b/src-tauri/src/core/instance.rs
@@ -0,0 +1,325 @@
+//! Instance/Profile management module.
+//!
+//! This module provides functionality to:
+//! - Create and manage multiple isolated game instances
+//! - Each instance has its own versions, libraries, assets, mods, and saves
+//! - Support for instance switching and isolation
+
+use serde::{Deserialize, Serialize};
+use std::fs;
+use std::path::{Path, PathBuf};
+use std::sync::Mutex;
+use tauri::{AppHandle, Manager};
+
+/// Represents a game instance/profile
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Instance {
+ pub id: String, // 唯一标识符(UUID)
+ pub name: String, // 显示名称
+ pub game_dir: PathBuf, // 游戏目录路径
+ pub version_id: Option<String>, // 当前选择的版本ID
+ pub created_at: i64, // 创建时间戳
+ pub last_played: Option<i64>, // 最后游玩时间
+ pub icon_path: Option<String>, // 图标路径(可选)
+ pub notes: Option<String>, // 备注(可选)
+ pub mod_loader: Option<String>, // 模组加载器类型:"fabric", "forge", "vanilla"
+ pub mod_loader_version: Option<String>, // 模组加载器版本
+}
+
+/// Configuration for all instances
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct InstanceConfig {
+ pub instances: Vec<Instance>,
+ pub active_instance_id: Option<String>, // 当前活动的实例ID
+}
+
+/// State management for instances
+pub struct InstanceState {
+ pub instances: Mutex<InstanceConfig>,
+ pub file_path: PathBuf,
+}
+
+impl InstanceState {
+ /// Create a new InstanceState
+ pub fn new(app_handle: &AppHandle) -> Self {
+ let app_dir = app_handle.path().app_data_dir().unwrap();
+ let file_path = app_dir.join("instances.json");
+
+ let config = if file_path.exists() {
+ let content = fs::read_to_string(&file_path).unwrap_or_default();
+ serde_json::from_str(&content).unwrap_or_else(|_| InstanceConfig::default())
+ } else {
+ InstanceConfig::default()
+ };
+
+ Self {
+ instances: Mutex::new(config),
+ file_path,
+ }
+ }
+
+ /// Save the instance configuration to disk
+ pub fn save(&self) -> Result<(), String> {
+ let config = self.instances.lock().unwrap();
+ let content = serde_json::to_string_pretty(&*config).map_err(|e| e.to_string())?;
+ fs::create_dir_all(self.file_path.parent().unwrap()).map_err(|e| e.to_string())?;
+ fs::write(&self.file_path, content).map_err(|e| e.to_string())?;
+ Ok(())
+ }
+
+ /// Create a new instance
+ pub fn create_instance(
+ &self,
+ name: String,
+ app_handle: &AppHandle,
+ ) -> Result<Instance, String> {
+ let app_dir = app_handle.path().app_data_dir().unwrap();
+ let instance_id = uuid::Uuid::new_v4().to_string();
+ let instance_dir = app_dir.join("instances").join(&instance_id);
+ let game_dir = instance_dir.clone();
+
+ // Create instance directory structure
+ fs::create_dir_all(&instance_dir).map_err(|e| e.to_string())?;
+ fs::create_dir_all(instance_dir.join("versions")).map_err(|e| e.to_string())?;
+ fs::create_dir_all(instance_dir.join("libraries")).map_err(|e| e.to_string())?;
+ fs::create_dir_all(instance_dir.join("assets")).map_err(|e| e.to_string())?;
+ fs::create_dir_all(instance_dir.join("mods")).map_err(|e| e.to_string())?;
+ fs::create_dir_all(instance_dir.join("config")).map_err(|e| e.to_string())?;
+ fs::create_dir_all(instance_dir.join("saves")).map_err(|e| e.to_string())?;
+
+ let instance = Instance {
+ id: instance_id.clone(),
+ name,
+ game_dir,
+ version_id: None,
+ created_at: chrono::Utc::now().timestamp(),
+ last_played: None,
+ icon_path: None,
+ notes: None,
+ mod_loader: Some("vanilla".to_string()),
+ mod_loader_version: None,
+ };
+
+ let mut config = self.instances.lock().unwrap();
+ config.instances.push(instance.clone());
+
+ // If this is the first instance, set it as active
+ if config.active_instance_id.is_none() {
+ config.active_instance_id = Some(instance_id);
+ }
+
+ drop(config);
+ self.save()?;
+
+ Ok(instance)
+ }
+
+ /// Delete an instance
+ pub fn delete_instance(&self, id: &str) -> Result<(), String> {
+ let mut config = self.instances.lock().unwrap();
+
+ // Find the instance
+ let instance_index = config
+ .instances
+ .iter()
+ .position(|i| i.id == id)
+ .ok_or_else(|| format!("Instance {} not found", id))?;
+
+ let instance = config.instances[instance_index].clone();
+
+ // Remove from list
+ config.instances.remove(instance_index);
+
+ // If this was the active instance, clear or set another as active
+ if config.active_instance_id.as_ref() == Some(&id.to_string()) {
+ config.active_instance_id = config.instances.first().map(|i| i.id.clone());
+ }
+
+ drop(config);
+ self.save()?;
+
+ // Delete the instance directory
+ if instance.game_dir.exists() {
+ fs::remove_dir_all(&instance.game_dir)
+ .map_err(|e| format!("Failed to delete instance directory: {}", e))?;
+ }
+
+ Ok(())
+ }
+
+ /// Update an instance
+ pub fn update_instance(&self, instance: Instance) -> Result<(), String> {
+ let mut config = self.instances.lock().unwrap();
+
+ let index = config
+ .instances
+ .iter()
+ .position(|i| i.id == instance.id)
+ .ok_or_else(|| format!("Instance {} not found", instance.id))?;
+
+ config.instances[index] = instance;
+ drop(config);
+ self.save()?;
+
+ Ok(())
+ }
+
+ /// Get an instance by ID
+ pub fn get_instance(&self, id: &str) -> Option<Instance> {
+ let config = self.instances.lock().unwrap();
+ config.instances.iter().find(|i| i.id == id).cloned()
+ }
+
+ /// List all instances
+ pub fn list_instances(&self) -> Vec<Instance> {
+ let config = self.instances.lock().unwrap();
+ config.instances.clone()
+ }
+
+ /// Set the active instance
+ pub fn set_active_instance(&self, id: &str) -> Result<(), String> {
+ let mut config = self.instances.lock().unwrap();
+
+ // Verify the instance exists
+ if !config.instances.iter().any(|i| i.id == id) {
+ return Err(format!("Instance {} not found", id));
+ }
+
+ config.active_instance_id = Some(id.to_string());
+ drop(config);
+ self.save()?;
+
+ Ok(())
+ }
+
+ /// Get the active instance
+ pub fn get_active_instance(&self) -> Option<Instance> {
+ let config = self.instances.lock().unwrap();
+ config
+ .active_instance_id
+ .as_ref()
+ .and_then(|id| config.instances.iter().find(|i| i.id == *id))
+ .cloned()
+ }
+
+ /// Get the game directory for an instance
+ pub fn get_instance_game_dir(&self, id: &str) -> Option<PathBuf> {
+ self.get_instance(id).map(|i| i.game_dir)
+ }
+
+ /// Duplicate an instance
+ pub fn duplicate_instance(
+ &self,
+ id: &str,
+ new_name: String,
+ app_handle: &AppHandle,
+ ) -> Result<Instance, String> {
+ let source_instance = self
+ .get_instance(id)
+ .ok_or_else(|| format!("Instance {} not found", id))?;
+
+ // Create new instance
+ let mut new_instance = self.create_instance(new_name, app_handle)?;
+
+ // Copy instance properties
+ new_instance.version_id = source_instance.version_id.clone();
+ new_instance.mod_loader = source_instance.mod_loader.clone();
+ new_instance.mod_loader_version = source_instance.mod_loader_version.clone();
+ new_instance.notes = source_instance.notes.clone();
+
+ // Copy directory contents
+ if source_instance.game_dir.exists() {
+ copy_dir_all(&source_instance.game_dir, &new_instance.game_dir)
+ .map_err(|e| format!("Failed to copy instance directory: {}", e))?;
+ }
+
+ self.update_instance(new_instance.clone())?;
+
+ Ok(new_instance)
+ }
+}
+
+/// Copy a directory recursively
+fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), std::io::Error> {
+ fs::create_dir_all(dst)?;
+ for entry in fs::read_dir(src)? {
+ let entry = entry?;
+ let ty = entry.file_type()?;
+ if ty.is_dir() {
+ copy_dir_all(&entry.path(), &dst.join(entry.file_name()))?;
+ } else {
+ fs::copy(entry.path(), dst.join(entry.file_name()))?;
+ }
+ }
+ Ok(())
+}
+
+/// Migrate legacy data to instance system
+pub fn migrate_legacy_data(
+ app_handle: &AppHandle,
+ instance_state: &InstanceState,
+) -> Result<(), String> {
+ let app_dir = app_handle.path().app_data_dir().unwrap();
+ let old_versions_dir = app_dir.join("versions");
+ let old_libraries_dir = app_dir.join("libraries");
+ let old_assets_dir = app_dir.join("assets");
+
+ // Check if legacy data exists
+ let has_legacy_data =
+ old_versions_dir.exists() || old_libraries_dir.exists() || old_assets_dir.exists();
+
+ if !has_legacy_data {
+ return Ok(()); // No legacy data to migrate
+ }
+
+ // Check if instances already exist
+ let config = instance_state.instances.lock().unwrap();
+ if !config.instances.is_empty() {
+ drop(config);
+ return Ok(()); // Already have instances, skip migration
+ }
+ drop(config);
+
+ // Create default instance
+ let default_instance = instance_state
+ .create_instance("Default".to_string(), app_handle)
+ .map_err(|e| format!("Failed to create default instance: {}", e))?;
+
+ let new_versions_dir = default_instance.game_dir.join("versions");
+ let new_libraries_dir = default_instance.game_dir.join("libraries");
+ let new_assets_dir = default_instance.game_dir.join("assets");
+
+ // Move legacy data
+ if old_versions_dir.exists() {
+ if new_versions_dir.exists() {
+ // Merge directories
+ copy_dir_all(&old_versions_dir, &new_versions_dir)
+ .map_err(|e| format!("Failed to migrate versions: {}", e))?;
+ } else {
+ fs::rename(&old_versions_dir, &new_versions_dir)
+ .map_err(|e| format!("Failed to migrate versions: {}", e))?;
+ }
+ }
+
+ if old_libraries_dir.exists() {
+ if new_libraries_dir.exists() {
+ copy_dir_all(&old_libraries_dir, &new_libraries_dir)
+ .map_err(|e| format!("Failed to migrate libraries: {}", e))?;
+ } else {
+ fs::rename(&old_libraries_dir, &new_libraries_dir)
+ .map_err(|e| format!("Failed to migrate libraries: {}", e))?;
+ }
+ }
+
+ if old_assets_dir.exists() {
+ if new_assets_dir.exists() {
+ copy_dir_all(&old_assets_dir, &new_assets_dir)
+ .map_err(|e| format!("Failed to migrate assets: {}", e))?;
+ } else {
+ fs::rename(&old_assets_dir, &new_assets_dir)
+ .map_err(|e| format!("Failed to migrate assets: {}", e))?;
+ }
+ }
+
+ Ok(())
+}
diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs
index 7ad6ef9..dcbd47a 100644
--- a/src-tauri/src/core/mod.rs
+++ b/src-tauri/src/core/mod.rs
@@ -6,6 +6,7 @@ pub mod downloader;
pub mod fabric;
pub mod forge;
pub mod game_version;
+pub mod instance;
pub mod java;
pub mod manifest;
pub mod maven;
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 661309a..2871b03 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -68,11 +68,16 @@ async fn start_game(
auth_state: State<'_, core::auth::AccountState>,
config_state: State<'_, core::config::ConfigState>,
assistant_state: State<'_, core::assistant::AssistantState>,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
version_id: String,
) -> Result<String, String> {
emit_log!(
window,
- format!("Starting game launch for version: {}", version_id)
+ format!(
+ "Starting game launch for version: {} in instance: {}",
+ version_id, instance_id
+ )
);
// Check for active account
@@ -93,14 +98,10 @@ async fn start_game(
format!("Memory: {}MB - {}MB", config.min_memory, config.max_memory)
);
- // Get App Data Directory (e.g., ~/.local/share/com.dropout.launcher or similar)
- // The identifier is set in tauri.conf.json.
- // If not accessible, use a specific logic.
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+ // Get game directory from instance
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
// Ensure game directory exists
tokio::fs::create_dir_all(&game_dir)
@@ -169,6 +170,7 @@ async fn start_game(
};
// Check if configured Java is compatible
+ let app_handle = window.app_handle();
let mut java_path_to_use = config.java_path.clone();
if !java_path_to_use.is_empty() && java_path_to_use != "java" {
let is_compatible =
@@ -890,12 +892,15 @@ async fn get_versions(window: Window) -> Result<Vec<core::manifest::Version>, St
/// Check if a version is installed (has client.jar)
#[tauri::command]
-async fn check_version_installed(window: Window, version_id: String) -> Result<bool, String> {
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+async fn check_version_installed(
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+ version_id: String,
+) -> Result<bool, String> {
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
// For modded versions, check the parent vanilla version
let minecraft_version = if version_id.starts_with("fabric-loader-") {
@@ -929,19 +934,24 @@ async fn check_version_installed(window: Window, version_id: String) -> Result<b
async fn install_version(
window: Window,
config_state: State<'_, core::config::ConfigState>,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
version_id: String,
) -> Result<(), String> {
emit_log!(
window,
- format!("Starting installation for version: {}", version_id)
+ format!(
+ "Starting installation for version: {} in instance: {}",
+ version_id, instance_id
+ )
);
let config = config_state.config.lock().unwrap().clone();
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+
+ // Get game directory from instance
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
// Ensure game directory exists
tokio::fs::create_dir_all(&game_dir)
@@ -1530,22 +1540,22 @@ async fn get_fabric_loaders_for_version(
#[tauri::command]
async fn install_fabric(
window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
game_version: String,
loader_version: String,
) -> Result<core::fabric::InstalledFabricVersion, String> {
emit_log!(
window,
format!(
- "Installing Fabric {} for Minecraft {}...",
- loader_version, game_version
+ "Installing Fabric {} for Minecraft {} in instance {}...",
+ loader_version, game_version, instance_id
)
);
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
let result = core::fabric::install_fabric(&game_dir, &game_version, &loader_version)
.await
@@ -1564,12 +1574,14 @@ async fn install_fabric(
/// List installed Fabric versions
#[tauri::command]
-async fn list_installed_fabric_versions(window: Window) -> Result<Vec<String>, String> {
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+async fn list_installed_fabric_versions(
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+) -> Result<Vec<String>, String> {
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
core::fabric::list_installed_fabric_versions(&game_dir)
.await
@@ -1579,14 +1591,14 @@ async fn list_installed_fabric_versions(window: Window) -> Result<Vec<String>, S
/// Get Java version requirement for a specific version
#[tauri::command]
async fn get_version_java_version(
- window: Window,
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
version_id: String,
) -> Result<Option<u64>, String> {
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
// Try to load the version JSON to get javaVersion
match core::manifest::load_version(&game_dir, &version_id).await {
@@ -1607,12 +1619,15 @@ struct VersionMetadata {
/// Delete a version (remove version directory)
#[tauri::command]
-async fn delete_version(window: Window, version_id: String) -> Result<(), String> {
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+async fn delete_version(
+ window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+ version_id: String,
+) -> Result<(), String> {
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
let version_dir = game_dir.join("versions").join(&version_id);
@@ -1634,14 +1649,14 @@ async fn delete_version(window: Window, version_id: String) -> Result<(), String
/// Get detailed metadata for a specific version
#[tauri::command]
async fn get_version_metadata(
- window: Window,
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
version_id: String,
) -> Result<VersionMetadata, String> {
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
// Initialize metadata
let mut metadata = VersionMetadata {
@@ -1728,12 +1743,14 @@ struct InstalledVersion {
/// List all installed versions from the data directory
/// Simply lists all folders in the versions directory without validation
#[tauri::command]
-async fn list_installed_versions(window: Window) -> Result<Vec<InstalledVersion>, String> {
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+async fn list_installed_versions(
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+) -> Result<Vec<InstalledVersion>, String> {
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
let versions_dir = game_dir.join("versions");
let mut installed = Vec::new();
@@ -1813,15 +1830,15 @@ async fn list_installed_versions(window: Window) -> Result<Vec<InstalledVersion>
/// Check if Fabric is installed for a specific version
#[tauri::command]
async fn is_fabric_installed(
- window: Window,
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
game_version: String,
loader_version: String,
) -> Result<bool, String> {
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
Ok(core::fabric::is_fabric_installed(
&game_dir,
@@ -1853,25 +1870,26 @@ async fn get_forge_versions_for_game(
async fn install_forge(
window: Window,
config_state: State<'_, core::config::ConfigState>,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
game_version: String,
forge_version: String,
) -> Result<core::forge::InstalledForgeVersion, String> {
emit_log!(
window,
format!(
- "Installing Forge {} for Minecraft {}...",
- forge_version, game_version
+ "Installing Forge {} for Minecraft {} in instance {}...",
+ forge_version, game_version, instance_id
)
);
- let app_handle = window.app_handle();
- let game_dir = app_handle
- .path()
- .app_data_dir()
- .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
// Get Java path from config or detect
let config = config_state.config.lock().unwrap().clone();
+ let app_handle = window.app_handle();
let java_path_str = if !config.java_path.is_empty() && config.java_path != "java" {
config.java_path.clone()
} else {
@@ -2075,6 +2093,85 @@ async fn list_openai_models(
assistant.list_openai_models(&config.assistant).await
}
+// ==================== Instance Management Commands ====================
+
+/// Create a new instance
+#[tauri::command]
+async fn create_instance(
+ window: Window,
+ state: State<'_, core::instance::InstanceState>,
+ name: String,
+) -> Result<core::instance::Instance, String> {
+ let app_handle = window.app_handle();
+ state.create_instance(name, app_handle)
+}
+
+/// Delete an instance
+#[tauri::command]
+async fn delete_instance(
+ state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+) -> Result<(), String> {
+ state.delete_instance(&instance_id)
+}
+
+/// Update an instance
+#[tauri::command]
+async fn update_instance(
+ state: State<'_, core::instance::InstanceState>,
+ instance: core::instance::Instance,
+) -> Result<(), String> {
+ state.update_instance(instance)
+}
+
+/// Get all instances
+#[tauri::command]
+async fn list_instances(
+ state: State<'_, core::instance::InstanceState>,
+) -> Result<Vec<core::instance::Instance>, String> {
+ Ok(state.list_instances())
+}
+
+/// Get a single instance by ID
+#[tauri::command]
+async fn get_instance(
+ state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+) -> Result<core::instance::Instance, String> {
+ state
+ .get_instance(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))
+}
+
+/// Set the active instance
+#[tauri::command]
+async fn set_active_instance(
+ state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+) -> Result<(), String> {
+ state.set_active_instance(&instance_id)
+}
+
+/// Get the active instance
+#[tauri::command]
+async fn get_active_instance(
+ state: State<'_, core::instance::InstanceState>,
+) -> Result<Option<core::instance::Instance>, String> {
+ Ok(state.get_active_instance())
+}
+
+/// Duplicate an instance
+#[tauri::command]
+async fn duplicate_instance(
+ window: Window,
+ state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+ new_name: String,
+) -> Result<core::instance::Instance, String> {
+ let app_handle = window.app_handle();
+ state.duplicate_instance(&instance_id, new_name, app_handle)
+}
+
#[tauri::command]
async fn assistant_chat_stream(
window: tauri::Window,
@@ -2101,6 +2198,16 @@ fn main() {
let config_state = core::config::ConfigState::new(app.handle());
app.manage(config_state);
+ // Initialize instance state
+ let instance_state = core::instance::InstanceState::new(app.handle());
+
+ // Migrate legacy data if needed
+ if let Err(e) = core::instance::migrate_legacy_data(app.handle(), &instance_state) {
+ eprintln!("[Startup] Warning: Failed to migrate legacy data: {}", e);
+ }
+
+ app.manage(instance_state);
+
// Load saved account on startup
let app_dir = app.path().app_data_dir().unwrap();
let storage = core::account_storage::AccountStorage::new(app_dir);
@@ -2176,7 +2283,16 @@ fn main() {
assistant_chat,
assistant_chat_stream,
list_ollama_models,
- list_openai_models
+ list_openai_models,
+ // Instance management commands
+ create_instance,
+ delete_instance,
+ update_instance,
+ list_instances,
+ get_instance,
+ set_active_instance,
+ get_active_instance,
+ duplicate_instance
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs
index 651d26b..c9ac368 100644
--- a/src-tauri/src/utils/mod.rs
+++ b/src-tauri/src/utils/mod.rs
@@ -1,5 +1,5 @@
-pub mod zip;
pub mod path;
+pub mod zip;
// File system related utility functions
#[allow(dead_code)]
diff --git a/src-tauri/src/utils/path.rs b/src-tauri/src/utils/path.rs
index deaebb5..1db6e5b 100644
--- a/src-tauri/src/utils/path.rs
+++ b/src-tauri/src/utils/path.rs
@@ -59,7 +59,7 @@ pub fn normalize_java_path(java_path: &str) -> Result<PathBuf, String> {
}
}
}
-
+
// If still not found after PATH search, return specific error
if !path.exists() {
return Err(
@@ -80,7 +80,7 @@ pub fn normalize_java_path(java_path: &str) -> Result<PathBuf, String> {
// Canonicalize and strip UNC prefix for clean path
let canonical = std::fs::canonicalize(&path)
.map_err(|e| format!("Failed to resolve Java path '{}': {}", path.display(), e))?;
-
+
Ok(strip_unc_prefix(canonical))
}
@@ -98,7 +98,7 @@ pub fn normalize_java_path(java_path: &str) -> Result<PathBuf, String> {
}
}
}
-
+
// If still not found after PATH search, return specific error
if !path.exists() {
return Err(
@@ -119,7 +119,7 @@ pub fn normalize_java_path(java_path: &str) -> Result<PathBuf, String> {
// Canonicalize to resolve symlinks and get absolute path
let canonical = std::fs::canonicalize(&path)
.map_err(|e| format!("Failed to resolve Java path '{}': {}", path.display(), e))?;
-
+
Ok(strip_unc_prefix(canonical))
}
@@ -196,23 +196,23 @@ mod tests {
fn test_normalize_with_temp_file() {
// Create a temporary file to test with an actual existing path
let temp_dir = std::env::temp_dir();
-
+
#[cfg(target_os = "windows")]
let temp_file = temp_dir.join("test_java_normalize.exe");
#[cfg(not(target_os = "windows"))]
let temp_file = temp_dir.join("test_java_normalize");
-
+
// Create the file
if let Ok(mut file) = fs::File::create(&temp_file) {
let _ = file.write_all(b"#!/bin/sh\necho test");
drop(file);
-
+
// Test normalization
let result = normalize_java_path(temp_file.to_str().unwrap());
-
+
// Clean up
let _ = fs::remove_file(&temp_file);
-
+
// Verify result
assert!(result.is_ok(), "Failed to normalize temp file path");
let normalized = result.unwrap();
@@ -227,12 +227,12 @@ mod tests {
let unc_path = PathBuf::from(r"\\?\C:\Windows\System32\cmd.exe");
let stripped = strip_unc_prefix(unc_path);
assert_eq!(stripped.to_string_lossy(), r"C:\Windows\System32\cmd.exe");
-
+
let normal_path = PathBuf::from(r"C:\Windows\System32\cmd.exe");
let unchanged = strip_unc_prefix(normal_path.clone());
assert_eq!(unchanged, normal_path);
}
-
+
#[cfg(not(target_os = "windows"))]
{
let path = PathBuf::from("/usr/bin/java");