refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 (#216)

* refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构

* feat(editor): 添加插件市场功能

* feat(editor): 重构插件市场以支持版本管理和ZIP打包

* feat(editor): 重构插件发布流程并修复React渲染警告

* fix(plugin): 修复插件发布和市场的路径不一致问题

* feat: 重构插件发布流程并添加插件删除功能

* fix(editor): 完善插件删除功能并修复多个关键问题

* fix(auth): 修复自动登录与手动登录的竞态条件问题

* feat(editor): 重构插件管理流程

* feat(editor): 支持 ZIP 文件直接发布插件

- 新增 PluginSourceParser 解析插件源
- 重构发布流程支持文件夹和 ZIP 两种方式
- 优化发布向导 UI

* feat(editor): 插件市场支持多版本安装

- 插件解压到项目 plugins 目录
- 新增 Tauri 后端安装/卸载命令
- 支持选择任意版本安装
- 修复打包逻辑,保留完整 dist 目录结构

* feat(editor): 个人中心支持多版本管理

- 合并同一插件的不同版本
- 添加版本历史展开/折叠功能
- 禁止有待审核 PR 时更新插件

* fix(editor): 修复 InspectorRegistry 服务注册

- InspectorRegistry 实现 IService 接口
- 注册到 Core.services 供插件使用

* feat(behavior-tree-editor): 完善插件注册和文件操作

- 添加文件创建模板和操作处理器
- 实现右键菜单创建行为树功能
- 修复文件读取权限问题(使用 Tauri 命令)
- 添加 BehaviorTreeEditorPanel 组件
- 修复 rollup 配置支持动态导入

* feat(plugin): 完善插件构建和发布流程

* fix(behavior-tree-editor): 完整恢复编辑器并修复 Toast 集成

* fix(behavior-tree-editor): 修复节点选中、连线跟随和文件加载问题并优化性能

* fix(behavior-tree-editor): 修复端口连接失败问题并优化连线样式

* refactor(behavior-tree-editor): 移除调试面板功能简化代码结构

* refactor(behavior-tree-editor): 清理冗余代码合并重复逻辑

* feat(behavior-tree-editor): 完善编辑器核心功能增强扩展性

* fix(lint): 修复ESLint错误确保CI通过

* refactor(behavior-tree-editor): 优化编辑器工具栏和编译器功能

* refactor(behavior-tree-editor): 清理技术债务,优化代码质量

* fix(editor-app): 修复字符串替换安全问题
This commit is contained in:
YHH
2025-11-18 14:46:51 +08:00
committed by GitHub
parent eac660b1a0
commit bce3a6e253
251 changed files with 26144 additions and 8844 deletions

View File

@@ -1,128 +0,0 @@
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::profiler_ws::ProfilerServer;
#[derive(Debug, Serialize, Deserialize)]
pub struct ProjectInfo {
pub name: String,
pub path: String,
pub version: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct EditorConfig {
pub theme: String,
pub auto_save: bool,
pub recent_projects: Vec<String>,
}
impl Default for EditorConfig {
fn default() -> Self {
Self {
theme: "dark".to_string(),
auto_save: true,
recent_projects: Vec::new(),
}
}
}
pub struct ProfilerState {
pub server: Arc<Mutex<Option<Arc<ProfilerServer>>>>,
}
#[tauri::command]
pub async fn start_profiler_server(
port: u16,
state: tauri::State<'_, ProfilerState>,
) -> Result<String, String> {
let mut server_lock = state.server.lock().await;
if server_lock.is_some() {
return Err("Profiler server is already running".to_string());
}
let server = Arc::new(ProfilerServer::new(port));
match server.start().await {
Ok(_) => {
*server_lock = Some(server);
Ok(format!("Profiler server started on port {}", port))
}
Err(e) => Err(format!("Failed to start profiler server: {}", e)),
}
}
#[tauri::command]
pub async fn stop_profiler_server(
state: tauri::State<'_, ProfilerState>,
) -> Result<String, String> {
let mut server_lock = state.server.lock().await;
if server_lock.is_none() {
return Err("Profiler server is not running".to_string());
}
*server_lock = None;
Ok("Profiler server stopped".to_string())
}
#[tauri::command]
pub async fn get_profiler_status(
state: tauri::State<'_, ProfilerState>,
) -> Result<bool, String> {
let server_lock = state.server.lock().await;
Ok(server_lock.is_some())
}
#[tauri::command]
pub async fn read_behavior_tree_file(file_path: String) -> Result<String, String> {
use std::fs;
// 使用 Rust 标准库直接读取文件,绕过 Tauri 的 scope 限制
fs::read_to_string(&file_path)
.map_err(|e| format!("Failed to read file {}: {}", file_path, e))
}
#[tauri::command]
pub async fn write_behavior_tree_file(file_path: String, content: String) -> Result<(), String> {
use std::fs;
// 使用 Rust 标准库直接写入文件
fs::write(&file_path, content)
.map_err(|e| format!("Failed to write file {}: {}", file_path, e))
}
#[tauri::command]
pub async fn read_global_blackboard(project_path: String) -> Result<String, String> {
use std::fs;
use std::path::Path;
let config_path = Path::new(&project_path).join(".ecs").join("global-blackboard.json");
if !config_path.exists() {
return Ok(String::from(r#"{"version":"1.0","variables":[]}"#));
}
fs::read_to_string(&config_path)
.map_err(|e| format!("Failed to read global blackboard: {}", e))
}
#[tauri::command]
pub async fn write_global_blackboard(project_path: String, content: String) -> Result<(), String> {
use std::fs;
use std::path::Path;
let ecs_dir = Path::new(&project_path).join(".ecs");
let config_path = ecs_dir.join("global-blackboard.json");
// 创建 .ecs 目录(如果不存在)
if !ecs_dir.exists() {
fs::create_dir_all(&ecs_dir)
.map_err(|e| format!("Failed to create .ecs directory: {}", e))?;
}
fs::write(&config_path, content)
.map_err(|e| format!("Failed to write global blackboard: {}", e))
}

View File

@@ -0,0 +1,97 @@
//! Dialog operations
//!
//! Generic system dialog commands for file/folder selection.
//! No business-specific logic - all filtering is done via parameters.
use tauri::AppHandle;
use tauri_plugin_dialog::DialogExt;
/// File filter definition
#[derive(serde::Deserialize)]
pub struct FileFilter {
pub name: String,
pub extensions: Vec<String>,
}
/// Open folder selection dialog
#[tauri::command]
pub async fn open_folder_dialog(
app: AppHandle,
title: Option<String>,
) -> Result<Option<String>, String> {
let mut dialog = app.dialog().file();
if let Some(t) = title {
dialog = dialog.set_title(&t);
} else {
dialog = dialog.set_title("Select Folder");
}
let folder = dialog.blocking_pick_folder();
Ok(folder.map(|path| path.to_string()))
}
/// Open file selection dialog (generic)
#[tauri::command]
pub async fn open_file_dialog(
app: AppHandle,
title: Option<String>,
filters: Option<Vec<FileFilter>>,
multiple: Option<bool>,
) -> Result<Option<Vec<String>>, String> {
let mut dialog = app.dialog().file();
if let Some(t) = title {
dialog = dialog.set_title(&t);
} else {
dialog = dialog.set_title("Select File");
}
if let Some(filter_list) = filters {
for filter in filter_list {
let extensions: Vec<&str> = filter.extensions.iter().map(|s| s.as_str()).collect();
dialog = dialog.add_filter(&filter.name, &extensions);
}
}
if multiple.unwrap_or(false) {
let files = dialog.blocking_pick_files();
Ok(files.map(|paths| paths.iter().map(|p| p.to_string()).collect()))
} else {
let file = dialog.blocking_pick_file();
Ok(file.map(|path| vec![path.to_string()]))
}
}
/// Save file dialog (generic)
#[tauri::command]
pub async fn save_file_dialog(
app: AppHandle,
title: Option<String>,
default_name: Option<String>,
filters: Option<Vec<FileFilter>>,
) -> Result<Option<String>, String> {
let mut dialog = app.dialog().file();
if let Some(t) = title {
dialog = dialog.set_title(&t);
} else {
dialog = dialog.set_title("Save File");
}
if let Some(name) = default_name {
dialog = dialog.set_file_name(&name);
}
if let Some(filter_list) = filters {
for filter in filter_list {
let extensions: Vec<&str> = filter.extensions.iter().map(|s| s.as_str()).collect();
dialog = dialog.add_filter(&filter.name, &extensions);
}
}
let file = dialog.blocking_save_file();
Ok(file.map(|path| path.to_string()))
}

View File

@@ -0,0 +1,205 @@
//! Generic file system operations
//!
//! Provides low-level file system commands that can be composed by the frontend
//! for business logic. No business-specific logic should be in this module.
use std::fs;
use std::path::Path;
/// Directory entry information
#[derive(serde::Serialize)]
pub struct DirectoryEntry {
pub name: String,
pub path: String,
pub is_dir: bool,
pub size: Option<u64>,
pub modified: Option<u64>,
}
/// Read text file content
#[tauri::command]
pub fn read_file_content(path: String) -> Result<String, String> {
fs::read_to_string(&path)
.map_err(|e| format!("Failed to read file {}: {}", path, e))
}
/// Write text content to file (auto-creates parent directories)
#[tauri::command]
pub fn write_file_content(path: String, content: String) -> Result<(), String> {
// Ensure parent directory exists
if let Some(parent) = Path::new(&path).parent() {
if !parent.exists() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create directory {}: {}", parent.display(), e))?;
}
}
fs::write(&path, content)
.map_err(|e| format!("Failed to write file {}: {}", path, e))
}
/// Write binary content to file (auto-creates parent directories)
#[tauri::command]
pub async fn write_binary_file(file_path: String, content: Vec<u8>) -> Result<(), String> {
// Ensure parent directory exists
if let Some(parent) = Path::new(&file_path).parent() {
if !parent.exists() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create directory {}: {}", parent.display(), e))?;
}
}
fs::write(&file_path, content)
.map_err(|e| format!("Failed to write binary file {}: {}", file_path, e))
}
/// Check if path exists
#[tauri::command]
pub fn path_exists(path: String) -> Result<bool, String> {
Ok(Path::new(&path).exists())
}
/// Create directory (recursive)
#[tauri::command]
pub fn create_directory(path: String) -> Result<(), String> {
fs::create_dir_all(&path)
.map_err(|e| format!("Failed to create directory {}: {}", path, e))
}
/// Create empty file
#[tauri::command]
pub fn create_file(path: String) -> Result<(), String> {
fs::File::create(&path)
.map_err(|e| format!("Failed to create file {}: {}", path, e))?;
Ok(())
}
/// Delete file
#[tauri::command]
pub fn delete_file(path: String) -> Result<(), String> {
fs::remove_file(&path)
.map_err(|e| format!("Failed to delete file {}: {}", path, e))
}
/// Delete directory (recursive)
#[tauri::command]
pub fn delete_folder(path: String) -> Result<(), String> {
fs::remove_dir_all(&path)
.map_err(|e| format!("Failed to delete folder {}: {}", path, e))
}
/// Rename or move file/folder
#[tauri::command]
pub fn rename_file_or_folder(old_path: String, new_path: String) -> Result<(), String> {
fs::rename(&old_path, &new_path)
.map_err(|e| format!("Failed to rename {} to {}: {}", old_path, new_path, e))
}
/// List directory contents with metadata
#[tauri::command]
pub fn list_directory(path: String) -> Result<Vec<DirectoryEntry>, String> {
let dir_path = Path::new(&path);
if !dir_path.exists() {
return Err(format!("Directory does not exist: {}", path));
}
if !dir_path.is_dir() {
return Err(format!("Path is not a directory: {}", path));
}
let mut entries = Vec::new();
let read_dir = fs::read_dir(dir_path)
.map_err(|e| format!("Failed to read directory {}: {}", path, e))?;
for entry in read_dir.flatten() {
let entry_path = entry.path();
if let Some(name) = entry_path.file_name() {
let is_dir = entry_path.is_dir();
let (size, modified) = fs::metadata(&entry_path)
.map(|metadata| {
let size = if is_dir { None } else { Some(metadata.len()) };
let modified = metadata
.modified()
.ok()
.and_then(|time| {
time.duration_since(std::time::UNIX_EPOCH)
.ok()
.map(|d| d.as_secs())
});
(size, modified)
})
.unwrap_or((None, None));
entries.push(DirectoryEntry {
name: name.to_string_lossy().to_string(),
path: entry_path.to_string_lossy().to_string(),
is_dir,
size,
modified,
});
}
}
// Sort: directories first, then alphabetically
entries.sort_by(|a, b| match (a.is_dir, b.is_dir) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
});
Ok(entries)
}
/// Scan directory for files matching a glob pattern
#[tauri::command]
pub fn scan_directory(path: String, pattern: String) -> Result<Vec<String>, String> {
use glob::glob;
let base_path = Path::new(&path);
if !base_path.exists() {
return Err(format!("Directory does not exist: {}", path));
}
let separator = if path.contains('\\') { '\\' } else { '/' };
let glob_pattern = format!(
"{}{}{}",
path.trim_end_matches(&['/', '\\'][..]),
separator,
pattern
);
let normalized_pattern = if cfg!(windows) {
glob_pattern.replace('/', "\\")
} else {
glob_pattern.replace('\\', "/")
};
let mut files = Vec::new();
match glob(&normalized_pattern) {
Ok(entries) => {
for entry in entries.flatten() {
if entry.is_file() {
files.push(entry.to_string_lossy().to_string());
}
}
}
Err(e) => return Err(format!("Failed to scan directory: {}", e)),
}
Ok(files)
}
/// Read file as base64 encoded string
#[tauri::command]
pub fn read_file_as_base64(file_path: String) -> Result<String, String> {
use base64::{engine::general_purpose, Engine as _};
let file_content = fs::read(&file_path)
.map_err(|e| format!("Failed to read file {}: {}", file_path, e))?;
Ok(general_purpose::STANDARD.encode(&file_content))
}

View File

@@ -0,0 +1,18 @@
//! Command modules
//!
//! All Tauri commands organized by domain.
pub mod dialog;
pub mod file_system;
pub mod plugin;
pub mod profiler;
pub mod project;
pub mod system;
// Re-export all commands for convenience
pub use dialog::*;
pub use file_system::*;
pub use plugin::*;
pub use profiler::*;
pub use project::*;
pub use system::*;

View File

@@ -0,0 +1,270 @@
//! Plugin management commands
//!
//! Building, installing, and uninstalling editor plugins.
use std::fs;
use std::io::{Cursor, Write};
use std::path::Path;
use std::process::Command;
use tauri::{AppHandle, Emitter};
use zip::write::FileOptions;
use zip::ZipArchive;
/// Build progress event payload
#[derive(serde::Serialize, Clone)]
pub struct BuildProgress {
pub step: String,
pub output: Option<String>,
}
/// Build a plugin from source
#[tauri::command]
pub async fn build_plugin(plugin_folder: String, app: AppHandle) -> Result<String, String> {
let plugin_path = Path::new(&plugin_folder);
if !plugin_path.exists() {
return Err(format!("Plugin folder does not exist: {}", plugin_folder));
}
let package_json_path = plugin_path.join("package.json");
if !package_json_path.exists() {
return Err("package.json not found in plugin folder".to_string());
}
let build_cache_dir = plugin_path.join(".build-cache");
if !build_cache_dir.exists() {
fs::create_dir_all(&build_cache_dir)
.map_err(|e| format!("Failed to create .build-cache directory: {}", e))?;
}
let npm_command = if cfg!(target_os = "windows") {
"npm.cmd"
} else {
"npm"
};
// Step 1: Install dependencies
app.emit(
"plugin-build-progress",
BuildProgress {
step: "install".to_string(),
output: None,
},
)
.ok();
let install_output = Command::new(npm_command)
.args(["install"])
.current_dir(&plugin_folder)
.output()
.map_err(|e| format!("Failed to run npm install: {}", e))?;
if !install_output.status.success() {
return Err(format!(
"npm install failed: {}",
String::from_utf8_lossy(&install_output.stderr)
));
}
// Step 2: Build
app.emit(
"plugin-build-progress",
BuildProgress {
step: "build".to_string(),
output: None,
},
)
.ok();
let build_output = Command::new(npm_command)
.args(["run", "build"])
.current_dir(&plugin_folder)
.output()
.map_err(|e| format!("Failed to run npm run build: {}", e))?;
if !build_output.status.success() {
return Err(format!(
"npm run build failed: {}",
String::from_utf8_lossy(&build_output.stderr)
));
}
let dist_path = plugin_path.join("dist");
if !dist_path.exists() {
return Err("dist directory not found after build".to_string());
}
// Step 3: Package
app.emit(
"plugin-build-progress",
BuildProgress {
step: "package".to_string(),
output: None,
},
)
.ok();
let zip_path = build_cache_dir.join("index.zip");
let zip_file =
fs::File::create(&zip_path).map_err(|e| format!("Failed to create zip file: {}", e))?;
let mut zip = zip::ZipWriter::new(zip_file);
let options = FileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.unix_permissions(0o755);
// Add package.json
let package_json_content = fs::read(&package_json_path)
.map_err(|e| format!("Failed to read package.json: {}", e))?;
zip.start_file("package.json", options)
.map_err(|e| format!("Failed to add package.json to zip: {}", e))?;
zip.write_all(&package_json_content)
.map_err(|e| format!("Failed to write package.json to zip: {}", e))?;
// Add dist directory
add_directory_to_zip(&mut zip, plugin_path, &dist_path, options)
.map_err(|e| format!("Failed to add dist directory to zip: {}", e))?;
zip.finish()
.map_err(|e| format!("Failed to finalize zip: {}", e))?;
// Step 4: Complete
app.emit(
"plugin-build-progress",
BuildProgress {
step: "complete".to_string(),
output: None,
},
)
.ok();
Ok(zip_path.to_string_lossy().to_string())
}
fn add_directory_to_zip<W: std::io::Write + std::io::Seek>(
zip: &mut zip::ZipWriter<W>,
base_path: &Path,
current_path: &Path,
options: FileOptions,
) -> Result<(), String> {
let entries = fs::read_dir(current_path)
.map_err(|e| format!("Failed to read directory {}: {}", current_path.display(), e))?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
add_directory_to_zip(zip, base_path, &path, options)?;
} else {
let relative_path = path
.strip_prefix(base_path)
.map_err(|e| format!("Failed to get relative path: {}", e))?;
let zip_path = relative_path.to_string_lossy().replace('\\', "/");
let file_content = fs::read(&path)
.map_err(|e| format!("Failed to read file {}: {}", path.display(), e))?;
zip.start_file(&zip_path, options)
.map_err(|e| format!("Failed to add file {} to zip: {}", zip_path, e))?;
zip.write_all(&file_content)
.map_err(|e| format!("Failed to write file {} to zip: {}", zip_path, e))?;
}
}
Ok(())
}
/// Install a plugin from marketplace
#[tauri::command]
pub async fn install_marketplace_plugin(
project_path: String,
plugin_id: String,
zip_data_base64: String,
) -> Result<String, String> {
use base64::{engine::general_purpose, Engine as _};
let project_path = Path::new(&project_path);
if !project_path.exists() {
return Err(format!(
"Project path does not exist: {}",
project_path.display()
));
}
let plugins_dir = project_path.join("plugins");
if !plugins_dir.exists() {
fs::create_dir_all(&plugins_dir)
.map_err(|e| format!("Failed to create plugins directory: {}", e))?;
}
let plugin_dir = plugins_dir.join(&plugin_id);
if plugin_dir.exists() {
fs::remove_dir_all(&plugin_dir)
.map_err(|e| format!("Failed to remove old plugin directory: {}", e))?;
}
fs::create_dir_all(&plugin_dir)
.map_err(|e| format!("Failed to create plugin directory: {}", e))?;
let zip_bytes = general_purpose::STANDARD
.decode(&zip_data_base64)
.map_err(|e| format!("Failed to decode base64 ZIP data: {}", e))?;
let cursor = Cursor::new(zip_bytes);
let mut archive =
ZipArchive::new(cursor).map_err(|e| format!("Failed to read ZIP archive: {}", e))?;
for i in 0..archive.len() {
let mut file = archive
.by_index(i)
.map_err(|e| format!("Failed to read ZIP entry {}: {}", i, e))?;
let file_path = match file.enclosed_name() {
Some(path) => path,
None => continue,
};
let out_path = plugin_dir.join(file_path);
if file.is_dir() {
fs::create_dir_all(&out_path)
.map_err(|e| format!("Failed to create directory {}: {}", out_path.display(), e))?;
} else {
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create parent directory: {}", e))?;
}
let mut out_file = fs::File::create(&out_path)
.map_err(|e| format!("Failed to create file {}: {}", out_path.display(), e))?;
std::io::copy(&mut file, &mut out_file)
.map_err(|e| format!("Failed to write file {}: {}", out_path.display(), e))?;
}
}
Ok(plugin_dir.to_string_lossy().to_string())
}
/// Uninstall a plugin
#[tauri::command]
pub async fn uninstall_marketplace_plugin(
project_path: String,
plugin_id: String,
) -> Result<(), String> {
let project_path = Path::new(&project_path);
let plugin_dir = project_path.join("plugins").join(&plugin_id);
if !plugin_dir.exists() {
return Err(format!(
"Plugin directory does not exist: {}",
plugin_dir.display()
));
}
fs::remove_dir_all(&plugin_dir)
.map_err(|e| format!("Failed to remove plugin directory: {}", e))?;
Ok(())
}

View File

@@ -0,0 +1,58 @@
//! Profiler server commands
//!
//! WebSocket profiler server management.
use std::sync::Arc;
use crate::profiler_ws::ProfilerServer;
use crate::state::ProfilerState;
/// Start the profiler WebSocket server
#[tauri::command]
pub async fn start_profiler_server(
port: u16,
state: tauri::State<'_, ProfilerState>,
) -> Result<String, String> {
let mut server_lock = state.server.lock().await;
if server_lock.is_some() {
return Err("Profiler server is already running".to_string());
}
let server = Arc::new(ProfilerServer::new(port));
match server.start().await {
Ok(_) => {
*server_lock = Some(server);
Ok(format!("Profiler server started on port {}", port))
}
Err(e) => Err(format!("Failed to start profiler server: {}", e)),
}
}
/// Stop the profiler WebSocket server
#[tauri::command]
pub async fn stop_profiler_server(
state: tauri::State<'_, ProfilerState>,
) -> Result<String, String> {
let mut server_lock = state.server.lock().await;
if server_lock.is_none() {
return Err("Profiler server is not running".to_string());
}
if let Some(server) = server_lock.as_ref() {
server.stop().await;
}
*server_lock = None;
Ok("Profiler server stopped".to_string())
}
/// Get profiler server status
#[tauri::command]
pub async fn get_profiler_status(
state: tauri::State<'_, ProfilerState>,
) -> Result<bool, String> {
let server_lock = state.server.lock().await;
Ok(server_lock.is_some())
}

View File

@@ -0,0 +1,77 @@
//! Project management commands
use std::fs;
use std::path::Path;
use crate::state::ProjectPaths;
#[tauri::command]
pub fn open_project(path: String) -> Result<String, String> {
Ok(format!("Project opened: {}", path))
}
/// Save project data
#[tauri::command]
pub fn save_project(path: String, data: String) -> Result<(), String> {
fs::write(&path, data).map_err(|e| format!("Failed to save project: {}", e))
}
/// Export binary data
#[tauri::command]
pub fn export_binary(data: Vec<u8>, output_path: String) -> Result<(), String> {
fs::write(&output_path, data).map_err(|e| format!("Failed to export binary: {}", e))
}
/// Set current project base path
#[tauri::command]
pub fn set_project_base_path(path: String, state: tauri::State<ProjectPaths>) -> Result<(), String> {
let mut paths = state
.lock()
.map_err(|e| format!("Failed to lock state: {}", e))?;
paths.insert("current".to_string(), path);
Ok(())
}
/// Scan for behavior tree files in project
#[tauri::command]
pub fn scan_behavior_trees(project_path: String) -> Result<Vec<String>, String> {
let behaviors_path = Path::new(&project_path).join(".ecs").join("behaviors");
if !behaviors_path.exists() {
fs::create_dir_all(&behaviors_path)
.map_err(|e| format!("Failed to create behaviors directory: {}", e))?;
return Ok(Vec::new());
}
let mut btree_files = Vec::new();
scan_directory_recursive(&behaviors_path, &behaviors_path, &mut btree_files)?;
Ok(btree_files)
}
fn scan_directory_recursive(
base_path: &Path,
current_path: &Path,
results: &mut Vec<String>,
) -> Result<(), String> {
let entries =
fs::read_dir(current_path).map_err(|e| format!("Failed to read directory: {}", e))?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
scan_directory_recursive(base_path, &path, results)?;
} else if path.extension().and_then(|s| s.to_str()) == Some("btree") {
if let Ok(relative) = path.strip_prefix(base_path) {
let relative_str = relative
.to_string_lossy()
.replace('\\', "/")
.trim_end_matches(".btree")
.to_string();
results.push(relative_str);
}
}
}
Ok(())
}

View File

@@ -0,0 +1,96 @@
//! System operations
//!
//! OS-level operations like opening files, showing in folder, devtools, etc.
use std::process::Command;
use tauri::{AppHandle, Manager};
/// Toggle developer tools (debug mode only)
#[tauri::command]
pub fn toggle_devtools(app: AppHandle) -> Result<(), String> {
#[cfg(debug_assertions)]
{
if let Some(window) = app.get_webview_window("main") {
if window.is_devtools_open() {
window.close_devtools();
} else {
window.open_devtools();
}
Ok(())
} else {
Err("Window not found".to_string())
}
}
#[cfg(not(debug_assertions))]
{
let _ = app;
Err("DevTools are only available in debug mode".to_string())
}
}
/// Open file with system default application
#[tauri::command]
pub fn open_file_with_default_app(file_path: String) -> Result<(), String> {
#[cfg(target_os = "windows")]
{
Command::new("cmd")
.args(["/C", "start", "", &file_path])
.spawn()
.map_err(|e| format!("Failed to open file: {}", e))?;
}
#[cfg(target_os = "macos")]
{
Command::new("open")
.arg(&file_path)
.spawn()
.map_err(|e| format!("Failed to open file: {}", e))?;
}
#[cfg(target_os = "linux")]
{
Command::new("xdg-open")
.arg(&file_path)
.spawn()
.map_err(|e| format!("Failed to open file: {}", e))?;
}
Ok(())
}
/// Show file in system file explorer
#[tauri::command]
pub fn show_in_folder(file_path: String) -> Result<(), String> {
#[cfg(target_os = "windows")]
{
Command::new("explorer")
.args(["/select,", &file_path])
.spawn()
.map_err(|e| format!("Failed to show in folder: {}", e))?;
}
#[cfg(target_os = "macos")]
{
Command::new("open")
.args(["-R", &file_path])
.spawn()
.map_err(|e| format!("Failed to show in folder: {}", e))?;
}
#[cfg(target_os = "linux")]
{
use std::path::Path;
let path = Path::new(&file_path);
let parent = path
.parent()
.ok_or_else(|| "Failed to get parent directory".to_string())?;
Command::new("xdg-open")
.arg(parent)
.spawn()
.map_err(|e| format!("Failed to show in folder: {}", e))?;
}
Ok(())
}

View File

@@ -1,9 +1,10 @@
// ECS Editor Library
//! ECS Editor Library
//!
//! Exports all public modules for the Tauri application.
pub mod commands;
pub mod project;
pub mod profiler_ws;
pub mod state;
pub use commands::*;
pub use project::*;
pub use profiler_ws::*;
// Re-export commonly used types
pub use state::{ProfilerState, ProjectPaths};

View File

@@ -1,631 +1,153 @@
// Prevents additional console window on Windows in release
//! ECS Framework Editor - Tauri Backend
//!
//! Clean entry point that handles application setup and command registration.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use tauri::Manager;
use tauri::AppHandle;
use std::sync::{Arc, Mutex};
mod commands;
mod profiler_ws;
mod state;
use std::collections::HashMap;
use ecs_editor_lib::profiler_ws::ProfilerServer;
use std::sync::{Arc, Mutex};
use tauri::Manager;
// IPC Commands
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! Welcome to ECS Framework Editor.", name)
}
#[tauri::command]
fn open_project(path: String) -> Result<String, String> {
// 项目打开逻辑
Ok(format!("Project opened: {}", path))
}
#[tauri::command]
fn save_project(path: String, data: String) -> Result<(), String> {
// 项目保存逻辑
std::fs::write(&path, data)
.map_err(|e| format!("Failed to save project: {}", e))?;
Ok(())
}
#[tauri::command]
fn export_binary(data: Vec<u8>, output_path: String) -> Result<(), String> {
std::fs::write(&output_path, data)
.map_err(|e| format!("Failed to export binary: {}", e))?;
Ok(())
}
#[tauri::command]
fn create_directory(path: String) -> Result<(), String> {
std::fs::create_dir_all(&path)
.map_err(|e| format!("Failed to create directory: {}", e))?;
Ok(())
}
#[tauri::command]
fn write_file_content(path: String, content: String) -> Result<(), String> {
std::fs::write(&path, content)
.map_err(|e| format!("Failed to write file: {}", e))?;
Ok(())
}
#[tauri::command]
fn path_exists(path: String) -> Result<bool, String> {
use std::path::Path;
Ok(Path::new(&path).exists())
}
#[tauri::command]
fn rename_file_or_folder(old_path: String, new_path: String) -> Result<(), String> {
std::fs::rename(&old_path, &new_path)
.map_err(|e| format!("Failed to rename: {}", e))?;
Ok(())
}
#[tauri::command]
fn delete_file(path: String) -> Result<(), String> {
std::fs::remove_file(&path)
.map_err(|e| format!("Failed to delete file: {}", e))?;
Ok(())
}
#[tauri::command]
fn delete_folder(path: String) -> Result<(), String> {
std::fs::remove_dir_all(&path)
.map_err(|e| format!("Failed to delete folder: {}", e))?;
Ok(())
}
#[tauri::command]
fn create_file(path: String) -> Result<(), String> {
use std::fs::File;
File::create(&path)
.map_err(|e| format!("Failed to create file: {}", e))?;
Ok(())
}
#[tauri::command]
async fn open_project_dialog(app: AppHandle) -> Result<Option<String>, String> {
use tauri_plugin_dialog::DialogExt;
let folder = app.dialog()
.file()
.set_title("Select Project Directory")
.blocking_pick_folder();
Ok(folder.map(|path| path.to_string()))
}
#[tauri::command]
async fn save_scene_dialog(app: AppHandle, default_name: Option<String>) -> Result<Option<String>, String> {
use tauri_plugin_dialog::DialogExt;
let mut dialog = app.dialog()
.file()
.set_title("Save ECS Scene")
.add_filter("ECS Scene Files", &["ecs"]);
if let Some(name) = default_name {
dialog = dialog.set_file_name(&name);
}
let file = dialog.blocking_save_file();
Ok(file.map(|path| path.to_string()))
}
#[tauri::command]
async fn open_scene_dialog(app: AppHandle) -> Result<Option<String>, String> {
use tauri_plugin_dialog::DialogExt;
let file = app.dialog()
.file()
.set_title("Open ECS Scene")
.add_filter("ECS Scene Files", &["ecs"])
.blocking_pick_file();
Ok(file.map(|path| path.to_string()))
}
#[tauri::command]
async fn open_behavior_tree_dialog(app: AppHandle) -> Result<Option<String>, String> {
use tauri_plugin_dialog::DialogExt;
let file = app.dialog()
.file()
.set_title("Select Behavior Tree")
.add_filter("Behavior Tree Files", &["btree"])
.blocking_pick_file();
Ok(file.map(|path| path.to_string()))
}
#[tauri::command]
fn scan_behavior_trees(project_path: String) -> Result<Vec<String>, String> {
use std::path::Path;
use std::fs;
let behaviors_path = Path::new(&project_path).join(".ecs").join("behaviors");
if !behaviors_path.exists() {
fs::create_dir_all(&behaviors_path)
.map_err(|e| format!("Failed to create behaviors directory: {}", e))?;
return Ok(Vec::new());
}
let mut btree_files = Vec::new();
scan_directory_recursive(&behaviors_path, &behaviors_path, &mut btree_files)?;
Ok(btree_files)
}
fn scan_directory_recursive(
base_path: &std::path::Path,
current_path: &std::path::Path,
results: &mut Vec<String>
) -> Result<(), String> {
use std::fs;
let entries = fs::read_dir(current_path)
.map_err(|e| format!("Failed to read directory: {}", e))?;
for entry in entries {
let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
let path = entry.path();
if path.is_dir() {
scan_directory_recursive(base_path, &path, results)?;
} else if path.extension().and_then(|s| s.to_str()) == Some("btree") {
if let Ok(relative) = path.strip_prefix(base_path) {
let relative_str = relative.to_string_lossy()
.replace('\\', "/")
.trim_end_matches(".btree")
.to_string();
results.push(relative_str);
}
}
}
Ok(())
}
#[tauri::command]
fn scan_directory(path: String, pattern: String) -> Result<Vec<String>, String> {
use glob::glob;
use std::path::Path;
let base_path = Path::new(&path);
if !base_path.exists() {
return Err(format!("Directory does not exist: {}", path));
}
let separator = if path.contains('\\') { '\\' } else { '/' };
let glob_pattern = format!("{}{}{}", path.trim_end_matches(&['/', '\\'][..]), separator, pattern);
let normalized_pattern = if cfg!(windows) {
glob_pattern.replace('/', "\\")
} else {
glob_pattern.replace('\\', "/")
};
let mut files = Vec::new();
match glob(&normalized_pattern) {
Ok(entries) => {
for entry in entries {
match entry {
Ok(path) => {
if path.is_file() {
files.push(path.to_string_lossy().to_string());
}
}
Err(e) => eprintln!("Error reading entry: {}", e),
}
}
}
Err(e) => return Err(format!("Failed to scan directory: {}", e)),
}
Ok(files)
}
#[tauri::command]
fn read_file_content(path: String) -> Result<String, String> {
std::fs::read_to_string(&path)
.map_err(|e| format!("Failed to read file {}: {}", path, e))
}
#[derive(serde::Serialize)]
struct DirectoryEntry {
name: String,
path: String,
is_dir: bool,
size: Option<u64>,
modified: Option<u64>,
}
#[tauri::command]
fn list_directory(path: String) -> Result<Vec<DirectoryEntry>, String> {
use std::fs;
use std::path::Path;
let dir_path = Path::new(&path);
if !dir_path.exists() {
return Err(format!("Directory does not exist: {}", path));
}
if !dir_path.is_dir() {
return Err(format!("Path is not a directory: {}", path));
}
let mut entries = Vec::new();
match fs::read_dir(dir_path) {
Ok(read_dir) => {
for entry in read_dir {
match entry {
Ok(entry) => {
let entry_path = entry.path();
if let Some(name) = entry_path.file_name() {
let is_dir = entry_path.is_dir();
// 获取文件元数据
let (size, modified) = match fs::metadata(&entry_path) {
Ok(metadata) => {
let size = if is_dir {
None
} else {
Some(metadata.len())
};
let modified = metadata.modified()
.ok()
.and_then(|time| {
time.duration_since(std::time::UNIX_EPOCH)
.ok()
.map(|d| d.as_secs())
});
(size, modified)
}
Err(_) => (None, None),
};
entries.push(DirectoryEntry {
name: name.to_string_lossy().to_string(),
path: entry_path.to_string_lossy().to_string(),
is_dir,
size,
modified,
});
}
}
Err(e) => eprintln!("Error reading directory entry: {}", e),
}
}
}
Err(e) => return Err(format!("Failed to read directory: {}", e)),
}
entries.sort_by(|a, b| {
match (a.is_dir, b.is_dir) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
}
});
Ok(entries)
}
#[tauri::command]
fn set_project_base_path(
path: String,
state: tauri::State<Arc<Mutex<HashMap<String, String>>>>
) -> Result<(), String> {
let mut paths = state.lock().map_err(|e| format!("Failed to lock state: {}", e))?;
paths.insert("current".to_string(), path);
Ok(())
}
#[tauri::command]
fn toggle_devtools(app: AppHandle) -> Result<(), String> {
#[cfg(debug_assertions)]
{
if let Some(window) = app.get_webview_window("main") {
if window.is_devtools_open() {
window.close_devtools();
} else {
window.open_devtools();
}
Ok(())
} else {
Err("Window not found".to_string())
}
}
#[cfg(not(debug_assertions))]
{
Err("DevTools are only available in debug mode".to_string())
}
}
// Profiler State
pub struct ProfilerState {
pub server: Arc<tokio::sync::Mutex<Option<Arc<ProfilerServer>>>>,
}
#[tauri::command]
async fn start_profiler_server(
port: u16,
state: tauri::State<'_, ProfilerState>,
) -> Result<String, String> {
let mut server_lock = state.server.lock().await;
if server_lock.is_some() {
return Err("Profiler server is already running".to_string());
}
let server = Arc::new(ProfilerServer::new(port));
match server.start().await {
Ok(_) => {
*server_lock = Some(server);
Ok(format!("Profiler server started on port {}", port))
}
Err(e) => Err(format!("Failed to start profiler server: {}", e)),
}
}
#[tauri::command]
async fn stop_profiler_server(
state: tauri::State<'_, ProfilerState>,
) -> Result<String, String> {
let mut server_lock = state.server.lock().await;
if server_lock.is_none() {
return Err("Profiler server is not running".to_string());
}
// 调用 stop 方法正确关闭服务器
if let Some(server) = server_lock.as_ref() {
server.stop().await;
}
*server_lock = None;
Ok("Profiler server stopped".to_string())
}
#[tauri::command]
async fn get_profiler_status(
state: tauri::State<'_, ProfilerState>,
) -> Result<bool, String> {
let server_lock = state.server.lock().await;
Ok(server_lock.is_some())
}
#[tauri::command]
async fn read_behavior_tree_file(file_path: String) -> Result<String, String> {
use std::fs;
// 使用 Rust 标准库直接读取文件,绕过 Tauri 的 scope 限制
fs::read_to_string(&file_path)
.map_err(|e| format!("Failed to read file {}: {}", file_path, e))
}
#[tauri::command]
async fn write_behavior_tree_file(file_path: String, content: String) -> Result<(), String> {
use std::fs;
// 使用 Rust 标准库直接写入文件
fs::write(&file_path, content)
.map_err(|e| format!("Failed to write file {}: {}", file_path, e))
}
#[tauri::command]
async fn write_binary_file(file_path: String, content: Vec<u8>) -> Result<(), String> {
use std::fs;
// 写入二进制文件
fs::write(&file_path, content)
.map_err(|e| format!("Failed to write binary file {}: {}", file_path, e))
}
#[tauri::command]
async fn read_global_blackboard(project_path: String) -> Result<String, String> {
use std::fs;
use std::path::Path;
let config_path = Path::new(&project_path).join(".ecs").join("global-blackboard.json");
if !config_path.exists() {
return Ok(String::from(r#"{"version":"1.0","variables":[]}"#));
}
fs::read_to_string(&config_path)
.map_err(|e| format!("Failed to read global blackboard: {}", e))
}
#[tauri::command]
async fn write_global_blackboard(project_path: String, content: String) -> Result<(), String> {
use std::fs;
use std::path::Path;
let ecs_dir = Path::new(&project_path).join(".ecs");
let config_path = ecs_dir.join("global-blackboard.json");
// 创建 .ecs 目录(如果不存在)
if !ecs_dir.exists() {
fs::create_dir_all(&ecs_dir)
.map_err(|e| format!("Failed to create .ecs directory: {}", e))?;
}
fs::write(&config_path, content)
.map_err(|e| format!("Failed to write global blackboard: {}", e))
}
#[tauri::command]
fn open_file_with_default_app(file_path: String) -> Result<(), String> {
use std::process::Command;
#[cfg(target_os = "windows")]
{
Command::new("cmd")
.args(["/C", "start", "", &file_path])
.spawn()
.map_err(|e| format!("Failed to open file: {}", e))?;
}
#[cfg(target_os = "macos")]
{
Command::new("open")
.arg(&file_path)
.spawn()
.map_err(|e| format!("Failed to open file: {}", e))?;
}
#[cfg(target_os = "linux")]
{
Command::new("xdg-open")
.arg(&file_path)
.spawn()
.map_err(|e| format!("Failed to open file: {}", e))?;
}
Ok(())
}
#[tauri::command]
fn show_in_folder(file_path: String) -> Result<(), String> {
use std::process::Command;
#[cfg(target_os = "windows")]
{
Command::new("explorer")
.args(["/select,", &file_path])
.spawn()
.map_err(|e| format!("Failed to show in folder: {}", e))?;
}
#[cfg(target_os = "macos")]
{
Command::new("open")
.args(["-R", &file_path])
.spawn()
.map_err(|e| format!("Failed to show in folder: {}", e))?;
}
#[cfg(target_os = "linux")]
{
use std::path::Path;
let path = Path::new(&file_path);
let parent = path.parent()
.ok_or_else(|| "Failed to get parent directory".to_string())?;
Command::new("xdg-open")
.arg(parent)
.spawn()
.map_err(|e| format!("Failed to show in folder: {}", e))?;
}
Ok(())
}
use state::{ProfilerState, ProjectPaths};
fn main() {
let project_paths: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
let project_paths_clone = Arc::clone(&project_paths);
// Initialize shared state
let project_paths: ProjectPaths = Arc::new(Mutex::new(HashMap::new()));
let project_paths_for_protocol = Arc::clone(&project_paths);
let profiler_state = ProfilerState {
server: Arc::new(tokio::sync::Mutex::new(None)),
};
let profiler_state = ProfilerState::new();
// Build and run the Tauri application
tauri::Builder::default()
// Register plugins
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_http::init())
// Register custom URI scheme for project files
.register_uri_scheme_protocol("project", move |_app, request| {
let project_paths = Arc::clone(&project_paths_clone);
let uri = request.uri();
let path = uri.path();
let file_path = {
let paths = project_paths.lock().unwrap();
if let Some(base_path) = paths.get("current") {
format!("{}{}", base_path, path)
} else {
return tauri::http::Response::builder()
.status(404)
.body(Vec::new())
.unwrap();
}
};
match std::fs::read(&file_path) {
Ok(content) => {
let mime_type = if file_path.ends_with(".ts") || file_path.ends_with(".tsx") {
"application/javascript"
} else if file_path.ends_with(".js") {
"application/javascript"
} else if file_path.ends_with(".json") {
"application/json"
} else {
"text/plain"
};
tauri::http::Response::builder()
.status(200)
.header("Content-Type", mime_type)
.header("Access-Control-Allow-Origin", "*")
.body(content)
.unwrap()
}
Err(e) => {
eprintln!("Failed to read file {}: {}", file_path, e);
tauri::http::Response::builder()
.status(404)
.body(Vec::new())
.unwrap()
}
}
handle_project_protocol(request, &project_paths_for_protocol)
})
// Setup application state
.setup(move |app| {
app.manage(project_paths);
app.manage(profiler_state);
Ok(())
})
// Register all commands
.invoke_handler(tauri::generate_handler![
greet,
open_project,
save_project,
export_binary,
create_directory,
write_file_content,
path_exists,
rename_file_or_folder,
delete_file,
delete_folder,
create_file,
open_project_dialog,
save_scene_dialog,
open_scene_dialog,
open_behavior_tree_dialog,
scan_directory,
scan_behavior_trees,
read_file_content,
list_directory,
set_project_base_path,
toggle_devtools,
start_profiler_server,
stop_profiler_server,
get_profiler_status,
read_behavior_tree_file,
write_behavior_tree_file,
write_binary_file,
read_global_blackboard,
write_global_blackboard,
open_file_with_default_app,
show_in_folder
// Project management
commands::open_project,
commands::save_project,
commands::export_binary,
commands::set_project_base_path,
commands::scan_behavior_trees,
// File system operations
commands::read_file_content,
commands::write_file_content,
commands::write_binary_file,
commands::path_exists,
commands::create_directory,
commands::create_file,
commands::delete_file,
commands::delete_folder,
commands::rename_file_or_folder,
commands::list_directory,
commands::scan_directory,
commands::read_file_as_base64,
// Dialog operations
commands::open_folder_dialog,
commands::open_file_dialog,
commands::save_file_dialog,
// Profiler server
commands::start_profiler_server,
commands::stop_profiler_server,
commands::get_profiler_status,
// Plugin management
commands::build_plugin,
commands::install_marketplace_plugin,
commands::uninstall_marketplace_plugin,
// System operations
commands::toggle_devtools,
commands::open_file_with_default_app,
commands::show_in_folder,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
/// Handle the custom 'project://' URI scheme protocol
///
/// This allows the frontend to load project files through a custom protocol,
/// enabling features like hot-reloading plugins from the project directory.
fn handle_project_protocol(
request: tauri::http::Request<Vec<u8>>,
project_paths: &ProjectPaths,
) -> tauri::http::Response<Vec<u8>> {
let uri = request.uri();
let path = uri.path();
let file_path = {
let paths = match project_paths.lock() {
Ok(p) => p,
Err(_) => {
return tauri::http::Response::builder()
.status(500)
.body(Vec::new())
.unwrap();
}
};
match paths.get("current") {
Some(base_path) => format!("{}{}", base_path, path),
None => {
return tauri::http::Response::builder()
.status(404)
.body(Vec::new())
.unwrap();
}
}
};
match std::fs::read(&file_path) {
Ok(content) => {
let mime_type = get_mime_type(&file_path);
tauri::http::Response::builder()
.status(200)
.header("Content-Type", mime_type)
.header("Access-Control-Allow-Origin", "*")
.body(content)
.unwrap()
}
Err(e) => {
eprintln!("Failed to read file {}: {}", file_path, e);
tauri::http::Response::builder()
.status(404)
.body(Vec::new())
.unwrap()
}
}
}
/// Get MIME type based on file extension
fn get_mime_type(file_path: &str) -> &'static str {
if file_path.ends_with(".ts") || file_path.ends_with(".tsx") {
"application/javascript"
} else if file_path.ends_with(".js") {
"application/javascript"
} else if file_path.ends_with(".json") {
"application/json"
} else if file_path.ends_with(".css") {
"text/css"
} else if file_path.ends_with(".html") {
"text/html"
} else {
"text/plain"
}
}

View File

@@ -84,6 +84,7 @@ impl ProfilerServer {
println!("[ProfilerServer] Server stopped");
}
#[allow(dead_code)]
pub fn broadcast(&self, message: String) {
let _ = self.tx.send(message);
}

View File

@@ -1,42 +0,0 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Project {
pub name: String,
pub path: PathBuf,
pub scenes: Vec<String>,
pub assets: Vec<String>,
}
impl Project {
pub fn new(name: String, path: PathBuf) -> Self {
Self {
name,
path,
scenes: Vec::new(),
assets: Vec::new(),
}
}
pub fn load(path: &PathBuf) -> Result<Self, String> {
let content = std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read project file: {}", e))?;
serde_json::from_str(&content)
.map_err(|e| format!("Failed to parse project file: {}", e))
}
pub fn save(&self) -> Result<(), String> {
let mut project_file = self.path.clone();
project_file.push("project.json");
let content = serde_json::to_string_pretty(self)
.map_err(|e| format!("Failed to serialize project: {}", e))?;
std::fs::write(&project_file, content)
.map_err(|e| format!("Failed to write project file: {}", e))?;
Ok(())
}
}

View File

@@ -0,0 +1,34 @@
//! Application state definitions
//!
//! Centralized state management for the Tauri application.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::sync::Mutex as TokioMutex;
use crate::profiler_ws::ProfilerServer;
/// Project paths state
///
/// Stores the current project path and other path-related information.
pub type ProjectPaths = Arc<Mutex<HashMap<String, String>>>;
/// Profiler server state
///
/// Manages the lifecycle of the WebSocket profiler server.
pub struct ProfilerState {
pub server: Arc<TokioMutex<Option<Arc<ProfilerServer>>>>,
}
impl ProfilerState {
pub fn new() -> Self {
Self {
server: Arc::new(TokioMutex::new(None)),
}
}
}
impl Default for ProfilerState {
fn default() -> Self {
Self::new()
}
}