diff --git a/cli/src/main.rs b/cli/src/main.rs index 0942668..0d28ddd 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -28,18 +28,67 @@ enum Command { index: usize, #[arg(short, long)] system: bool, }, + /// 编辑指定位置的路径 + Edit { + index: usize, + new_path: String, + #[arg(short, long)] system: bool, + }, + /// 上移路径(--steps 指定移动格数,默认 1) + MoveUp { + index: usize, + #[arg(long, default_value = "1")] steps: usize, + #[arg(short, long)] system: bool, + }, + /// 下移路径(--steps 指定移动格数,默认 1) + MoveDown { + index: usize, + #[arg(long, default_value = "1")] steps: usize, + #[arg(short, long)] system: bool, + }, + /// 清理无效和重复路径 + Clean { + #[arg(short, long)] system: bool, + #[arg(short, long)] user: bool, + #[arg(long)] dry_run: bool, + #[arg(long)] json: bool, + }, + /// 启用指定位置的路径 + Enable { + index: usize, + #[arg(short, long)] system: bool, + #[arg(short, long)] user: bool, + }, + /// 禁用指定位置的路径 + Disable { + index: usize, + #[arg(short, long)] system: bool, + #[arg(short, long)] user: bool, + }, + /// 从文件导入 PATH(JSON/CSV/TXT) + Import { + file: String, + #[arg(long, default_value = "both")] target: String, + }, + /// 导出 PATH 为文件 + Export { + #[arg(long, default_value = "json")] format: String, + #[arg(short, long)] output: Option, + }, + /// 创建注册表备份 + Backup, /// 检测可执行文件冲突 Conflicts { #[arg(long)] json: bool }, - /// 列出 PATH 中可执行文件 + /// 列出 PATH 目录中的可执行文件 Scan { #[arg(long)] query: Option, #[arg(long)] json: bool, }, + /// 检查管理员权限 + CheckAdmin { #[arg(long)] json: bool }, /// 管理配置文件 #[command(subcommand)] Profile(ProfileCmd), - /// 检查管理员权限 - CheckAdmin { #[arg(long)] json: bool }, } #[derive(Subcommand)] @@ -65,6 +114,21 @@ fn pick_target(system: bool, _user: bool) -> &'static str { if system { "system" } else { "user" } } +type SaveFn = fn(Vec) -> Result<(), String>; + +fn load_and_save(system: bool, f: impl FnOnce(Vec) -> Vec) { + let target = pick_target(system, false); + let (list, save): (Vec, SaveFn) = if target == "system" { + (core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e)), + core::registry::save_system_paths) + } else { + (core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e)), + core::registry::save_user_paths) + }; + let new_list = f(list); + save(new_list).unwrap_or_else(|e| exit_err(&e)); +} + // ── 命令实现 ── fn cmd_list(system: bool, user: bool, json_out: bool) { @@ -93,34 +157,146 @@ fn cmd_list(system: bool, user: bool, json_out: bool) { fn cmd_add(path: String, system: bool, user: bool) { let target = pick_target(system, user); - let (mut list, save_fn): (Vec, _) = if target == "system" { - (core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e)), - core::registry::save_system_paths as fn(Vec) -> Result<(), String>) - } else { - (core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e)), - core::registry::save_user_paths as fn(Vec) -> Result<(), String>) - }; - list.push(path.clone()); - save_fn(list).unwrap_or_else(|e| exit_err(&e)); + load_and_save(system || false, |mut list| { + list.push(path.clone()); + list + }); let label = if target == "system" { "系统" } else { "用户" }; println!("已添加到{} PATH: {path}", label); } fn cmd_remove(index: usize, system: bool) { let target = pick_target(system, false); - let (mut list, save_fn): (Vec, _) = if target == "system" { - (core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e)), - core::registry::save_system_paths as fn(Vec) -> Result<(), String>) + let mut list = if target == "system" { + core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e)) } else { - (core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e)), - core::registry::save_user_paths as fn(Vec) -> Result<(), String>) + core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e)) }; if index >= list.len() { exit_err(&format!("索引 {index} 超出范围 (共 {} 条)", list.len())); } let removed = list.remove(index); - save_fn(list).unwrap_or_else(|e| exit_err(&e)); + let save: SaveFn = if target == "system" { core::registry::save_system_paths } else { core::registry::save_user_paths }; + save(list).unwrap_or_else(|e| exit_err(&e)); println!("已删除: {removed}"); } +fn cmd_edit(index: usize, new_path: String, system: bool) { + let target = pick_target(system, false); + let mut list = if target == "system" { + core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e)) + } else { + core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e)) + }; + if index >= list.len() { exit_err(&format!("索引 {index} 超出范围 (共 {} 条)", list.len())); } + let old = std::mem::replace(&mut list[index], new_path.clone()); + let save: SaveFn = if target == "system" { core::registry::save_system_paths } else { core::registry::save_user_paths }; + save(list).unwrap_or_else(|e| exit_err(&e)); + println!("已编辑: {old} → {new_path}"); +} + +fn cmd_move(index: usize, steps: usize, system: bool, up: bool) { + load_and_save(system || false, |mut list| { + if index >= list.len() { exit_err(&format!("索引 {index} 超出范围 (共 {} 条)", list.len())); } + let end = if up { + if steps > index { 0 } else { index - steps } + } else { + let max = list.len() - 1; + if index + steps > max { max } else { index + steps } + }; + let removed = list.remove(index); + list.insert(end, removed); + list + }); + let dir = if up { "上移" } else { "下移" }; + println!("{dir} {steps} 格完成"); +} + +fn cmd_clean(system: bool, user: bool, dry_run: bool, json_out: bool) { + let target = pick_target(system, user); + let list = if target == "system" { + core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e)) + } else { + core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e)) + }; + let (kept, removed) = core::registry::clean_paths(list); + + if json_out { + println!("{}", json!({ "kept": kept, "removed": removed, "kept_count": kept.len(), "removed_count": removed.len() }).to_string()); + } else if dry_run { + println!("═══ 将被移除({} 条)═══", removed.len()); + for r in &removed { println!(" ✗ {}", r); } + println!("═══ 将保留({} 条)═══", kept.len()); + for k in &kept { println!(" ✓ {}", k); } + } else { + let kept_count = kept.len(); + let save: SaveFn = if target == "system" { core::registry::save_system_paths } else { core::registry::save_user_paths }; + save(kept).unwrap_or_else(|e| exit_err(&e)); + println!("清理完成:移除 {} 条,保留 {} 条", removed.len(), kept_count); + if !removed.is_empty() { + for r in &removed { println!(" 已移除: {}", r); } + } + } +} + +fn cmd_toggle(index: usize, system: bool, user: bool, enable: bool) { + let target = pick_target(system, user); + let list = if target == "system" { + core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e)) + } else { + core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e)) + }; + if index >= list.len() { exit_err(&format!("索引 {index} 超出范围 (共 {} 条)", list.len())); } + let path = &list[index]; + + let (mut sys_dis, mut usr_dis) = core::disabled::load_disabled_state().unwrap_or_else(|_| (vec![], vec![])); + let target_list: &mut Vec = if target == "system" { &mut sys_dis } else { &mut usr_dis }; + + if enable { + target_list.retain(|p| p != path); + } else if !target_list.contains(path) { + target_list.push(path.clone()); + } + core::disabled::save_disabled_state(sys_dis, usr_dis).unwrap_or_else(|e| exit_err(&e)); + let action = if enable { "启用" } else { "禁用" }; + println!("已{action}: {path}"); +} + +fn cmd_import(file: String, target: String) { + let content = core::fs::read_text_file(&file).unwrap_or_else(|e| exit_err(&e)); + let (sys, usr) = core::fs::import_paths(&file, &content).unwrap_or_else(|e| exit_err(&e)); + match target.as_str() { + "system" => { + core::registry::save_system_paths(sys).unwrap_or_else(|e| exit_err(&e)); + println!("已导入到系统 PATH"); + } + "user" => { + core::registry::save_user_paths(usr).unwrap_or_else(|e| exit_err(&e)); + println!("已导入到用户 PATH"); + } + _ => { + core::registry::save_system_paths(sys).unwrap_or_else(|e| exit_err(&e)); + core::registry::save_user_paths(usr).unwrap_or_else(|e| exit_err(&e)); + println!("已导入到系统 + 用户 PATH"); + } + } +} + +fn cmd_export(format: String, output: Option) { + let sys = core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e)); + let usr = core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e)); + let content = core::fs::export_paths(&sys, &usr, &format); + if let Some(path) = output { + std::fs::write(&path, &content).unwrap_or_else(|e| exit_err(&format!("无法写入文件: {e}"))); + println!("已导出到: {path}"); + } else { + println!("{content}"); + } +} + +fn cmd_backup() { + let path = core::backup::backup_registry(None).unwrap_or_else(|e| exit_err(&e)); + println!("备份已保存: {path}"); +} + fn cmd_conflicts(json_out: bool) { let mut paths: Vec = vec![]; if let Ok(sys) = core::registry::load_system_paths() { paths.extend(sys); } @@ -216,6 +392,15 @@ fn main() { Command::List { system, user, json } => cmd_list(system, user, json), Command::Add { path, system, user } => cmd_add(path, system, user), Command::Remove { index, system } => cmd_remove(index, system), + Command::Edit { index, new_path, system } => cmd_edit(index, new_path, system), + Command::MoveUp { index, steps, system } => cmd_move(index, steps, system, true), + Command::MoveDown { index, steps, system } => cmd_move(index, steps, system, false), + Command::Clean { system, user, dry_run, json } => cmd_clean(system, user, dry_run, json), + Command::Enable { index, system, user } => cmd_toggle(index, system, user, true), + Command::Disable { index, system, user } => cmd_toggle(index, system, user, false), + Command::Import { file, target } => cmd_import(file, target), + Command::Export { format, output } => cmd_export(format, output), + Command::Backup => cmd_backup(), Command::Conflicts { json } => cmd_conflicts(json), Command::Scan { query, json } => cmd_scan(query, json), Command::CheckAdmin { json } => cmd_check_admin(json), diff --git a/core/src/fs.rs b/core/src/fs.rs index 7ab2788..aedd18d 100644 --- a/core/src/fs.rs +++ b/core/src/fs.rs @@ -2,3 +2,105 @@ pub fn read_text_file(path: &str) -> Result { std::fs::read_to_string(path).map_err(|e| format!("无法读取文件: {}", e)) } + +/// 导入路径文件(JSON / CSV / TXT),返回 (系统路径, 用户路径) +pub fn import_paths(path: &str, content: &str) -> Result<(Vec, Vec), String> { + let ext = std::path::Path::new(path) + .extension() + .map(|e| e.to_ascii_lowercase()) + .unwrap_or_default(); + let ext = ext.to_string_lossy(); + + match ext.as_ref() { + "json" => import_json(content), + "csv" => import_csv(content), + "txt" => import_txt(content), + _ => Err(format!("不支持的格式: .{}", ext)), + } +} + +fn import_json(content: &str) -> Result<(Vec, Vec), String> { + #[derive(serde::Deserialize)] + struct ImportData { + #[serde(default)] + system: Vec, + #[serde(default)] + user: Vec, + } + let data: ImportData = + serde_json::from_str(content).map_err(|e| format!("JSON 解析失败: {}", e))?; + Ok((data.system, data.user)) +} + +fn import_csv(content: &str) -> Result<(Vec, Vec), String> { + let mut sys = Vec::new(); + let mut usr = Vec::new(); + for line in content.lines() { + let fields: Vec<&str> = line.split(',').collect(); + if fields.len() >= 2 { + match fields[0].trim() { + "system" | "sys" => sys.push(fields[1].trim().to_string()), + "user" | "usr" => usr.push(fields[1].trim().to_string()), + _ => {} + } + } + } + if sys.is_empty() && usr.is_empty() { + return Err("CSV 文件中未找到有效路径".into()); + } + Ok((sys, usr)) +} + +fn import_txt(content: &str) -> Result<(Vec, Vec), String> { + let paths: Vec = content + .lines() + .map(|l| l.trim().to_string()) + .filter(|l| !l.is_empty() && !l.starts_with('#')) + .collect(); + if paths.is_empty() { + return Err("TXT 文件中未找到路径".into()); + } + // TXT 格式全部导入为用户路径 + Ok((vec![], paths)) +} + +/// 导出 PATH 为指定格式字符串 +pub fn export_paths(sys: &[String], usr: &[String], format: &str) -> String { + match format { + "json" => { + let data = serde_json::json!({ + "version": "5.0.0", + "timestamp": chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string(), + "system": sys, + "user": usr, + }); + serde_json::to_string_pretty(&data).unwrap_or_default() + } + "csv" => { + let mut out = String::from("type,path\n"); + for p in sys { + out.push_str(&format!("system,{}\n", p)); + } + for p in usr { + out.push_str(&format!("user,{}\n", p)); + } + out + } + _ => { + let mut out = String::new(); + if !sys.is_empty() { + out.push_str(&format!("# 系统 PATH ({})\n", sys.len())); + for p in sys { + out.push_str(&format!("{}\n", p)); + } + } + if !usr.is_empty() { + out.push_str(&format!("# 用户 PATH ({})\n", usr.len())); + for p in usr { + out.push_str(&format!("{}\n", p)); + } + } + out + } + } +} diff --git a/core/src/registry.rs b/core/src/registry.rs index fa20723..bd0c947 100644 --- a/core/src/registry.rs +++ b/core/src/registry.rs @@ -81,6 +81,29 @@ fn join_path(paths: &[String]) -> String { .join(";") } +/// 清理路径列表:移除不存在的目录 + 重复路径(保留首次出现) +/// 返回 (保留的路径, 被移除的路径) +pub fn clean_paths(paths: Vec) -> (Vec, Vec) { + use std::collections::HashSet; + let mut seen: HashSet = HashSet::new(); + let mut kept = Vec::new(); + let mut removed = Vec::new(); + for p in paths { + let key = p.trim().to_lowercase(); + if seen.contains(&key) { + removed.push(p); + continue; + } + seen.insert(key); + if !p.contains('%') && !std::path::Path::new(&p).is_dir() { + removed.push(p); + continue; + } + kept.push(p); + } + (kept, removed) +} + #[cfg(test)] mod tests { use super::*;