深度解析macOS清理工具从CLI到GUI的技术迁移之路为什么做这个软件上一篇文章发布后,有读者留言说:"Mole很好用,但家里人不习惯用命令行,有没有图形界面版本?"这个需求很真实。Mole的功能确实强大,但终端工具天然有门槛——需要记住命令、理解输出、忍受全英文界
深度解析 macOS 清理工具从 CLI 到 GUI 的技术迁移之路
上一篇文章发布后,有读者留言说:
"Mole 很好用,但家里人不习惯用命令行,有没有图形界面版本?"
这个需求很真实。Mole 的功能确实强大,但终端工具天然有门槛——需要记住命令、理解输出、忍受全英文界面。
对于普通用户来说,清理 Mac 空间这样高频的需求,值得拥有一个更友好的图形界面。
同时,市面上的清理软件要么收费昂贵,要么功能单一,要么广告满天飞。
我们希望做一个开源、免费、功能完整的 macOS 清理工具,让每个人都能轻松管理自己的 Mac 存储空间。
FreshDisk 就是这个尝试的产物——保留 Mole 的核心清理能力,用现代化的桌面界面呈现。
Mole 是一个用 Go + Shell 编写的 macOS 终端清理工具,提供八大功能模块:
| 模块 | 功能 |
|---|---|
analyze |
磁盘空间深度分析 |
status |
实时系统监控 |
clean |
缓存、日志清理 |
purge |
开发者缓存清理 |
optimize |
系统优化维护 |
uninstall |
应用卸载 |
installer |
安装包管理 |
touchid |
权限配置 |
核心逻辑在 lib/ 目录下,以 Shell 脚本形式组织。
FreshDisk 基于 Tauri 2.0 + React 19 构建,采用前后端分离架构:
React → Tauri IPC → Rust 后端 → 系统调用
| 维度 | Mole | FreshDisk |
|---|---|---|
| UI 框架 | Bubble Tea (TUI) | Tauri + React |
| 核心语言 | Go + Shell | Rust + TypeScript |
| 交互模式 | 同步阻塞 | 异步事件驱动 |
| 进度反馈 | 日志输出 | 实时进度条 |
┌─────────────────────────────────────────┐
│ React 前端层 │
│ Workspace / Panel / Dialog / Progress │
├─────────────────────────────────────────┤
│ Tauri IPC 层 │
│ Commands + Events + State │
├─────────────────────────────────────────┤
│ Rust 后端层 │
│ analyze / clean / status 模块 │
├─────────────────────────────────────────┤
│ 系统调用层 │
│ sysinfo / glob / trash / walkdir │
└─────────────────────────────────────────┘
| Mole 模块 | FreshDisk 模块 | 迁移方式 |
|---|---|---|
lib/core/ |
src-tauri/src/core/ |
重构为 Rust |
lib/clean/*.sh |
src-tauri/src/clean/ |
重写逻辑 |
cmd/analyze/ |
src-tauri/src/analyze/ |
复用思路 |
lib/uninstall/ |
src-tauri/src/uninstall/ |
待实现 |
将 Shell 中的清理路径提取为 Rust 常量:
// 常量定义
pub const BROWSER_CACHE_PATHS: &[&str] = &[
"~/Library/Caches/com.apple.Safari",
"~/Library/Caches/com.google.Chrome",
];
pub const DEV_CACHE_PATHS: &[&str] = &[
"~/Library/Developer/Xcode/DerivedData",
"node_modules",
"__pycache__",
];
pub const PROTECTED_BUNDLES: &[&str] = &[
"com.apple.Safari",
"com.apple.finder",
];
迁移 Mole 的 lib/core/file_ops.sh 逻辑:
// 验证路径安全性
pub fn validate_path(path: &Path) -> Result<(), Error> {
// 防止路径遍历
// 检查白名单保护
// 验证系统关键路径
}
// 安全删除(到回收站)
pub fn safe_remove(path: &Path) -> Result<(), Error> {
if is_protected(path)? {
return Err(Error::ProtectedPath);
}
trash(path)?;
Ok(())
}
复用 Mole 的 cmd/analyze/scanner.go 并发思路:
// 使用 walkdir + rayon 并行扫描
pub fn scan_directory(path: &Path) -> Vec<ScanItem> {
WalkDir::new(path)
.into_iter()
.par_bridge() // 并行处理
.filter_map(|e| e.ok())
.filter(|e| e.metadata().is_ok())
.map(|e| ScanItem {
path: e.path().to_string(),
size: e.metadata().unwrap().len(),
})
.collect()
}
通过 Tauri Events 实现实时进度:
// Rust 后端:发送进度
window.emit("scan_progress", Progress {
percentage: (scanned * 100 / total) as u8,
current_path: path.to_string(),
})?;
// TypeScript 前端:监听进度
listen<Progress>("scan_progress", (event) => {
setProgress(event.payload);
});
使用 sysinfo crate 实现状态监控:
use sysinfo::{System, SystemExt, CpuExt, DiskExt};
pub fn get_status() -> SystemStatus {
let mut sys = System::new_all();
sys.refresh_all();
SystemStatus {
cpu: sys.global_cpu_info().cpu_usage(),
memory: sys.used_memory() as f64 / sys.total_memory() as f64,
disks: sys.disks().iter().map(|d| DiskStatus {
mount: d.mount_point().to_string(),
used_percent: (d.total_space() - d.available_space()) as f64 / d.total_space() as f64,
}).collect(),
}
}
通过子进程调用 brew 命令:
pub fn brew_uninstall(name: &str) -> Result<(), Error> {
let output = Command::new("brew")
.args(&["uninstall", "--cask", "--zap", name])
.output()?;
if !output.status.success() {
return Err(Error::BrewFailed);
}
// 自动清理依赖
Command::new("brew").args(&["autoremove"]).output()?;
Ok(())
}
用户点击扫描
↓
React:invoke('clean_scan_all')
↓
Rust:scan_cleanable_items()
↓
使用 glob 匹配路径
↓
WalkDir 遍历文件
↓
计算文件大小
↓
Window.emit('progress')
↓
React 更新进度条
↓
返回扫描结果
用户确认清理
↓
检查白名单保护
↓
验证路径安全性
↓
使用 trash crate 删除
↓
记录清理结果
↓
发送完成通知
| Shell 命令 | Rust 方案 |
|---|---|
find |
walkdir |
du |
walkdir + std::fs |
xargs |
rayon |
grep |
regex |
使用 objc2 调用 AppKit 处理 Touch ID 和完整磁盘访问:
// 请求完整磁盘访问权限
pub fn request_full_disk_access() -> bool {
let panel = NSOpenPanel::new();
panel.setCanChooseDirectories(true);
panel.runModal() == NSModalResponseOK
}
invoke 等待结果Events 实时推送// rayon 并行处理大目录
items.par_iter().for_each(|item| {
let size = calculate_size(item.path);
// ...
});
缓存上次扫描结果,只扫描变化的文件:
pub fn incremental_scan(path: &Path, last: &ScanSnapshot) -> ScanDelta {
// 比较文件修改时间
// 只返回新增和修改的项目
}
// 缓存目录大小计算结果
let cache = Mutex::new(LruCache::new(100));
analyze → 磁盘分析(核心)status → 状态监控(简单)clean → 深度清理(高频)purge → 项目清理(进阶)uninstall → 应用卸载(复杂)optimize → 系统优化(收尾)| 场景 | 推荐 |
|---|---|
| 文件遍历 | walkdir |
| 并行处理 | rayon |
| 系统信息 | sysinfo |
| 安全删除 | trash |
| 路径匹配 | glob + regex |
通过以上策略和技术选型,FreshDisk 成功实现了从 Mole 命令行工具到桌面应用的迁移,提供了更友好的用户体验和强大的功能。
希望这篇文章能为有类似需求的开发者提供参考和帮助。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!