use crate::error::{LsPlusError, Result}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DirectoryDescription {
pub path: PathBuf,
pub description: String,
pub created_at: chrono::DateTime<chrono::Local>,
pub updated_at: chrono::DateTime<chrono::Local>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DescriptionDatabase {
pub descriptions: HashMap<String, DirectoryDescription>,
}
impl DescriptionDatabase {
pub fn new() -> Self {
Self {
descriptions: HashMap::new(), }
}
pub fn database_file() -> Result<PathBuf> {
let config_dir = crate::config::Config::config_dir()?; Ok(config_dir.join("descriptions.json")) }
pub fn load() -> Result<Self> {
let db_file = Self::database_file()?;
if !db_file.exists() {
return Ok(Self::new());
}
let content = fs::read_to_string(&db_file)
.map_err(|e| LsPlusError::system_error(format!("无法读取描述数据库: {e}")))?;
let database: Self = serde_json::from_str(&content)
.map_err(|e| LsPlusError::format_error(format!("描述数据库格式错误: {e}")))?;
Ok(database)
}
pub fn save(&self) -> Result<()> {
let config_dir = crate::config::Config::config_dir()?;
let db_file = Self::database_file()?;
if !config_dir.exists() {
fs::create_dir_all(&config_dir)
.map_err(|e| LsPlusError::system_error(format!("无法创建配置目录: {e}")))?;
}
let content = serde_json::to_string_pretty(self)
.map_err(|e| LsPlusError::format_error(format!("无法序列化描述数据库: {e}")))?;
fs::write(&db_file, content)
.map_err(|e| LsPlusError::system_error(format!("无法保存描述数据库: {e}")))?;
Ok(())
}
pub fn set_description<P: AsRef<Path>>(&mut self, path: P, description: String) -> Result<()> {
let path = path.as_ref();
let canonical_path = path
.canonicalize()
.map_err(|_e| LsPlusError::path_not_found(path.to_path_buf()))?;
let path_key = canonical_path.to_string_lossy().to_string();
let now = chrono::Local::now();
if let Some(existing) = self.descriptions.get_mut(&path_key) {
existing.description = description;
existing.updated_at = now; } else {
let desc = DirectoryDescription {
path: canonical_path,
description,
created_at: now, updated_at: now, };
self.descriptions.insert(path_key, desc);
}
Ok(())
}
pub fn get_description<P: AsRef<Path>>(&self, path: P) -> Option<&DirectoryDescription> {
let path = path.as_ref();
if let Ok(canonical_path) = path.canonicalize() {
let path_key = canonical_path.to_string_lossy().to_string();
self.descriptions.get(&path_key) } else {
None }
}
pub fn remove_description<P: AsRef<Path>>(&mut self, path: P) -> Result<bool> {
let path = path.as_ref();
let canonical_path = path
.canonicalize()
.map_err(|_e| LsPlusError::path_not_found(path.to_path_buf()))?;
let path_key = canonical_path.to_string_lossy().to_string();
Ok(self.descriptions.remove(&path_key).is_some())
}
pub fn list_all(&self) -> Vec<&DirectoryDescription> {
let mut descriptions: Vec<&DirectoryDescription> = self.descriptions.values().collect();
descriptions.sort_by(|a, b| a.path.cmp(&b.path));
descriptions
}
pub fn search_descriptions(&self, query: &str) -> Vec<&DirectoryDescription> {
let query_lower = query.to_lowercase(); self.descriptions
.values()
.filter(|desc| {
desc.description.to_lowercase().contains(&query_lower) ||
desc.path.to_string_lossy().to_lowercase().contains(&query_lower)
})
.collect()
}
pub fn cleanup_missing_directories(&mut self) -> usize {
let mut to_remove = Vec::new();
for (key, desc) in &self.descriptions {
if !desc.path.exists() {
to_remove.push(key.clone()); }
}
let removed_count = to_remove.len();
for key in to_remove {
self.descriptions.remove(&key);
}
removed_count }
}
pub struct DescriptionManager {
database: DescriptionDatabase,
}
impl DescriptionManager {
pub fn new() -> Result<Self> {
let database = DescriptionDatabase::load()?; Ok(Self { database })
}
pub fn set_description<P: AsRef<Path>>(&mut self, path: P, description: String) -> Result<()> {
let path = path.as_ref();
if !path.exists() {
return Err(LsPlusError::path_not_found(path.to_path_buf()));
}
if !path.is_dir() {
return Err(LsPlusError::format_error("只能为目录添加描述"));
}
self.database.set_description(path, description)?;
self.database.save()?;
Ok(())
}
pub fn get_description<P: AsRef<Path>>(&self, path: P) -> Option<&DirectoryDescription> {
self.database.get_description(path)
}
pub fn remove_description<P: AsRef<Path>>(&mut self, path: P) -> Result<bool> {
let removed = self.database.remove_description(path)?;
if removed {
self.database.save()?;
}
Ok(removed)
}
pub fn list_all(&self) -> Vec<&DirectoryDescription> {
self.database.list_all()
}
pub fn search(&self, query: &str) -> Vec<&DirectoryDescription> {
self.database.search_descriptions(query)
}
pub fn cleanup(&mut self) -> Result<usize> {
let removed_count = self.database.cleanup_missing_directories();
if removed_count > 0 {
self.database.save()?;
}
Ok(removed_count)
}
}
pub async fn handle_describe_command(action: crate::cli::DescribeAction) -> crate::Result<()> {
use crate::cli::DescribeAction;
use console::style;
match action {
DescribeAction::Set { path, description } => {
let mut manager = DescriptionManager::new()?;
manager.set_description(&path, description.clone())?;
println!(
"✅ 已为目录 {} 设置描述: {}",
style(path.display()).cyan(), style(&description).green() );
}
DescribeAction::Get { path } => {
let manager = DescriptionManager::new()?;
if let Some(desc) = manager.get_description(&path) {
println!("📁 {}", style(&desc.path.display()).cyan().bold()); println!("📝 {}", desc.description); println!(
"🕐 创建时间: {}",
desc.created_at.format("%Y-%m-%d %H:%M:%S")
); println!(
"🕑 更新时间: {}",
desc.updated_at.format("%Y-%m-%d %H:%M:%S")
); } else {
println!("❌ 目录 {} 没有描述", style(path.display()).yellow());
}
}
DescribeAction::Remove { path } => {
let mut manager = DescriptionManager::new()?;
if manager.remove_description(&path)? {
println!("✅ 已删除目录 {} 的描述", style(path.display()).cyan());
} else {
println!("❌ 目录 {} 没有描述可删除", style(path.display()).yellow());
}
}
DescribeAction::List => {
let manager = DescriptionManager::new()?;
let descriptions = manager.list_all();
if descriptions.is_empty() {
println!("📭 暂无目录描述");
} else {
println!("📋 所有目录描述 ({} 个):", descriptions.len());
println!();
for desc in descriptions {
println!("📁 {}", style(&desc.path.display()).cyan().bold()); println!(" 📝 {}", desc.description); println!(" 🕐 {}", desc.updated_at.format("%Y-%m-%d %H:%M:%S")); println!(); }
}
}
DescribeAction::Search { query } => {
let manager = DescriptionManager::new()?;
let results = manager.search(&query);
if results.is_empty() {
println!("🔍 未找到包含 \"{}\" 的描述", style(&query).yellow());
} else {
println!("🔍 搜索结果 ({} 个):", results.len());
println!();
for desc in results {
println!("📁 {}", style(&desc.path.display()).cyan().bold());
let highlighted_desc = highlight_search_term(&desc.description, &query);
println!(" 📝 {highlighted_desc}"); println!(" 🕐 {}", desc.updated_at.format("%Y-%m-%d %H:%M:%S")); println!(); }
}
}
DescribeAction::Cleanup => {
let mut manager = DescriptionManager::new()?;
let removed_count = manager.cleanup()?;
if removed_count > 0 {
println!("✅ 已清理 {removed_count} 个不存在目录的描述");
} else {
println!("✨ 没有需要清理的描述");
}
}
}
Ok(())
}
fn highlight_search_term(text: &str, query: &str) -> String {
let query_lower = query.to_lowercase(); let mut result = String::new();
let mut last_end = 0;
for (start, part) in text.match_indices(&query_lower) {
result.push_str(&text[last_end..start]);
result.push_str(
&console::style(&text[start..start + part.len()])
.yellow()
.bold()
.to_string(),
);
last_end = start + part.len();
}
result.push_str(&text[last_end..]);
if result.is_empty() {
text.to_string()
} else {
result
}
}