web based infinite canvas
1use std::fs;
2use std::path::{Path, PathBuf};
3use tauri::AppHandle;
4
5#[derive(serde::Serialize, serde::Deserialize)]
6pub struct FileEntry {
7 pub path: String,
8 pub name: String,
9 pub is_dir: bool,
10}
11
12/// Read directory contents and return matching files
13#[tauri::command]
14fn read_directory(directory: String, pattern: Option<String>) -> Result<Vec<FileEntry>, String> {
15 let path = Path::new(&directory);
16 if !path.exists() {
17 return Err(format!("Directory does not exist: {}", directory));
18 }
19 if !path.is_dir() {
20 return Err(format!("Path is not a directory: {}", directory));
21 }
22
23 let entries = fs::read_dir(path).map_err(|e| format!("Failed to read directory: {}", e))?;
24
25 let mut results = Vec::new();
26 let pattern = pattern.unwrap_or_else(|| "*.inkfinite.json".to_string());
27
28 for entry in entries {
29 let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
30 let entry_path = entry.path();
31 let metadata = entry
32 .metadata()
33 .map_err(|e| format!("Failed to read metadata: {}", e))?;
34
35 let name = entry.file_name().to_string_lossy().to_string();
36
37 if metadata.is_file() {
38 if pattern.contains('*') {
39 let pattern_without_star = pattern.replace('*', "");
40 if !name.contains(&pattern_without_star) {
41 continue;
42 }
43 } else if !name.ends_with(&pattern) {
44 continue;
45 }
46 }
47
48 results.push(FileEntry {
49 path: entry_path.to_string_lossy().to_string(),
50 name,
51 is_dir: metadata.is_dir(),
52 });
53 }
54
55 // Sort: directories first, then files, alphabetically
56 results.sort_by(|a, b| {
57 if a.is_dir == b.is_dir {
58 a.name.to_lowercase().cmp(&b.name.to_lowercase())
59 } else if a.is_dir {
60 std::cmp::Ordering::Less
61 } else {
62 std::cmp::Ordering::Greater
63 }
64 });
65
66 Ok(results)
67}
68
69/// Rename a file
70#[tauri::command]
71fn rename_file(old_path: String, new_path: String) -> Result<(), String> {
72 let old = Path::new(&old_path);
73 let new = Path::new(&new_path);
74
75 if !old.exists() {
76 return Err(format!("Source file does not exist: {}", old_path));
77 }
78
79 fs::rename(old, new).map_err(|e| format!("Failed to rename file: {}", e))?;
80
81 Ok(())
82}
83
84/// Delete a file
85#[tauri::command]
86fn delete_file(file_path: String) -> Result<(), String> {
87 let path = Path::new(&file_path);
88
89 if !path.exists() {
90 return Err(format!("File does not exist: {}", file_path));
91 }
92
93 if path.is_dir() {
94 return Err(format!("Path is a directory, not a file: {}", file_path));
95 }
96
97 fs::remove_file(path).map_err(|e| format!("Failed to delete file: {}", e))?;
98
99 Ok(())
100}
101
102/// Pick a workspace directory using the system folder picker
103#[tauri::command]
104async fn pick_workspace_directory(app: AppHandle) -> Result<Option<String>, String> {
105 use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
106
107 let result = app.dialog().file().blocking_pick_folder();
108
109 match result {
110 Some(path) => Ok(Some(path.to_string_lossy().to_string())),
111 None => Ok(None),
112 }
113}
114
115#[cfg_attr(mobile, tauri::mobile_entry_point)]
116pub fn run() {
117 tauri::Builder::default()
118 .plugin(tauri_plugin_opener::init())
119 .plugin(tauri_plugin_dialog::init())
120 .plugin(tauri_plugin_fs::init())
121 .plugin(tauri_plugin_store::Builder::default().build())
122 .invoke_handler(tauri::generate_handler![
123 read_directory,
124 rename_file,
125 delete_file,
126 pick_workspace_directory
127 ])
128 .run(tauri::generate_context!())
129 .expect("error while running tauri application");
130}