mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 01:37:22 +08:00
refactor: 提取 core 库 + 新增 CLI 版本
- 创建 Cargo workspace(core / src-tauri / cli 三 crate) - core: 纯 Rust 库,零 Tauri 依赖,包含所有业务逻辑 - src-tauri/commands: 改为薄包装,调用 core 函数 - cli: 基于 clap 的命令行工具,支持 JSON 输出 - CLI 命令: list, add, remove, conflicts, scan, profile, check-admin Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Generated
+5329
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"core",
|
||||
"src-tauri",
|
||||
"cli",
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "patheditor-cli"
|
||||
version = "5.0.0"
|
||||
description = "PathEditor CLI — command-line interface for Windows PATH management"
|
||||
authors = ["刘航宇"]
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
path-editor-core = { path = "../core" }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
[[bin]]
|
||||
name = "patheditor"
|
||||
path = "src/main.rs"
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use path_editor_core as core;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "patheditor", version = "5.0.0")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
/// 列出 PATH 路径
|
||||
List {
|
||||
#[arg(short, long)] system: bool,
|
||||
#[arg(short, long)] user: bool,
|
||||
#[arg(long)] json: bool,
|
||||
},
|
||||
/// 添加一条路径
|
||||
Add {
|
||||
path: String,
|
||||
#[arg(short, long)] system: bool,
|
||||
#[arg(short, long)] user: bool,
|
||||
},
|
||||
/// 删除指定位置的路径
|
||||
Remove {
|
||||
index: usize,
|
||||
#[arg(short, long)] system: bool,
|
||||
},
|
||||
/// 检测可执行文件冲突
|
||||
Conflicts { #[arg(long)] json: bool },
|
||||
/// 列出 PATH 中可执行文件
|
||||
Scan {
|
||||
#[arg(long)] query: Option<String>,
|
||||
#[arg(long)] json: bool,
|
||||
},
|
||||
/// 管理配置文件
|
||||
#[command(subcommand)]
|
||||
Profile(ProfileCmd),
|
||||
/// 检查管理员权限
|
||||
CheckAdmin { #[arg(long)] json: bool },
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum ProfileCmd {
|
||||
/// 列出所有配置
|
||||
List { #[arg(long)] json: bool },
|
||||
/// 保存当前 PATH 为配置
|
||||
Save { name: String },
|
||||
/// 加载配置(预览)
|
||||
Load { name: String },
|
||||
/// 应用配置(写入注册表)
|
||||
Apply { name: String },
|
||||
/// 删除配置
|
||||
Delete { name: String },
|
||||
}
|
||||
|
||||
fn exit_err(msg: &str) -> ! {
|
||||
eprintln!("错误: {msg}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
fn pick_target(system: bool, user: bool) -> &'static str {
|
||||
if system { "system" } else { "user" }
|
||||
}
|
||||
|
||||
// ── 命令实现 ──
|
||||
|
||||
fn cmd_list(system: bool, user: bool, json_out: bool) {
|
||||
let mut sys: Vec<String> = vec![];
|
||||
let mut usr: Vec<String> = vec![];
|
||||
if system || (!system && !user) {
|
||||
sys = core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e));
|
||||
}
|
||||
if user || (!system && !user) {
|
||||
usr = core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e));
|
||||
}
|
||||
if json_out {
|
||||
let output = json!({ "system": { "paths": sys, "count": sys.len() }, "user": { "paths": usr, "count": usr.len() } });
|
||||
println!("{}", serde_json::to_string_pretty(&output).unwrap());
|
||||
} else {
|
||||
if !sys.is_empty() {
|
||||
println!("═══ 系统 PATH ({}) ═══", sys.len());
|
||||
for (i, p) in sys.iter().enumerate() { println!(" [{}] {}", i, p); }
|
||||
}
|
||||
if !usr.is_empty() {
|
||||
println!("═══ 用户 PATH ({}) ═══", usr.len());
|
||||
for (i, p) in usr.iter().enumerate() { println!(" [{}] {}", i, p); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_add(path: String, system: bool, user: bool) {
|
||||
let target = pick_target(system, user);
|
||||
let (mut list, save_fn): (Vec<String>, _) = if target == "system" {
|
||||
(core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e)),
|
||||
core::registry::save_system_paths as fn(Vec<String>) -> Result<(), String>)
|
||||
} else {
|
||||
(core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e)),
|
||||
core::registry::save_user_paths as fn(Vec<String>) -> Result<(), String>)
|
||||
};
|
||||
list.push(path.clone());
|
||||
save_fn(list).unwrap_or_else(|e| exit_err(&e));
|
||||
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<String>, _) = if target == "system" {
|
||||
(core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e)),
|
||||
core::registry::save_system_paths as fn(Vec<String>) -> Result<(), String>)
|
||||
} else {
|
||||
(core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e)),
|
||||
core::registry::save_user_paths as fn(Vec<String>) -> Result<(), String>)
|
||||
};
|
||||
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));
|
||||
println!("已删除: {removed}");
|
||||
}
|
||||
|
||||
fn cmd_conflicts(json_out: bool) {
|
||||
let mut paths: Vec<String> = vec![];
|
||||
if let Ok(sys) = core::registry::load_system_paths() { paths.extend(sys); }
|
||||
if let Ok(usr) = core::registry::load_user_paths() { paths.extend(usr); }
|
||||
let conflicts = core::scanner::scan_conflicts(paths).unwrap_or_else(|e| exit_err(&e));
|
||||
if json_out {
|
||||
println!("{}", serde_json::to_string_pretty(&conflicts).unwrap());
|
||||
} else if conflicts.is_empty() {
|
||||
println!("未发现可执行文件冲突。");
|
||||
} else {
|
||||
println!("═══ 可执行文件冲突({} 个)═══\n", conflicts.len());
|
||||
for c in &conflicts {
|
||||
println!(" {}", c.name);
|
||||
for loc in &c.locations {
|
||||
println!(" {} {}", if loc.priority == 0 { "✓ 优先" } else { "✗ 遮蔽" }, loc.dir);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_scan(query: Option<String>, json_out: bool) {
|
||||
let mut paths: Vec<String> = vec![];
|
||||
if let Ok(sys) = core::registry::load_system_paths() { paths.extend(sys); }
|
||||
if let Ok(usr) = core::registry::load_user_paths() { paths.extend(usr); }
|
||||
let groups = core::scanner::scan_tools(paths, query.unwrap_or_default()).unwrap_or_else(|e| exit_err(&e));
|
||||
if json_out {
|
||||
println!("{}", serde_json::to_string_pretty(&groups).unwrap());
|
||||
} else {
|
||||
for g in &groups {
|
||||
if !g.exists { println!(" {} (不存在)", g.dir); continue; }
|
||||
println!("═══ {} ═══", g.dir);
|
||||
for exe in &g.exes { println!(" {}", exe); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_check_admin(json_out: bool) {
|
||||
let is_admin = core::system::check_admin();
|
||||
if json_out {
|
||||
println!("{}", json!({"admin": is_admin}));
|
||||
} else {
|
||||
println!("管理员权限: {}", if is_admin { "是" } else { "否" });
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_list(json_out: bool) {
|
||||
let list = core::profiles::list_profiles().unwrap_or_else(|e| exit_err(&e));
|
||||
if json_out {
|
||||
println!("{}", serde_json::to_string_pretty(&list).unwrap());
|
||||
} else if list.is_empty() {
|
||||
println!("暂无配置文件。");
|
||||
} else {
|
||||
for p in &list { println!(" {} ({})", p.name, p.modified); }
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_save(name: String) {
|
||||
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 sys_entries = sys.into_iter().map(|p| core::profiles::ProfilePathEntry { path: p, enabled: true }).collect();
|
||||
let usr_entries = usr.into_iter().map(|p| core::profiles::ProfilePathEntry { path: p, enabled: true }).collect();
|
||||
core::profiles::save_profile(name.clone(), sys_entries, usr_entries).unwrap_or_else(|e| exit_err(&e));
|
||||
println!("已保存配置: {name}");
|
||||
}
|
||||
|
||||
fn profile_load(name: String) {
|
||||
let data = core::profiles::load_profile(name.clone()).unwrap_or_else(|e| exit_err(&e));
|
||||
println!("═══ 系统 PATH ({} 条) ═══", data.sys.len());
|
||||
for e in &data.sys { println!(" [{}] {}", if e.enabled { "✓" } else { "✗" }, e.path); }
|
||||
println!("═══ 用户 PATH ({} 条) ═══", data.user.len());
|
||||
for e in &data.user { println!(" [{}] {}", if e.enabled { "✓" } else { "✗" }, e.path); }
|
||||
}
|
||||
|
||||
fn profile_apply(name: String) {
|
||||
let data = core::profiles::load_profile(name.clone()).unwrap_or_else(|e| exit_err(&e));
|
||||
let sys: Vec<String> = data.sys.into_iter().filter(|e| e.enabled).map(|e| e.path).collect();
|
||||
let usr: Vec<String> = data.user.into_iter().filter(|e| e.enabled).map(|e| e.path).collect();
|
||||
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));
|
||||
core::system::broadcast_env_change();
|
||||
println!("配置文件 \"{name}\" 已写入注册表。");
|
||||
}
|
||||
|
||||
fn profile_delete(name: String) {
|
||||
core::profiles::delete_profile(name.clone()).unwrap_or_else(|e| exit_err(&e));
|
||||
println!("已删除配置: {name}");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
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::Conflicts { json } => cmd_conflicts(json),
|
||||
Command::Scan { query, json } => cmd_scan(query, json),
|
||||
Command::CheckAdmin { json } => cmd_check_admin(json),
|
||||
Command::Profile(cmd) => match cmd {
|
||||
ProfileCmd::List { json } => profile_list(json),
|
||||
ProfileCmd::Save { name } => profile_save(name),
|
||||
ProfileCmd::Load { name } => profile_load(name),
|
||||
ProfileCmd::Apply { name } => profile_apply(name),
|
||||
ProfileCmd::Delete { name } => profile_delete(name),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "path-editor-core"
|
||||
version = "5.0.0"
|
||||
description = "PathEditor core library — shared between GUI and CLI"
|
||||
authors = ["刘航宇"]
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
log = "0.4"
|
||||
winreg = "0.52"
|
||||
dirs = "5"
|
||||
chrono = "0.4"
|
||||
@@ -0,0 +1,68 @@
|
||||
use chrono::Local;
|
||||
use std::path::PathBuf;
|
||||
use winreg::enums::*;
|
||||
use crate::registry::{self, SYS_REG_PATH, USER_REG_PATH};
|
||||
|
||||
fn backup_base_dir() -> PathBuf {
|
||||
dirs::data_dir()
|
||||
.or_else(dirs::home_dir)
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("PathEditor")
|
||||
.join("backups")
|
||||
}
|
||||
|
||||
/// 获取 APPDATA 路径下的备份目录
|
||||
|
||||
pub fn get_appdata_dir() -> String {
|
||||
backup_base_dir().to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
/// 备份当前注册表中的系统 PATH 和用户 PATH
|
||||
/// 在保存前调用,备份的是注册表中的当前值(保存前的状态)
|
||||
|
||||
pub fn backup_registry(custom_dir: Option<String>) -> Result<String, String> {
|
||||
let backup_dir = match custom_dir {
|
||||
Some(ref dir) if !dir.is_empty() => std::path::PathBuf::from(dir),
|
||||
_ => backup_base_dir(),
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(&backup_dir)
|
||||
.map_err(|e| format!("无法创建备份目录: {}", e))?;
|
||||
|
||||
// 读取当前注册表中的值(保存前的旧值)
|
||||
let sys_paths = registry::load_paths(
|
||||
HKEY_LOCAL_MACHINE,
|
||||
SYS_REG_PATH,
|
||||
"系统",
|
||||
)?;
|
||||
let user_paths = registry::load_paths(
|
||||
HKEY_CURRENT_USER,
|
||||
USER_REG_PATH,
|
||||
"用户",
|
||||
)?;
|
||||
|
||||
let timestamp = Local::now().format("%Y%m%d_%H%M%S_%3f");
|
||||
let filename = format!("path_backup_{}.txt", timestamp);
|
||||
let filepath = backup_dir.join(&filename);
|
||||
|
||||
let mut content = String::new();
|
||||
content.push_str(&format!(
|
||||
"PathEditor Backup - {}\n",
|
||||
Local::now().format("%Y-%m-%d %H:%M:%S")
|
||||
));
|
||||
content.push_str("\n[System PATH]\n");
|
||||
for path in &sys_paths {
|
||||
content.push_str(&format!("{}\n", path));
|
||||
}
|
||||
content.push_str("\n[User PATH]\n");
|
||||
for path in &user_paths {
|
||||
content.push_str(&format!("{}\n", path));
|
||||
}
|
||||
|
||||
std::fs::write(&filepath, &content)
|
||||
.map_err(|e| format!("无法写入备份文件: {}", e))?;
|
||||
|
||||
let result = filepath.to_string_lossy().to_string();
|
||||
log::info!("备份已保存到: {}", result);
|
||||
Ok(result)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn disabled_file_path() -> PathBuf {
|
||||
dirs::data_dir()
|
||||
.or_else(dirs::home_dir)
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("PathEditor")
|
||||
.join("disabled.json")
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
struct DisabledState {
|
||||
#[serde(default)]
|
||||
system: Vec<String>,
|
||||
#[serde(default)]
|
||||
user: Vec<String>,
|
||||
}
|
||||
|
||||
/// 保存禁用路径列表(即时持久化,不依赖注册表保存按钮)
|
||||
|
||||
pub fn save_disabled_state(system: Vec<String>, user: Vec<String>) -> Result<(), String> {
|
||||
let state = DisabledState { system, user };
|
||||
let path = disabled_file_path();
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("无法创建配置目录: {}", e))?;
|
||||
}
|
||||
|
||||
let json = serde_json::to_string_pretty(&state)
|
||||
.map_err(|e| format!("JSON 序列化失败: {}", e))?;
|
||||
|
||||
fs::write(&path, &json)
|
||||
.map_err(|e| format!("无法写入 disabled.json: {}", e))?;
|
||||
|
||||
log::info!("已保存禁用状态到: {}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 加载禁用路径列表,返回 (system_disabled, user_disabled)
|
||||
|
||||
pub fn load_disabled_state() -> Result<(Vec<String>, Vec<String>), String> {
|
||||
let path = disabled_file_path();
|
||||
|
||||
if !path.exists() {
|
||||
return Ok((vec![], vec![]));
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&path)
|
||||
.map_err(|e| format!("无法读取 disabled.json: {}", e))?;
|
||||
|
||||
if content.trim().is_empty() {
|
||||
return Ok((vec![], vec![]));
|
||||
}
|
||||
|
||||
let state: DisabledState = serde_json::from_str(&content)
|
||||
.map_err(|e| format!("JSON 解析失败: {}", e))?;
|
||||
|
||||
Ok((state.system, state.user))
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/// 读取文本文件内容(供前端原生对话框选择文件后使用)
|
||||
|
||||
pub fn read_text_file(path: &str) -> Result<String, String> {
|
||||
std::fs::read_to_string(path).map_err(|e| format!("无法读取文件: {}", e))
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
pub mod backup;
|
||||
pub mod disabled;
|
||||
pub mod fs;
|
||||
pub mod profiles;
|
||||
pub mod registry;
|
||||
pub mod scanner;
|
||||
pub mod system;
|
||||
|
||||
pub use profiles::{ProfileData, ProfileMeta};
|
||||
pub use scanner::{ConflictEntry, ConflictLocation, ToolGroup};
|
||||
@@ -0,0 +1,146 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn profiles_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".patheditor")
|
||||
.join("profiles")
|
||||
}
|
||||
|
||||
fn profile_path(name: &str) -> PathBuf {
|
||||
profiles_dir().join(format!("{}.json", name))
|
||||
}
|
||||
|
||||
/// 内部用的 PathEntry(与前端 PathEntry 字段一致)
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct ProfilePathEntry {
|
||||
pub path: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ProfileMeta {
|
||||
pub name: String,
|
||||
pub created: String,
|
||||
pub modified: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ProfileData {
|
||||
pub name: String,
|
||||
pub sys: Vec<ProfilePathEntry>,
|
||||
pub user: Vec<ProfilePathEntry>,
|
||||
pub created: String,
|
||||
pub modified: String,
|
||||
}
|
||||
|
||||
/// 列出所有配置文件的元数据
|
||||
|
||||
pub fn list_profiles() -> Result<Vec<ProfileMeta>, String> {
|
||||
let dir = profiles_dir();
|
||||
if !dir.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut profiles: Vec<ProfileMeta> = Vec::new();
|
||||
let entries = fs::read_dir(&dir).map_err(|e| format!("无法读取配置目录: {}", e))?;
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().map_or(true, |e| e != "json") {
|
||||
continue;
|
||||
}
|
||||
let content = fs::read_to_string(&path)
|
||||
.map_err(|e| format!("无法读取 {}: {}", path.display(), e))?;
|
||||
if let Ok(data) = serde_json::from_str::<ProfileData>(&content) {
|
||||
profiles.push(ProfileMeta {
|
||||
name: data.name,
|
||||
created: data.created,
|
||||
modified: data.modified,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
profiles.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(profiles)
|
||||
}
|
||||
|
||||
/// 保存当前 PATH 为配置文件
|
||||
|
||||
pub fn save_profile(
|
||||
name: String,
|
||||
sys: Vec<ProfilePathEntry>,
|
||||
user: Vec<ProfilePathEntry>,
|
||||
) -> Result<(), String> {
|
||||
let dir = profiles_dir();
|
||||
fs::create_dir_all(&dir).map_err(|e| format!("无法创建配置目录: {}", e))?;
|
||||
|
||||
let path = profile_path(&name);
|
||||
let now = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string();
|
||||
|
||||
let data = ProfileData {
|
||||
name,
|
||||
sys,
|
||||
user,
|
||||
created: now.clone(),
|
||||
modified: now,
|
||||
};
|
||||
|
||||
let json =
|
||||
serde_json::to_string_pretty(&data).map_err(|e| format!("JSON 序列化失败: {}", e))?;
|
||||
fs::write(&path, &json).map_err(|e| format!("无法写入配置文件: {}", e))?;
|
||||
|
||||
log::info!("已保存配置: {}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 加载配置文件
|
||||
|
||||
pub fn load_profile(name: String) -> Result<ProfileData, String> {
|
||||
let path = profile_path(&name);
|
||||
if !path.exists() {
|
||||
return Err(format!("配置文件不存在: {}", name));
|
||||
}
|
||||
let content = fs::read_to_string(&path)
|
||||
.map_err(|e| format!("无法读取配置文件: {}", e))?;
|
||||
serde_json::from_str(&content)
|
||||
.map_err(|e| format!("JSON 解析失败: {}", e))
|
||||
}
|
||||
|
||||
/// 删除配置文件
|
||||
|
||||
pub fn delete_profile(name: String) -> Result<(), String> {
|
||||
let path = profile_path(&name);
|
||||
fs::remove_file(&path).map_err(|e| format!("无法删除配置文件: {}", e))?;
|
||||
log::info!("已删除配置: {}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 重命名配置文件
|
||||
|
||||
pub fn rename_profile(old_name: String, new_name: String) -> Result<(), String> {
|
||||
let old_path = profile_path(&old_name);
|
||||
if !old_path.exists() {
|
||||
return Err(format!("配置文件不存在: {}", old_name));
|
||||
}
|
||||
|
||||
let mut data: ProfileData =
|
||||
serde_json::from_str(&fs::read_to_string(&old_path).map_err(|e| format!("无法读取配置文件: {}", e))?).map_err(|e| format!("JSON 解析失败: {}", e))?;
|
||||
|
||||
data.name = new_name.clone();
|
||||
data.modified = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string();
|
||||
|
||||
let new_path = profile_path(&new_name);
|
||||
let json =
|
||||
serde_json::to_string_pretty(&data).map_err(|e| format!("JSON 序列化失败: {}", e))?;
|
||||
fs::write(&new_path, &json).map_err(|e| format!("无法写入配置文件: {}", e))?;
|
||||
|
||||
if old_path != new_path {
|
||||
fs::remove_file(&old_path).map_err(|e| format!("无法删除旧配置文件: {}", e))?;
|
||||
}
|
||||
|
||||
log::info!("已重命名配置: {} -> {}", old_name, new_name);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
|
||||
pub(crate) const SYS_REG_PATH: &str = "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment";
|
||||
pub(crate) const USER_REG_PATH: &str = "Environment";
|
||||
const PATH_VALUE: &str = "Path";
|
||||
|
||||
pub(crate) fn load_paths(root: winreg::HKEY, sub_path: &str, label: &str) -> Result<Vec<String>, String> {
|
||||
let key = RegKey::predef(root);
|
||||
let env_key = key
|
||||
.open_subkey_with_flags(sub_path, KEY_READ)
|
||||
.map_err(|e| format!("无法打开{}注册表项: {}", label, e))?;
|
||||
|
||||
let value: String = env_key
|
||||
.get_value(PATH_VALUE)
|
||||
.map_err(|e| format!("无法读取{} PATH: {}", label, e))?;
|
||||
|
||||
Ok(split_path(&value))
|
||||
}
|
||||
|
||||
fn save_paths(root: winreg::HKEY, sub_path: &str, label: &str, paths: &[String]) -> Result<(), String> {
|
||||
let value = join_path(paths);
|
||||
|
||||
// Windows 注册表 REG_EXPAND_SZ 上限 32767 字符
|
||||
const MAX_PATH_LEN: usize = 32767;
|
||||
if value.len() > MAX_PATH_LEN {
|
||||
return Err(format!(
|
||||
"{} PATH 总长度 {} 超出 Windows 限制 {} 字符,请移除部分路径后再保存",
|
||||
label, value.len(), MAX_PATH_LEN
|
||||
));
|
||||
}
|
||||
|
||||
let key = RegKey::predef(root);
|
||||
let env_key = key
|
||||
.open_subkey_with_flags(sub_path, KEY_WRITE)
|
||||
.map_err(|e| format!("无法写入{}注册表(需要管理员权限): {}", label, e))?;
|
||||
|
||||
env_key
|
||||
.set_value(PATH_VALUE, &value)
|
||||
.map_err(|e| format!("无法写入{} PATH: {}", label, e))?;
|
||||
|
||||
log::info!("已保存{} PATH,{} 个条目", label, paths.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn load_system_paths() -> Result<Vec<String>, String> {
|
||||
load_paths(HKEY_LOCAL_MACHINE, SYS_REG_PATH, "系统")
|
||||
}
|
||||
|
||||
|
||||
pub fn load_user_paths() -> Result<Vec<String>, String> {
|
||||
load_paths(HKEY_CURRENT_USER, USER_REG_PATH, "用户")
|
||||
}
|
||||
|
||||
|
||||
pub fn save_system_paths(paths: Vec<String>) -> Result<(), String> {
|
||||
save_paths(HKEY_LOCAL_MACHINE, SYS_REG_PATH, "系统", &paths)
|
||||
}
|
||||
|
||||
|
||||
pub fn save_user_paths(paths: Vec<String>) -> Result<(), String> {
|
||||
save_paths(HKEY_CURRENT_USER, USER_REG_PATH, "用户", &paths)
|
||||
}
|
||||
|
||||
/// 将分号分隔的 PATH 字符串拆分为数组。
|
||||
/// 注意:TS 端 src/core/validation.ts 有相同逻辑的 split_path,修改时需同步两端。
|
||||
fn split_path(raw: &str) -> Vec<String> {
|
||||
raw.split(';')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn join_path(paths: &[String]) -> String {
|
||||
paths
|
||||
.iter()
|
||||
.map(|p| p.trim())
|
||||
.filter(|p| !p.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(";")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn split_empty() {
|
||||
assert_eq!(split_path(""), Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_single() {
|
||||
assert_eq!(split_path("C:\\Windows"), vec!["C:\\Windows"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_multiple() {
|
||||
assert_eq!(
|
||||
split_path("C:\\Windows;D:\\Projects"),
|
||||
vec!["C:\\Windows", "D:\\Projects"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_trims_and_filters_empty() {
|
||||
assert_eq!(
|
||||
split_path(" C:\\ ; ; D:\\ "),
|
||||
vec!["C:\\", "D:\\"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_and_split_roundtrip() {
|
||||
let paths = vec!["C:\\Windows".to_string(), "D:\\Projects".to_string()];
|
||||
let joined = join_path(&paths);
|
||||
let split = split_path(&joined);
|
||||
assert_eq!(split, paths);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_trims_entries() {
|
||||
let paths = vec![" C:\\Windows ".to_string(), " D:\\ ".to_string()];
|
||||
assert_eq!(join_path(&paths), "C:\\Windows;D:\\");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
const EXECUTABLE_EXTENSIONS: &[&str] = &["exe", "bat", "cmd", "com", "ps1"];
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
pub struct ConflictLocation {
|
||||
pub dir: String,
|
||||
pub priority: usize,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
pub struct ConflictEntry {
|
||||
pub name: String,
|
||||
pub locations: Vec<ConflictLocation>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct ToolGroup {
|
||||
pub dir: String,
|
||||
pub exists: bool,
|
||||
pub exes: Vec<String>,
|
||||
}
|
||||
|
||||
/// 扫描 PATH 中的可执行文件冲突
|
||||
///
|
||||
/// 遍历每个 PATH 目录,查找 .exe/.bat/.cmd/.com/.ps1 文件,
|
||||
/// 标记出现在多个目录中的同名文件(后面的目录会被前面的「遮蔽」)
|
||||
|
||||
pub fn scan_conflicts(paths: Vec<String>) -> Result<Vec<ConflictEntry>, String> {
|
||||
// exe_name (小写) → [(priority, dir)]
|
||||
let mut map: HashMap<String, Vec<(usize, String)>> = HashMap::new();
|
||||
|
||||
for (priority, dir) in paths.iter().enumerate() {
|
||||
let p = Path::new(dir);
|
||||
if !p.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let entries = fs::read_dir(p).map_err(|e| format!("无法读取目录 {}: {}", dir, e))?;
|
||||
for entry in entries.flatten() {
|
||||
let fname = entry.file_name();
|
||||
let name = fname.to_string_lossy();
|
||||
if let Some(ext) = Path::new(name.as_ref()).extension() {
|
||||
let ext_lower = ext.to_ascii_lowercase();
|
||||
if EXECUTABLE_EXTENSIONS.contains(&ext_lower.to_str().unwrap_or("")) {
|
||||
let key = name.to_lowercase();
|
||||
map.entry(key).or_default().push((priority, dir.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut results: Vec<ConflictEntry> = map
|
||||
.into_iter()
|
||||
.filter(|(_, locs)| locs.len() >= 2)
|
||||
.map(|(name, locs)| ConflictEntry {
|
||||
name,
|
||||
locations: locs
|
||||
.into_iter()
|
||||
.map(|(priority, dir)| ConflictLocation { dir, priority })
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
results.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// 扫描 PATH 中各目录提供的可执行文件
|
||||
///
|
||||
/// query 非空时只返回文件名包含关键词的结果
|
||||
|
||||
pub fn scan_tools(paths: Vec<String>, query: String) -> Result<Vec<ToolGroup>, String> {
|
||||
let query_lower = query.to_lowercase();
|
||||
let mut groups: Vec<ToolGroup> = Vec::new();
|
||||
|
||||
for dir in &paths {
|
||||
let p = Path::new(dir);
|
||||
if !p.is_dir() {
|
||||
groups.push(ToolGroup {
|
||||
dir: dir.clone(),
|
||||
exists: false,
|
||||
exes: vec![],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let entries = fs::read_dir(p).map_err(|e| format!("无法读取目录 {}: {}", dir, e))?;
|
||||
let mut exes: Vec<String> = Vec::new();
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let fname = entry.file_name();
|
||||
let name = fname.to_string_lossy();
|
||||
if let Some(ext) = Path::new(name.as_ref()).extension() {
|
||||
let ext_lower = ext.to_ascii_lowercase();
|
||||
if EXECUTABLE_EXTENSIONS.contains(&ext_lower.to_str().unwrap_or("")) {
|
||||
if query_lower.is_empty() || name.to_lowercase().contains(&query_lower) {
|
||||
exes.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exes.sort();
|
||||
groups.push(ToolGroup {
|
||||
dir: dir.clone(),
|
||||
exists: true,
|
||||
exes,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
|
||||
/// 检测当前进程是否有管理员权限(尝试写入系统注册表键)
|
||||
|
||||
pub fn check_admin() -> bool {
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
hklm.open_subkey_with_flags(
|
||||
"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment",
|
||||
KEY_WRITE,
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// 验证路径是否存在于文件系统中(且是目录)
|
||||
/// 包含 % 的路径(环境变量路径)无法验证,返回 true
|
||||
|
||||
pub fn validate_path(path: &str) -> bool {
|
||||
if path.contains('%') {
|
||||
return true;
|
||||
}
|
||||
std::fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 展开路径中的环境变量(如 %JAVA_HOME%\bin → C:\Program Files\Java\jdk-17\bin)
|
||||
|
||||
pub fn expand_env_vars(path: &str) -> String {
|
||||
if !path.contains('%') {
|
||||
return path.to_string();
|
||||
}
|
||||
|
||||
// 转为 UTF-16 宽字符串(以 null 结尾)
|
||||
let wide_path: Vec<u16> = path
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
|
||||
// SAFETY: wide_path 是以 null 结尾的 UTF-16 字符串,lpDst 为 null 且 nSize 为 0,
|
||||
// 根据 MSDN 文档此时 API 只查询所需缓冲区大小而不写入数据
|
||||
let required = unsafe {
|
||||
ExpandEnvironmentStringsW(wide_path.as_ptr(), std::ptr::null_mut(), 0)
|
||||
};
|
||||
|
||||
if required == 0 {
|
||||
log::warn!("expand_env_vars: API 查询缓冲区失败, 返回原始路径: {path}");
|
||||
return path.to_string();
|
||||
}
|
||||
|
||||
// SAFETY: buffer 容量为 required(API 返回的精确大小),wide_path 以 null 结尾,
|
||||
// 且两个指针指向不同的内存区域,不存在重叠
|
||||
let mut buffer: Vec<u16> = vec![0; required as usize];
|
||||
let result = unsafe {
|
||||
ExpandEnvironmentStringsW(wide_path.as_ptr(), buffer.as_mut_ptr(), required)
|
||||
};
|
||||
|
||||
if result == 0 || result > required {
|
||||
log::warn!("expand_env_vars: 展开失败或缓冲区不足, 返回原始路径: {path}");
|
||||
return path.to_string();
|
||||
}
|
||||
|
||||
// 转回 UTF-8 (去掉结尾 null)
|
||||
let len = buffer.iter().position(|&c| c == 0).unwrap_or(buffer.len());
|
||||
String::from_utf16_lossy(&buffer[..len])
|
||||
}
|
||||
|
||||
/// 广播环境变量更改通知(WM_SETTINGCHANGE)
|
||||
|
||||
pub fn broadcast_env_change() {
|
||||
const HWND_BROADCAST: isize = 0xFFFF;
|
||||
const WM_SETTINGCHANGE: u32 = 0x001A;
|
||||
const SMTO_ABORTIFHUNG: u32 = 0x0002;
|
||||
|
||||
// SAFETY: env_str 是以 null 结尾的 UTF-16 字符串,所有指针和常量均遵循 Win32 API 约定
|
||||
let env_str: Vec<u16> = "Environment\0".encode_utf16().collect();
|
||||
|
||||
// SAFETY: env_str.as_ptr() 指向以 null 结尾的字符串,HWND_BROADCAST 是合法句柄,
|
||||
// lpdwResult 为 null 表示不需要返回值,其他参数均为常量
|
||||
let result = unsafe {
|
||||
SendMessageTimeoutW(
|
||||
HWND_BROADCAST,
|
||||
WM_SETTINGCHANGE,
|
||||
0,
|
||||
env_str.as_ptr() as isize,
|
||||
SMTO_ABORTIFHUNG,
|
||||
5000,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
if result == 0 {
|
||||
log::warn!("广播 WM_SETTINGCHANGE 失败");
|
||||
} else {
|
||||
log::info!("已广播环境变量更改通知");
|
||||
}
|
||||
}
|
||||
|
||||
// ── 外部 FFI 声明 ──
|
||||
|
||||
extern "system" {
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-expandenvironmentstringsw
|
||||
fn ExpandEnvironmentStringsW(
|
||||
lpSrc: *const u16,
|
||||
lpDst: *mut u16,
|
||||
nSize: u32,
|
||||
) -> u32;
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessagetimeoutw
|
||||
fn SendMessageTimeoutW(
|
||||
hWnd: isize,
|
||||
Msg: u32,
|
||||
wParam: usize,
|
||||
lParam: isize,
|
||||
fuFlags: u32,
|
||||
uTimeout: u32,
|
||||
lpdwResult: *mut usize,
|
||||
) -> isize;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn validate_path_env_var_always_valid() {
|
||||
assert!(validate_path("%JAVA_HOME%\\bin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_env_vars_no_percent_returns_original() {
|
||||
let result = expand_env_vars("C:\\Windows");
|
||||
assert_eq!(result, "C:\\Windows");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_env_vars_with_invalid_var_returns_original() {
|
||||
// 展开不存在的变量可能会回归原始值或产生部分展开;测试是否不会崩溃
|
||||
let result = expand_env_vars("%__NONEXISTENT_VAR__%");
|
||||
// 至少不应为空白
|
||||
assert!(!result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_admin_returns_bool() {
|
||||
let result = check_admin();
|
||||
// 在任意机器上应返回 true 或 false,不应 panic
|
||||
assert!((result == true) || (result == false));
|
||||
}
|
||||
}
|
||||
Generated
+1
-1
@@ -2271,7 +2271,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "patheditor"
|
||||
version = "4.0.0"
|
||||
version = "5.0.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs 5.0.1",
|
||||
|
||||
@@ -16,14 +16,10 @@ crate-type = ["staticlib", "rlib"]
|
||||
tauri-build = { version = "2.6.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
path-editor-core = { path = "../core" }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4"
|
||||
tauri = { version = "2.11.2", features = [] }
|
||||
tauri-plugin-log = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
|
||||
# Windows API
|
||||
winreg = "0.52"
|
||||
dirs = "5"
|
||||
chrono = "0.4"
|
||||
|
||||
@@ -1,68 +1,6 @@
|
||||
use chrono::Local;
|
||||
use std::path::PathBuf;
|
||||
use winreg::enums::*;
|
||||
use crate::commands::registry::{self, SYS_REG_PATH, USER_REG_PATH};
|
||||
use path_editor_core::backup;
|
||||
|
||||
fn backup_base_dir() -> PathBuf {
|
||||
dirs::data_dir()
|
||||
.or_else(dirs::home_dir)
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("PathEditor")
|
||||
.join("backups")
|
||||
}
|
||||
|
||||
/// 获取 APPDATA 路径下的备份目录
|
||||
#[tauri::command]
|
||||
pub fn get_appdata_dir() -> String {
|
||||
backup_base_dir().to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
/// 备份当前注册表中的系统 PATH 和用户 PATH
|
||||
/// 在保存前调用,备份的是注册表中的当前值(保存前的状态)
|
||||
pub fn backup_registry(custom_dir: Option<String>) -> Result<String, String> { backup::backup_registry(custom_dir) }
|
||||
#[tauri::command]
|
||||
pub fn backup_registry(custom_dir: Option<String>) -> Result<String, String> {
|
||||
let backup_dir = match custom_dir {
|
||||
Some(ref dir) if !dir.is_empty() => std::path::PathBuf::from(dir),
|
||||
_ => backup_base_dir(),
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(&backup_dir)
|
||||
.map_err(|e| format!("无法创建备份目录: {}", e))?;
|
||||
|
||||
// 读取当前注册表中的值(保存前的旧值)
|
||||
let sys_paths = registry::load_paths(
|
||||
HKEY_LOCAL_MACHINE,
|
||||
SYS_REG_PATH,
|
||||
"系统",
|
||||
)?;
|
||||
let user_paths = registry::load_paths(
|
||||
HKEY_CURRENT_USER,
|
||||
USER_REG_PATH,
|
||||
"用户",
|
||||
)?;
|
||||
|
||||
let timestamp = Local::now().format("%Y%m%d_%H%M%S_%3f");
|
||||
let filename = format!("path_backup_{}.txt", timestamp);
|
||||
let filepath = backup_dir.join(&filename);
|
||||
|
||||
let mut content = String::new();
|
||||
content.push_str(&format!(
|
||||
"PathEditor Backup - {}\n",
|
||||
Local::now().format("%Y-%m-%d %H:%M:%S")
|
||||
));
|
||||
content.push_str("\n[System PATH]\n");
|
||||
for path in &sys_paths {
|
||||
content.push_str(&format!("{}\n", path));
|
||||
}
|
||||
content.push_str("\n[User PATH]\n");
|
||||
for path in &user_paths {
|
||||
content.push_str(&format!("{}\n", path));
|
||||
}
|
||||
|
||||
std::fs::write(&filepath, &content)
|
||||
.map_err(|e| format!("无法写入备份文件: {}", e))?;
|
||||
|
||||
let result = filepath.to_string_lossy().to_string();
|
||||
log::info!("备份已保存到: {}", result);
|
||||
Ok(result)
|
||||
}
|
||||
pub fn get_appdata_dir() -> String { backup::get_appdata_dir() }
|
||||
|
||||
@@ -1,62 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use path_editor_core::disabled;
|
||||
|
||||
fn disabled_file_path() -> PathBuf {
|
||||
dirs::data_dir()
|
||||
.or_else(dirs::home_dir)
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("PathEditor")
|
||||
.join("disabled.json")
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
struct DisabledState {
|
||||
#[serde(default)]
|
||||
system: Vec<String>,
|
||||
#[serde(default)]
|
||||
user: Vec<String>,
|
||||
}
|
||||
|
||||
/// 保存禁用路径列表(即时持久化,不依赖注册表保存按钮)
|
||||
#[tauri::command]
|
||||
pub fn save_disabled_state(system: Vec<String>, user: Vec<String>) -> Result<(), String> {
|
||||
let state = DisabledState { system, user };
|
||||
let path = disabled_file_path();
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("无法创建配置目录: {}", e))?;
|
||||
}
|
||||
|
||||
let json = serde_json::to_string_pretty(&state)
|
||||
.map_err(|e| format!("JSON 序列化失败: {}", e))?;
|
||||
|
||||
fs::write(&path, &json)
|
||||
.map_err(|e| format!("无法写入 disabled.json: {}", e))?;
|
||||
|
||||
log::info!("已保存禁用状态到: {}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 加载禁用路径列表,返回 (system_disabled, user_disabled)
|
||||
pub fn save_disabled_state(system: Vec<String>, user: Vec<String>) -> Result<(), String> { disabled::save_disabled_state(system, user) }
|
||||
#[tauri::command]
|
||||
pub fn load_disabled_state() -> Result<(Vec<String>, Vec<String>), String> {
|
||||
let path = disabled_file_path();
|
||||
|
||||
if !path.exists() {
|
||||
return Ok((vec![], vec![]));
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&path)
|
||||
.map_err(|e| format!("无法读取 disabled.json: {}", e))?;
|
||||
|
||||
if content.trim().is_empty() {
|
||||
return Ok((vec![], vec![]));
|
||||
}
|
||||
|
||||
let state: DisabledState = serde_json::from_str(&content)
|
||||
.map_err(|e| format!("JSON 解析失败: {}", e))?;
|
||||
|
||||
Ok((state.system, state.user))
|
||||
}
|
||||
pub fn load_disabled_state() -> Result<(Vec<String>, Vec<String>), String> { disabled::load_disabled_state() }
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/// 读取文本文件内容(供前端原生对话框选择文件后使用)
|
||||
use path_editor_core::fs;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn read_text_file(path: &str) -> Result<String, String> {
|
||||
std::fs::read_to_string(path).map_err(|e| format!("无法读取文件: {}", e))
|
||||
}
|
||||
pub fn read_text_file(path: &str) -> Result<String, String> { fs::read_text_file(path) }
|
||||
|
||||
@@ -1,146 +1,12 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use path_editor_core::profiles;
|
||||
|
||||
fn profiles_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".patheditor")
|
||||
.join("profiles")
|
||||
}
|
||||
|
||||
fn profile_path(name: &str) -> PathBuf {
|
||||
profiles_dir().join(format!("{}.json", name))
|
||||
}
|
||||
|
||||
/// 内部用的 PathEntry(与前端 PathEntry 字段一致)
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct ProfilePathEntry {
|
||||
pub path: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ProfileMeta {
|
||||
pub name: String,
|
||||
pub created: String,
|
||||
pub modified: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ProfileData {
|
||||
pub name: String,
|
||||
pub sys: Vec<ProfilePathEntry>,
|
||||
pub user: Vec<ProfilePathEntry>,
|
||||
pub created: String,
|
||||
pub modified: String,
|
||||
}
|
||||
|
||||
/// 列出所有配置文件的元数据
|
||||
#[tauri::command]
|
||||
pub fn list_profiles() -> Result<Vec<ProfileMeta>, String> {
|
||||
let dir = profiles_dir();
|
||||
if !dir.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut profiles: Vec<ProfileMeta> = Vec::new();
|
||||
let entries = fs::read_dir(&dir).map_err(|e| format!("无法读取配置目录: {}", e))?;
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().map_or(true, |e| e != "json") {
|
||||
continue;
|
||||
}
|
||||
let content = fs::read_to_string(&path)
|
||||
.map_err(|e| format!("无法读取 {}: {}", path.display(), e))?;
|
||||
if let Ok(data) = serde_json::from_str::<ProfileData>(&content) {
|
||||
profiles.push(ProfileMeta {
|
||||
name: data.name,
|
||||
created: data.created,
|
||||
modified: data.modified,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
profiles.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(profiles)
|
||||
}
|
||||
|
||||
/// 保存当前 PATH 为配置文件
|
||||
pub fn list_profiles() -> Result<Vec<profiles::ProfileMeta>, String> { profiles::list_profiles() }
|
||||
#[tauri::command]
|
||||
pub fn save_profile(
|
||||
name: String,
|
||||
sys: Vec<ProfilePathEntry>,
|
||||
user: Vec<ProfilePathEntry>,
|
||||
) -> Result<(), String> {
|
||||
let dir = profiles_dir();
|
||||
fs::create_dir_all(&dir).map_err(|e| format!("无法创建配置目录: {}", e))?;
|
||||
|
||||
let path = profile_path(&name);
|
||||
let now = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string();
|
||||
|
||||
let data = ProfileData {
|
||||
name,
|
||||
sys,
|
||||
user,
|
||||
created: now.clone(),
|
||||
modified: now,
|
||||
};
|
||||
|
||||
let json =
|
||||
serde_json::to_string_pretty(&data).map_err(|e| format!("JSON 序列化失败: {}", e))?;
|
||||
fs::write(&path, &json).map_err(|e| format!("无法写入配置文件: {}", e))?;
|
||||
|
||||
log::info!("已保存配置: {}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 加载配置文件
|
||||
pub fn save_profile(name: String, sys: Vec<profiles::ProfilePathEntry>, user: Vec<profiles::ProfilePathEntry>) -> Result<(), String> { profiles::save_profile(name, sys, user) }
|
||||
#[tauri::command]
|
||||
pub fn load_profile(name: String) -> Result<ProfileData, String> {
|
||||
let path = profile_path(&name);
|
||||
if !path.exists() {
|
||||
return Err(format!("配置文件不存在: {}", name));
|
||||
}
|
||||
let content = fs::read_to_string(&path)
|
||||
.map_err(|e| format!("无法读取配置文件: {}", e))?;
|
||||
serde_json::from_str(&content)
|
||||
.map_err(|e| format!("JSON 解析失败: {}", e))
|
||||
}
|
||||
|
||||
/// 删除配置文件
|
||||
pub fn load_profile(name: String) -> Result<profiles::ProfileData, String> { profiles::load_profile(name) }
|
||||
#[tauri::command]
|
||||
pub fn delete_profile(name: String) -> Result<(), String> {
|
||||
let path = profile_path(&name);
|
||||
fs::remove_file(&path).map_err(|e| format!("无法删除配置文件: {}", e))?;
|
||||
log::info!("已删除配置: {}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 重命名配置文件
|
||||
pub fn delete_profile(name: String) -> Result<(), String> { profiles::delete_profile(name) }
|
||||
#[tauri::command]
|
||||
pub fn rename_profile(old_name: String, new_name: String) -> Result<(), String> {
|
||||
let old_path = profile_path(&old_name);
|
||||
if !old_path.exists() {
|
||||
return Err(format!("配置文件不存在: {}", old_name));
|
||||
}
|
||||
|
||||
let mut data: ProfileData =
|
||||
serde_json::from_str(&fs::read_to_string(&old_path).map_err(|e| format!("无法读取配置文件: {}", e))?).map_err(|e| format!("JSON 解析失败: {}", e))?;
|
||||
|
||||
data.name = new_name.clone();
|
||||
data.modified = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string();
|
||||
|
||||
let new_path = profile_path(&new_name);
|
||||
let json =
|
||||
serde_json::to_string_pretty(&data).map_err(|e| format!("JSON 序列化失败: {}", e))?;
|
||||
fs::write(&new_path, &json).map_err(|e| format!("无法写入配置文件: {}", e))?;
|
||||
|
||||
if old_path != new_path {
|
||||
fs::remove_file(&old_path).map_err(|e| format!("无法删除旧配置文件: {}", e))?;
|
||||
}
|
||||
|
||||
log::info!("已重命名配置: {} -> {}", old_name, new_name);
|
||||
Ok(())
|
||||
}
|
||||
pub fn rename_profile(old_name: String, new_name: String) -> Result<(), String> { profiles::rename_profile(old_name, new_name) }
|
||||
|
||||
@@ -1,127 +1,10 @@
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
|
||||
pub(crate) const SYS_REG_PATH: &str = "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment";
|
||||
pub(crate) const USER_REG_PATH: &str = "Environment";
|
||||
pub(crate) const PATH_VALUE: &str = "Path";
|
||||
|
||||
pub(crate) fn load_paths(root: winreg::HKEY, sub_path: &str, label: &str) -> Result<Vec<String>, String> {
|
||||
let key = RegKey::predef(root);
|
||||
let env_key = key
|
||||
.open_subkey_with_flags(sub_path, KEY_READ)
|
||||
.map_err(|e| format!("无法打开{}注册表项: {}", label, e))?;
|
||||
|
||||
let value: String = env_key
|
||||
.get_value(PATH_VALUE)
|
||||
.map_err(|e| format!("无法读取{} PATH: {}", label, e))?;
|
||||
|
||||
Ok(split_path(&value))
|
||||
}
|
||||
|
||||
pub(crate) fn save_paths(root: winreg::HKEY, sub_path: &str, label: &str, paths: &[String]) -> Result<(), String> {
|
||||
let value = join_path(paths);
|
||||
|
||||
// Windows 注册表 REG_EXPAND_SZ 上限 32767 字符
|
||||
const MAX_PATH_LEN: usize = 32767;
|
||||
if value.len() > MAX_PATH_LEN {
|
||||
return Err(format!(
|
||||
"{} PATH 总长度 {} 超出 Windows 限制 {} 字符,请移除部分路径后再保存",
|
||||
label, value.len(), MAX_PATH_LEN
|
||||
));
|
||||
}
|
||||
|
||||
let key = RegKey::predef(root);
|
||||
let env_key = key
|
||||
.open_subkey_with_flags(sub_path, KEY_WRITE)
|
||||
.map_err(|e| format!("无法写入{}注册表(需要管理员权限): {}", label, e))?;
|
||||
|
||||
env_key
|
||||
.set_value(PATH_VALUE, &value)
|
||||
.map_err(|e| format!("无法写入{} PATH: {}", label, e))?;
|
||||
|
||||
log::info!("已保存{} PATH,{} 个条目", label, paths.len());
|
||||
Ok(())
|
||||
}
|
||||
use path_editor_core::registry;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn load_system_paths() -> Result<Vec<String>, String> {
|
||||
load_paths(HKEY_LOCAL_MACHINE, SYS_REG_PATH, "系统")
|
||||
}
|
||||
|
||||
pub fn load_system_paths() -> Result<Vec<String>, String> { registry::load_system_paths() }
|
||||
#[tauri::command]
|
||||
pub fn load_user_paths() -> Result<Vec<String>, String> {
|
||||
load_paths(HKEY_CURRENT_USER, USER_REG_PATH, "用户")
|
||||
}
|
||||
|
||||
pub fn load_user_paths() -> Result<Vec<String>, String> { registry::load_user_paths() }
|
||||
#[tauri::command]
|
||||
pub fn save_system_paths(paths: Vec<String>) -> Result<(), String> {
|
||||
save_paths(HKEY_LOCAL_MACHINE, SYS_REG_PATH, "系统", &paths)
|
||||
}
|
||||
|
||||
pub fn save_system_paths(paths: Vec<String>) -> Result<(), String> { registry::save_system_paths(paths) }
|
||||
#[tauri::command]
|
||||
pub fn save_user_paths(paths: Vec<String>) -> Result<(), String> {
|
||||
save_paths(HKEY_CURRENT_USER, USER_REG_PATH, "用户", &paths)
|
||||
}
|
||||
|
||||
/// 将分号分隔的 PATH 字符串拆分为数组。
|
||||
/// 注意:TS 端 src/core/validation.ts 有相同逻辑的 split_path,修改时需同步两端。
|
||||
pub(crate) fn split_path(raw: &str) -> Vec<String> {
|
||||
raw.split(';')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn join_path(paths: &[String]) -> String {
|
||||
paths
|
||||
.iter()
|
||||
.map(|p| p.trim())
|
||||
.filter(|p| !p.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(";")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn split_empty() {
|
||||
assert_eq!(split_path(""), Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_single() {
|
||||
assert_eq!(split_path("C:\\Windows"), vec!["C:\\Windows"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_multiple() {
|
||||
assert_eq!(
|
||||
split_path("C:\\Windows;D:\\Projects"),
|
||||
vec!["C:\\Windows", "D:\\Projects"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_trims_and_filters_empty() {
|
||||
assert_eq!(
|
||||
split_path(" C:\\ ; ; D:\\ "),
|
||||
vec!["C:\\", "D:\\"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_and_split_roundtrip() {
|
||||
let paths = vec!["C:\\Windows".to_string(), "D:\\Projects".to_string()];
|
||||
let joined = join_path(&paths);
|
||||
let split = split_path(&joined);
|
||||
assert_eq!(split, paths);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_trims_entries() {
|
||||
let paths = vec![" C:\\Windows ".to_string(), " D:\\ ".to_string()];
|
||||
assert_eq!(join_path(&paths), "C:\\Windows;D:\\");
|
||||
}
|
||||
}
|
||||
pub fn save_user_paths(paths: Vec<String>) -> Result<(), String> { registry::save_user_paths(paths) }
|
||||
|
||||
@@ -1,114 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use path_editor_core::scanner;
|
||||
|
||||
const EXECUTABLE_EXTENSIONS: &[&str] = &["exe", "bat", "cmd", "com", "ps1"];
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
pub struct ConflictLocation {
|
||||
pub dir: String,
|
||||
pub priority: usize,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
pub struct ConflictEntry {
|
||||
pub name: String,
|
||||
pub locations: Vec<ConflictLocation>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct ToolGroup {
|
||||
pub dir: String,
|
||||
pub exists: bool,
|
||||
pub exes: Vec<String>,
|
||||
}
|
||||
|
||||
/// 扫描 PATH 中的可执行文件冲突
|
||||
///
|
||||
/// 遍历每个 PATH 目录,查找 .exe/.bat/.cmd/.com/.ps1 文件,
|
||||
/// 标记出现在多个目录中的同名文件(后面的目录会被前面的「遮蔽」)
|
||||
#[tauri::command]
|
||||
pub fn scan_conflicts(paths: Vec<String>) -> Result<Vec<ConflictEntry>, String> {
|
||||
// exe_name (小写) → [(priority, dir)]
|
||||
let mut map: HashMap<String, Vec<(usize, String)>> = HashMap::new();
|
||||
|
||||
for (priority, dir) in paths.iter().enumerate() {
|
||||
let p = Path::new(dir);
|
||||
if !p.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let entries = fs::read_dir(p).map_err(|e| format!("无法读取目录 {}: {}", dir, e))?;
|
||||
for entry in entries.flatten() {
|
||||
let fname = entry.file_name();
|
||||
let name = fname.to_string_lossy();
|
||||
if let Some(ext) = Path::new(name.as_ref()).extension() {
|
||||
let ext_lower = ext.to_ascii_lowercase();
|
||||
if EXECUTABLE_EXTENSIONS.contains(&ext_lower.to_str().unwrap_or("")) {
|
||||
let key = name.to_lowercase();
|
||||
map.entry(key).or_default().push((priority, dir.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut results: Vec<ConflictEntry> = map
|
||||
.into_iter()
|
||||
.filter(|(_, locs)| locs.len() >= 2)
|
||||
.map(|(name, locs)| ConflictEntry {
|
||||
name,
|
||||
locations: locs
|
||||
.into_iter()
|
||||
.map(|(priority, dir)| ConflictLocation { dir, priority })
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
results.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// 扫描 PATH 中各目录提供的可执行文件
|
||||
///
|
||||
/// query 非空时只返回文件名包含关键词的结果
|
||||
pub fn scan_conflicts(paths: Vec<String>) -> Result<Vec<scanner::ConflictEntry>, String> { scanner::scan_conflicts(paths) }
|
||||
#[tauri::command]
|
||||
pub fn scan_tools(paths: Vec<String>, query: String) -> Result<Vec<ToolGroup>, String> {
|
||||
let query_lower = query.to_lowercase();
|
||||
let mut groups: Vec<ToolGroup> = Vec::new();
|
||||
|
||||
for dir in &paths {
|
||||
let p = Path::new(dir);
|
||||
if !p.is_dir() {
|
||||
groups.push(ToolGroup {
|
||||
dir: dir.clone(),
|
||||
exists: false,
|
||||
exes: vec![],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let entries = fs::read_dir(p).map_err(|e| format!("无法读取目录 {}: {}", dir, e))?;
|
||||
let mut exes: Vec<String> = Vec::new();
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let fname = entry.file_name();
|
||||
let name = fname.to_string_lossy();
|
||||
if let Some(ext) = Path::new(name.as_ref()).extension() {
|
||||
let ext_lower = ext.to_ascii_lowercase();
|
||||
if EXECUTABLE_EXTENSIONS.contains(&ext_lower.to_str().unwrap_or("")) {
|
||||
if query_lower.is_empty() || name.to_lowercase().contains(&query_lower) {
|
||||
exes.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exes.sort();
|
||||
groups.push(ToolGroup {
|
||||
dir: dir.clone(),
|
||||
exists: true,
|
||||
exes,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
pub fn scan_tools(paths: Vec<String>, query: String) -> Result<Vec<scanner::ToolGroup>, String> { scanner::scan_tools(paths, query) }
|
||||
|
||||
@@ -1,148 +1,10 @@
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
use path_editor_core::system;
|
||||
|
||||
/// 检测当前进程是否有管理员权限(尝试写入系统注册表键)
|
||||
#[tauri::command]
|
||||
pub fn check_admin() -> bool {
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
hklm.open_subkey_with_flags(
|
||||
"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment",
|
||||
KEY_WRITE,
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// 验证路径是否存在于文件系统中(且是目录)
|
||||
/// 包含 % 的路径(环境变量路径)无法验证,返回 true
|
||||
pub fn check_admin() -> bool { system::check_admin() }
|
||||
#[tauri::command]
|
||||
pub fn validate_path(path: &str) -> bool {
|
||||
if path.contains('%') {
|
||||
return true;
|
||||
}
|
||||
std::fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 展开路径中的环境变量(如 %JAVA_HOME%\bin → C:\Program Files\Java\jdk-17\bin)
|
||||
pub fn validate_path(path: &str) -> bool { system::validate_path(path) }
|
||||
#[tauri::command]
|
||||
pub fn expand_env_vars(path: &str) -> String {
|
||||
if !path.contains('%') {
|
||||
return path.to_string();
|
||||
}
|
||||
|
||||
// 转为 UTF-16 宽字符串(以 null 结尾)
|
||||
let wide_path: Vec<u16> = path
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
|
||||
// SAFETY: wide_path 是以 null 结尾的 UTF-16 字符串,lpDst 为 null 且 nSize 为 0,
|
||||
// 根据 MSDN 文档此时 API 只查询所需缓冲区大小而不写入数据
|
||||
let required = unsafe {
|
||||
ExpandEnvironmentStringsW(wide_path.as_ptr(), std::ptr::null_mut(), 0)
|
||||
};
|
||||
|
||||
if required == 0 {
|
||||
log::warn!("expand_env_vars: API 查询缓冲区失败, 返回原始路径: {path}");
|
||||
return path.to_string();
|
||||
}
|
||||
|
||||
// SAFETY: buffer 容量为 required(API 返回的精确大小),wide_path 以 null 结尾,
|
||||
// 且两个指针指向不同的内存区域,不存在重叠
|
||||
let mut buffer: Vec<u16> = vec![0; required as usize];
|
||||
let result = unsafe {
|
||||
ExpandEnvironmentStringsW(wide_path.as_ptr(), buffer.as_mut_ptr(), required)
|
||||
};
|
||||
|
||||
if result == 0 || result > required {
|
||||
log::warn!("expand_env_vars: 展开失败或缓冲区不足, 返回原始路径: {path}");
|
||||
return path.to_string();
|
||||
}
|
||||
|
||||
// 转回 UTF-8 (去掉结尾 null)
|
||||
let len = buffer.iter().position(|&c| c == 0).unwrap_or(buffer.len());
|
||||
String::from_utf16_lossy(&buffer[..len])
|
||||
}
|
||||
|
||||
/// 广播环境变量更改通知(WM_SETTINGCHANGE)
|
||||
pub fn expand_env_vars(path: &str) -> String { system::expand_env_vars(path) }
|
||||
#[tauri::command]
|
||||
pub fn broadcast_env_change() {
|
||||
const HWND_BROADCAST: isize = 0xFFFF;
|
||||
const WM_SETTINGCHANGE: u32 = 0x001A;
|
||||
const SMTO_ABORTIFHUNG: u32 = 0x0002;
|
||||
|
||||
// SAFETY: env_str 是以 null 结尾的 UTF-16 字符串,所有指针和常量均遵循 Win32 API 约定
|
||||
let env_str: Vec<u16> = "Environment\0".encode_utf16().collect();
|
||||
|
||||
// SAFETY: env_str.as_ptr() 指向以 null 结尾的字符串,HWND_BROADCAST 是合法句柄,
|
||||
// lpdwResult 为 null 表示不需要返回值,其他参数均为常量
|
||||
let result = unsafe {
|
||||
SendMessageTimeoutW(
|
||||
HWND_BROADCAST,
|
||||
WM_SETTINGCHANGE,
|
||||
0,
|
||||
env_str.as_ptr() as isize,
|
||||
SMTO_ABORTIFHUNG,
|
||||
5000,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
if result == 0 {
|
||||
log::warn!("广播 WM_SETTINGCHANGE 失败");
|
||||
} else {
|
||||
log::info!("已广播环境变量更改通知");
|
||||
}
|
||||
}
|
||||
|
||||
// ── 外部 FFI 声明 ──
|
||||
|
||||
extern "system" {
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-expandenvironmentstringsw
|
||||
fn ExpandEnvironmentStringsW(
|
||||
lpSrc: *const u16,
|
||||
lpDst: *mut u16,
|
||||
nSize: u32,
|
||||
) -> u32;
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessagetimeoutw
|
||||
fn SendMessageTimeoutW(
|
||||
hWnd: isize,
|
||||
Msg: u32,
|
||||
wParam: usize,
|
||||
lParam: isize,
|
||||
fuFlags: u32,
|
||||
uTimeout: u32,
|
||||
lpdwResult: *mut usize,
|
||||
) -> isize;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn validate_path_env_var_always_valid() {
|
||||
assert!(validate_path("%JAVA_HOME%\\bin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_env_vars_no_percent_returns_original() {
|
||||
let result = expand_env_vars("C:\\Windows");
|
||||
assert_eq!(result, "C:\\Windows");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_env_vars_with_invalid_var_returns_original() {
|
||||
// 展开不存在的变量可能会回归原始值或产生部分展开;测试是否不会崩溃
|
||||
let result = expand_env_vars("%__NONEXISTENT_VAR__%");
|
||||
// 至少不应为空白
|
||||
assert!(!result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_admin_returns_bool() {
|
||||
let result = check_admin();
|
||||
// 在任意机器上应返回 true 或 false,不应 panic
|
||||
assert!((result == true) || (result == false));
|
||||
}
|
||||
}
|
||||
pub fn broadcast_env_change() { system::broadcast_env_change() }
|
||||
|
||||
Reference in New Issue
Block a user