use serde::Serialize;
use std::fs;
use std::path::{Path, PathBuf};
use tauri::AppHandle;
use tauri_plugin_dialog::DialogExt;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenedFile {
pub path: String,
pub content: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FileEntry {
pub path: String,
pub name: String,
pub is_dir: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<FileEntry>>,
}
const IGNORE_DIRS: &[&str] = &[
".git",
"node_modules",
"target",
"dist",
"build",
".next",
".turbo",
".cache",
".svelte-kit",
"out",
"coverage",
];
#[tauri::command]
pub fn open_file(path: String) -> Result<OpenedFile, String> {
let content = fs::read_to_string(&path).map_err(|e| e.to_string())?;
Ok(OpenedFile { path, content })
}
#[tauri::command]
pub fn save_file(path: String, content: String) -> Result<(), String> {
fs::write(&path, content).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn create_file(path: String) -> Result<(), String> {
if let Some(parent) = Path::new(&path).parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub fn create_dir(path: String) -> Result<(), String> {
fs::create_dir_all(&path).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn delete_path(path: String) -> Result<(), String> {
let p = Path::new(&path);
if p.is_dir() {
fs::remove_dir_all(p).map_err(|e| e.to_string())
} else {
fs::remove_file(p).map_err(|e| e.to_string())
}
}
#[tauri::command]
pub fn rename_path(from: String, to: String) -> Result<(), String> {
fs::rename(&from, &to).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn list_dir(path: String, depth: u32) -> Result<FileEntry, String> {
let p = PathBuf::from(&path);
build_entry(&p, depth).ok_or_else(|| format!("cannot read {path}"))
}
#[tauri::command]
pub async fn pick_folder(app: AppHandle) -> Result<Option<String>, String> {
let picked = app.dialog().file().blocking_pick_folder();
Ok(picked
.and_then(|p| p.into_path().ok())
.map(|pb| pb.to_string_lossy().to_string()))
}
fn build_entry(path: &Path, depth: u32) -> Option<FileEntry> {
let name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| path.to_string_lossy().to_string());
let is_dir = path.is_dir();
let children = if is_dir && depth > 0 && !is_ignored(&name) {
Some(read_children(path, depth - 1))
} else {
None
};
Some(FileEntry {
path: path.to_string_lossy().to_string(),
name,
is_dir,
children,
})
}
fn read_children(dir: &Path, depth: u32) -> Vec<FileEntry> {
let mut entries: Vec<FileEntry> = match fs::read_dir(dir) {
Ok(rd) => rd
.filter_map(|e| e.ok())
.filter_map(|e| build_entry(&e.path(), depth))
.collect(),
Err(_) => Vec::new(),
};
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()),
});
entries
}
fn is_ignored(name: &str) -> bool {
IGNORE_DIRS.contains(&name)
}