feat: 重写为 Tauri + React + TypeScript (v4.0)

完全移除旧 C+IUP 代码,改用 Tauri 2.x + React 19 + TypeScript + Rust 技术栈重写。
功能与 v3.1 完全等价:

- React 前端:Tailwind CSS 4、Zustand 状态管理、i18next 国际化
- Rust 后端:winreg 注册表读写、Win32 API FFI 调用
- 核心逻辑:StringList、UndoRedoManager、PathManager、Import/Export
- 深色模式、中英文切换、键盘快捷键、合并预览
- 66 个 Vitest 单元测试

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 18:32:54 +08:00
parent cdcfd8e0a7
commit 48129a8908
2545 changed files with 12608 additions and 142894 deletions
-275
View File
@@ -1,275 +0,0 @@
# PathEditor 代码审查修复日志
> 审查日期:2026-04-28
> 审查范围:全项目代码质量审查
> 修复状态:29/29 已完成
---
## 一、高严重度问题(7 项)
### 1.1 add_string_list 中 realloc 失败导致内存泄漏
- **文件**`src/utils/string_ext.c`
- **问题**`realloc` 返回 NULL 时,原 `list->items` 指针被覆盖为 NULL,导致原有数组及其中所有字符串指针永久泄漏
- **修复**:使用临时变量保存 `realloc` 结果,失败时保留原数据不变
```c
// 修复前
list->items = (char **)realloc(list->items, list->capacity * sizeof(char *));
// 修复后
char **new_items = (char **)realloc(list->items, new_capacity * sizeof(char *));
if (!new_items)
return; // 失败时保留原数据
list->items = new_items;
```
### 1.2 add_string_list 中 _strdup 失败未检查
- **文件**`src/utils/string_ext.c`
- **问题**`_strdup` 可能返回 NULL,但代码未检查就存入数组并递增 count,后续字符串操作会崩溃
- **修复**:检查 `_strdup` 返回值,失败时不递增 count
### 1.3 string_list_set 中先 free 后 strdup 导致数据丢失
- **文件**`src/utils/string_ext.c`
- **问题**:先释放旧字符串,然后 `_strdup` 可能失败,导致旧数据丢失且无法恢复
- **修复**:先调用 `_strdup` 获取新字符串,成功后再释放旧字符串
### 1.4 wcsftime 缓冲区大小以字节而非宽字符数传入
- **文件**`src/utils/os_env.c`
- **问题**`sizeof(timestamp)` 返回字节数(128),而 `wcsftime` 期望宽字符数(64),可能导致栈缓冲区溢出
- **修复**:改用 `sizeof(timestamp) / sizeof(timestamp[0])`
```c
// 修复前
wcsftime(timestamp, sizeof(timestamp), L"%Y%m%d_%H%M%S", tm_info);
// 修复后
wcsftime(timestamp, sizeof(timestamp) / sizeof(timestamp[0]), L"%Y%m%d_%H%M%S", &tm_info);
```
### 1.5 load_single_path 在 malloc 失败时仍返回 ERR_OK
- **文件**`src/core/registry_service.c`
- **问题**`malloc` 失败时跳过数据读取,但仍返回 `ERR_OK`,调用者认为加载成功
- **修复**`malloc` 失败时返回 `ERR_OUT_OF_MEMORY`
### 1.6 导入数据内存泄漏
- **文件**`src/controller/callbacks_io.c`
- **问题**`btn_import_cb``ExportData imported``system``user` 从未调用 `clear_string_list` 释放
- **修复**:在函数所有返回路径前调用 `clear_string_list` 释放导入数据
### 1.7 JSON 导入解析器完全失效
- **文件**`src/core/import_export.c`
- **问题**:键名检测逻辑被 `in_string` 状态翻转拦截,`"system"``"user"` 的检测永远无法触发
- **修复**:重写 JSON 解析器,在字符串结束时检测键名,合并重复的数组解析逻辑
---
## 二、中严重度问题(9 项)
### 2.1 backup_registry 部分备份成功时错误报告为完全成功
- **文件**`src/utils/os_env.c`
- **修复**:分别跟踪系统和用户 PATH 的备份状态
### 2.2 btn_ok_cb 未检查 backup_registry 返回值
- **文件**`src/controller/callbacks_sys.c`
- **修复**:检查返回值,备份失败时提示用户是否继续保存
```c
// 修复后
ErrorCode backup_result = backup_registry();
if (backup_result != ERR_OK)
{
int choice = IupAlarm("警告", "备份失败!是否继续保存?",
"继续保存", "取消", NULL);
if (choice != 1)
return IUP_DEFAULT;
}
```
### 2.3 TABTITLE 设置在 Dialog 而非 Tabs 控件上
- **文件**`src/ui/main_window.c`
- **问题**`TABTITLE0``TABTITLE1``IupTabs` 属性,不是 `IupDialog` 属性,导致语言切换后选项卡标题不更新
- **修复**:先获取 Tabs 控件句柄,再设置 TABTITLE
### 2.4 list_dropfiles_cb 使用 ANSI 版本 API
- **文件**`src/controller/callbacks_search.c`
- **问题**`GetFileAttributesA` 无法正确处理中文等 Unicode 字符路径
- **修复**:改用 `utf8_to_wide` + `GetFileAttributesW`
### 2.5 load_all_paths 用户路径加载失败时未通知用户
- **文件**`src/controller/callbacks_sys.c`
- **修复**:添加 else 分支,在用户路径加载失败时弹出提示
### 2.6 escape_json_string 未处理所有控制字符
- **文件**`src/core/import_export.c`
- **问题**:只处理了 `\\``"``\n``\r``\t`,其他 0x00-0x1F 控制字符未转义
- **修复**:添加 `\b``\f` 处理,其他控制字符使用 `\uXXXX` 格式
### 2.7 CreateDirectoryW 不创建中间目录
- **文件**`src/utils/os_env.c`
- **修复**:改用 `SHCreateDirectoryExW` 递归创建目录
### 2.8 ExportData 浅拷贝隐患
- **文件**`include/core/import_export.h`
- **修复**:添加文档注释说明只读语义,防止误用 `clear_string_list`
### 2.9 export_paths_to_file 未检查 fprintf 返回值
- **文件**`src/core/import_export.c`
- **修复**:在 `fclose` 后检查 `ferror(fp)`,发现错误时返回 `ERR_FAILED`
---
## 三、低严重度问题(13 项)
### 3.1 JSON 解析器代码重复
- **文件**`src/core/import_export.c`
- **修复**:合并 system/user 数组解析逻辑为统一的键名检测机制
### 3.2 get_app_context_from_dlg 依赖指针到字符串的不安全转换
- **状态**:保留现状(IUP 框架标准用法)
### 3.3 putenv 使用字符串字面量的可移植性问题
- **文件**`src/main.c`
- **修复**:改用 `_wputenv_s`
### 3.4 path_manager_clean 时间复杂度为 O(n³)
- **文件**`src/core/path_manager.c`
- **修复**:使用标记+批量删除优化为 O(n²)
### 3.5 全局日志状态非线程安全
- **状态**:保留现状(当前是单线程 GUI 应用)
### 3.6 localtime 返回静态缓冲区指针,非线程安全
- **文件**`src/utils/os_env.c``src/core/import_export.c`
- **修复**:改用 `localtime_s`
### 3.7 JSON 解析器原地修改输入缓冲区
- **修复**:重写解析器时已解决,不再修改原始缓冲区
### 3.8 JSON 解析器对 `\\"` 场景处理错误
- **修复**:重写解析器时已解决,使用 `is_quote_escaped` 检查连续反斜杠
### 3.9 TXT 格式导入只能导入到系统路径
- **文件**`src/controller/callbacks_io.c`
- **修复**:添加选择对话框,允许用户选择导入到系统变量或用户变量
### 3.10 trim_whitespace 未检查 NULL 输入
- **文件**`src/core/import_export.c`
- **修复**:添加防御性 NULL 检查
### 3.11 is_json_file 使用 strcasecmp 限制可移植性
- **文件**`src/core/import_export.c`
- **修复**:改用 `_stricmp`MSVC/MinGW 都支持)
### 3.12 localtime 可能返回 NULL
- **修复**:改用 `localtime_s`,无需 NULL 检查
### 3.13 main.c 中 _() 嵌套调用可能崩溃
- **文件**`src/main.c`
- **问题**`_(lua_config_get_string(...))` 嵌套调用,若配置键不存在,`gettext(NULL)` 行为未定义
- **修复**:先将结果保存到临时变量并检查非 NULL
```c
// 修复前
IupMessage(_("Warning"), _(lua_config_get_string("status", "admin_warning")));
// 修复后
const char *admin_msg = lua_config_get_string("status", "admin_warning");
IupMessage(_("Warning"), admin_msg ? _(admin_msg) : "需要管理员权限才能编辑环境变量");
```
---
## 四、第一轮修复(原10条锐评)
| # | 问题 | 状态 |
|---|------|------|
| 1 | core层用了iup.h(架构矛盾) | ✅ 已修复 |
| 2 | main.c管理员权限检测代码灾难 | ✅ 已修复 |
| 3 | refresh_main_window_ui重复代码 | ✅ 已修复 |
| 4 | 字符串溢出风险(buffer不统一) | ✅ 已修复 |
| 5 | 错误码乱用(ERR_NULL_PTR误用) | ✅ 已修复 |
| 6 | 硬编码字符串满天飞 | ✅ 已修复 |
| 7 | backup_registry未实现 | ✅ 已修复 |
| 8 | callbacks.c 600行回调地狱 | ✅ 已拆分 |
| 9 | StringList封装不透明 | ✅ 已修复 |
| 10 | refresh_single_list_style黑盒 | ✅ 已修复 |
---
## 五、修改文件清单
| 文件 | 修改类型 |
|------|----------|
| `include/utils/string_ext.h` | 添加访问器函数声明 |
| `include/utils/ui_constants.h` | 新建,控件名称常量 |
| `include/core/import_export.h` | 添加文档注释 |
| `include/controller/callbacks_internal.h` | 新建,内部辅助函数声明 |
| `src/utils/string_ext.c` | 修复 realloc/strdup 安全性 |
| `src/utils/os_env.c` | 修复 wcsftime、localtime、目录创建 |
| `src/core/registry_service.c` | 修复 malloc 错误处理 |
| `src/core/import_export.c` | 重写 JSON 解析器、修复转义函数 |
| `src/core/path_manager.c` | 优化清理算法、修复错误码 |
| `src/main.c` | 修复 putenv、_() 嵌套调用 |
| `src/ui/main_window.c` | 修复 TABTITLE 设置位置 |
| `src/controller/callbacks.c` | 拆分后只保留辅助函数 |
| `src/controller/callbacks_basic.c` | 新建,基础 CRUD 回调 |
| `src/controller/callbacks_nav.c` | 新建,导航回调 |
| `src/controller/callbacks_search.c` | 新建,搜索/拖拽回调 |
| `src/controller/callbacks_io.c` | 新建,导入导出回调 |
| `src/controller/callbacks_sys.c` | 新建,系统操作回调 |
| `CMakeLists.txt` | 添加新源文件 |
---
## 六、编译验证
```bash
cmake -B build -G "MinGW Makefiles"
cmake --build build
```
**结果**:编译通过,无错误,无警告。
---
## 七、剩余已知问题(设计决策,非 Bug)
| 问题 | 原因 | 建议 |
|------|------|------|
| IUP 指针存储模式 | IUP 框架标准用法 | 保持现状 |
| 日志线程安全 | 当前是单线程应用 | 多线程时再处理 |
| localtime 线程安全 | 已改用 localtime_s | 已修复 |
---
*审查完成于 2026-04-28*
@@ -1,422 +0,0 @@
# 撤销/重做 UI 集成 — 实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将已完成的撤销/重做后端逻辑暴露到 UI 层——添加撤销/重做按钮及 Ctrl+Z/Ctrl+Y 快捷键
**Architecture:** 在现有 MVC 架构上,新增两个按钮回调(`btn_undo_cb`/`btn_redo_cb`),它们调用 `undo_redo.c` 中已有的 `undo()`/`redo()` 函数,然后同步 UI 列表。Ctrl+Z/Y 快捷键在 `list_k_any_cb` 中检测并分发到对应的按钮回调。
**Tech Stack:** C17, IUP GUI, GCC/MinGW-w64
---
## 文件结构
| 操作 | 文件 | 职责 |
|------|------|------|
| 修改 | `include/utils/ui_constants.h` | 新增 2 个按钮名称常量 |
| 修改 | `include/controller/callbacks.h` | 声明 `btn_undo_cb` / `btn_redo_cb` |
| 修改 | `src/controller/callbacks_nav.c` | 实现 `btn_undo_cb` / `btn_redo_cb``list_k_any_cb` 增加 Ctrl+Z/Y |
| 修改 | `src/ui/main_window.c` | 创建撤销/重做按钮,加入布局,绑定回调 |
| 修改 | `lua/config.lua` | 新增 `button.undo` / `button.redo` 配置 |
| 修改 | `po/zh_CN.po` | 新增 "Undo"→"撤销" / "Redo"→"重做" 翻译 |
| 修改 | `po/en_US.po` | 新增 "Undo"→"Undo" / "Redo"→"Redo" 翻译 |
| 修改 | `po/messages.pot` | 新增 msgid 条目 |
| 重新生成 | `locale/zh_CN/LC_MESSAGES/zh_CN.mo` | msgfmt 编译 |
| 重新生成 | `locale/en_US/LC_MESSAGES/en_US.mo` | msgfmt 编译 |
---
### Task 1: 添加 UI 常量
**Files:**
- Modify: `include/utils/ui_constants.h`
- [ ] **Step 1: 在 ui_constants.h 添加按钮常量**
`CTRL_BTN_LANG` 之后、`#endif` 之前添加:
```c
// 撤销/重做按钮
#define CTRL_BTN_UNDO "BTN_UNDO"
#define CTRL_BTN_REDO "BTN_REDO"
```
- [ ] **Step 2: 提交**
```bash
git add include/utils/ui_constants.h
git commit -m "feat(undo): 添加撤销/重做按钮的 UI 常量"
```
---
### Task 2: 声明回调函数
**Files:**
- Modify: `include/controller/callbacks.h`
- [ ] **Step 1: 在 callbacks.h 声明新回调函数**
`btn_lang_cb` 声明之后、搜索回调声明之前添加:
```c
// 撤销/重做回调
int btn_undo_cb(Ihandle *self);
int btn_redo_cb(Ihandle *self);
```
- [ ] **Step 2: 提交**
```bash
git add include/controller/callbacks.h
git commit -m "feat(undo): 声明撤销/重做按钮回调函数"
```
---
### Task 3: 实现撤销/重做回调逻辑
**Files:**
- Modify: `src/controller/callbacks_nav.c`
> 需要新增的 include`#include "core/app_context.h"`(已存在)、`#include "ui/ui_utils.h"`(已存在)
- [ ] **Step 1: 在 callbacks_nav.c 头部添加 app_context 头文件引用**
检查第 3 行已有 `#include "core/undo_redo.h"`,第 10 行已有 `#include "utils/ui_constants.h"`。确认无需添加新的 include。
- [ ] **Step 2: 添加刷新撤销/重做按钮状态的辅助函数**
在文件末尾 `list_k_any_cb` 之前添加:
```c
// 刷新撤销/重做按钮的启用状态
static void refresh_undo_redo_buttons(Ihandle *dlg)
{
AppContext *ctx = get_app_context_from_dlg(dlg);
if (!ctx || !ctx->undo_redo_mgr)
return;
Ihandle *btn_undo = IupGetDialogChild(dlg, CTRL_BTN_UNDO);
Ihandle *btn_redo = IupGetDialogChild(dlg, CTRL_BTN_REDO);
if (btn_undo)
IupSetAttribute(btn_undo, "ACTIVE", can_undo(ctx->undo_redo_mgr) ? "YES" : "NO");
if (btn_redo)
IupSetAttribute(btn_redo, "ACTIVE", can_redo(ctx->undo_redo_mgr) ? "YES" : "NO");
}
```
- [ ] **Step 3: 实现 btn_undo_cb**
`refresh_undo_redo_buttons` 之后添加:
```c
int btn_undo_cb(Ihandle *self)
{
Ihandle *dlg = IupGetDialog(self);
AppContext *ctx = get_app_context_from_dlg(dlg);
if (!ctx || !ctx->undo_redo_mgr)
return IUP_DEFAULT;
if (!can_undo(ctx->undo_redo_mgr))
return IUP_DEFAULT;
undo(ctx->undo_redo_mgr, &ctx->sys_paths, &ctx->user_paths);
Ihandle *list_sys = IupGetDialogChild(dlg, CTRL_LIST_SYS);
Ihandle *list_user = IupGetDialogChild(dlg, CTRL_LIST_USER);
sync_string_list_to_ui(list_sys, &ctx->sys_paths);
sync_string_list_to_ui(list_user, &ctx->user_paths);
Ihandle *lbl_status = IupGetDialogChild(dlg, CTRL_LBL_STATUS);
if (lbl_status)
IupSetAttribute(lbl_status, "TITLE", _("Undo completed"));
refresh_undo_redo_buttons(dlg);
return IUP_DEFAULT;
}
```
- [ ] **Step 4: 实现 btn_redo_cb**
`btn_undo_cb` 之后添加:
```c
int btn_redo_cb(Ihandle *self)
{
Ihandle *dlg = IupGetDialog(self);
AppContext *ctx = get_app_context_from_dlg(dlg);
if (!ctx || !ctx->undo_redo_mgr)
return IUP_DEFAULT;
if (!can_redo(ctx->undo_redo_mgr))
return IUP_DEFAULT;
redo(ctx->undo_redo_mgr, &ctx->sys_paths, &ctx->user_paths);
Ihandle *list_sys = IupGetDialogChild(dlg, CTRL_LIST_SYS);
Ihandle *list_user = IupGetDialogChild(dlg, CTRL_LIST_USER);
sync_string_list_to_ui(list_sys, &ctx->sys_paths);
sync_string_list_to_ui(list_user, &ctx->user_paths);
Ihandle *lbl_status = IupGetDialogChild(dlg, CTRL_LBL_STATUS);
if (lbl_status)
IupSetAttribute(lbl_status, "TITLE", _("Redo completed"));
refresh_undo_redo_buttons(dlg);
return IUP_DEFAULT;
}
```
- [ ] **Step 5: 修改 list_k_any_cb 添加 Ctrl+Z / Ctrl+Y 检测**
将现有的 `list_k_any_cb` 函数(第 149-164 行)替换为:
```c
int list_k_any_cb(Ihandle *self, int c)
{
if (IupGetInt(self, "ACTIVE") == 0)
return IUP_DEFAULT;
if (c == K_cZ) // Ctrl+Z 撤销
{
btn_undo_cb(self);
return IUP_IGNORE;
}
if (c == K_cY) // Ctrl+Y 重做
{
btn_redo_cb(self);
return IUP_IGNORE;
}
if (c == K_DEL) // DEL 键
{
btn_del_cb(self);
return IUP_IGNORE;
}
return IUP_DEFAULT;
}
```
- [ ] **Step 6: 提交**
```bash
git add src/controller/callbacks_nav.c
git commit -m "feat(undo): 实现撤销/重做按钮回调及 Ctrl+Z/Y 快捷键"
```
---
### Task 4: 在 UI 中添加撤销/重做按钮
**Files:**
- Modify: `src/ui/main_window.c`
- [ ] **Step 1: 在 main_window.c 头部添加 app_context 引用**
检查当前 include。如果尚未包含 `<iupkey.h>`,需要在 `#include <iup.h>` 之后添加(但 iup.h 通常已包含 iupkey.h)。确认无需新增 include。
- [ ] **Step 2: 创建撤销/重做按钮并绑定回调**
`btn_down` 按钮创建之后(第 67 行附近)、`btn_clean` 之前,添加新按钮:
```c
Ihandle *btn_undo = IupButton(_(lua_config_get_string("button", "undo")), NULL);
IupSetAttribute(btn_undo, "NAME", CTRL_BTN_UNDO);
IupSetAttribute(btn_undo, "ACTIVE", "NO"); // 初始无操作可撤销
Ihandle *btn_redo = IupButton(_(lua_config_get_string("button", "redo")), NULL);
IupSetAttribute(btn_redo, "NAME", CTRL_BTN_REDO);
IupSetAttribute(btn_redo, "ACTIVE", "NO"); // 初始无操作可重做
```
- [ ] **Step 3: 设置按钮回调**
在现有按钮回调设置区域(第 84-93 行),`btn_down` 回调之后添加:
```c
IupSetCallback(btn_undo, "ACTION", (Icallback)btn_undo_cb);
IupSetCallback(btn_redo, "ACTION", (Icallback)btn_redo_cb);
```
- [ ] **Step 4: 设置按钮大小**
在按钮大小设置区域(第 96-106 行),`btn_export` 之后添加:
```c
IupSetAttribute(btn_undo, "RASTERSIZE", btn_size);
IupSetAttribute(btn_redo, "RASTERSIZE", btn_size);
```
- [ ] **Step 5: 将按钮加入垂直布局**
修改 `vbox_btns` 布局(第 109-118 行),在 `btn_up, btn_down` 之后、`NULL` 之前加入 `btn_undo, btn_redo`
```c
Ihandle *vbox_btns = IupVbox(
btn_new, btn_edit, btn_browse, btn_del,
IupFill(),
btn_clean,
IupFill(),
btn_import, btn_export,
btn_up, btn_down,
btn_undo, btn_redo,
NULL);
```
- [ ] **Step 6: 在 refresh_main_window_ui 中添加按钮文本刷新**
`refresh_main_window_ui` 函数的 `SET_CHILD_TITLE` 宏调用区域(第 198-209 行),`CTRL_BTN_EXPORT` 之后添加:
```c
SET_CHILD_TITLE(CTRL_BTN_UNDO, "undo");
SET_CHILD_TITLE(CTRL_BTN_REDO, "redo");
```
- [ ] **Step 7: 提交**
```bash
git add src/ui/main_window.c
git commit -m "feat(undo): 在 UI 中添加撤销/重做按钮并集成布局"
```
---
### Task 5: 更新 Lua 配置
**Files:**
- Modify: `lua/config.lua`
- [ ] **Step 1: 在 config.lua 的 button 表中添加 undo 和 redo**
`button` 表的 `help = "Help"` 之后添加:
```lua
undo = "Undo",
redo = "Redo",
```
- [ ] **Step 2: 提交**
```bash
git add lua/config.lua
git commit -m "feat(undo): 在 Lua 配置中添加撤销/重做按钮文本"
```
---
### Task 6: 更新翻译文件
**Files:**
- Modify: `po/zh_CN.po`
- Modify: `po/en_US.po`
- Modify: `po/messages.pot`
- [ ] **Step 1: 在 zh_CN.po 中添加翻译条目**
在 "Clean Invalid" 条目之后、其他条目之前插入:
```
#: src/ui/main_window.c
msgid "Undo"
msgstr "撤销"
#: src/ui/main_window.c
msgid "Redo"
msgstr "重做"
```
以及状态栏消息:
```
#: src/controller/callbacks_nav.c
msgid "Undo completed"
msgstr "已撤销"
#: src/controller/callbacks_nav.c
msgid "Redo completed"
msgstr "已重做"
```
- [ ] **Step 2: 在 en_US.po 中添加翻译条目**
```
#: src/ui/main_window.c
msgid "Undo"
msgstr "Undo"
#: src/ui/main_window.c
msgid "Redo"
msgstr "Redo"
#: src/controller/callbacks_nav.c
msgid "Undo completed"
msgstr "Undo completed"
#: src/controller/callbacks_nav.c
msgid "Redo completed"
msgstr "Redo completed"
```
- [ ] **Step 3: 在 messages.pot 中添加翻译模板条目**
```
#: src/ui/main_window.c
msgid "Undo"
msgstr ""
#: src/ui/main_window.c
msgid "Redo"
msgstr ""
#: src/controller/callbacks_nav.c
msgid "Undo completed"
msgstr ""
#: src/controller/callbacks_nav.c
msgid "Redo completed"
msgstr ""
```
- [ ] **Step 4: 重新编译 .mo 文件**
```bash
cd D:/Code/doing_exercises/programs/PathEditor
msgfmt po/zh_CN.po -o locale/zh_CN/LC_MESSAGES/zh_CN.mo
msgfmt po/en_US.po -o locale/en_US/LC_MESSAGES/en_US.mo
```
- [ ] **Step 5: 提交**
```bash
git add po/zh_CN.po po/en_US.po po/messages.pot locale/zh_CN/LC_MESSAGES/zh_CN.mo locale/en_US/LC_MESSAGES/en_US.mo
git commit -m "feat(undo): 添加撤销/重做的中英文翻译"
```
---
### Task 7: 编译验证
- [ ] **Step 1: 编译项目**
```bash
cd D:/Code/doing_exercises/programs/PathEditor
cmake --build build
```
预期输出:编译成功,无错误无警告。
- [ ] **Step 2: 功能验证清单**
1. 启动程序,确认出现「撤销」「重做」按钮
2. 新建一条路径 → 撤销按钮变可用 → 点击撤销 → 路径消失 → 重做按钮变可用
3. 按 Ctrl+Z 撤销 → Ctrl+Y 重做
4. 删除一条路径 → 撤销恢复 → 重做再次删除
5. 无历史记录时,撤销/重做按钮灰色不可点击
6. 语言切换后按钮文本正确切换
@@ -1,37 +0,0 @@
# 全局快捷键 — 设计文档
## 背景
Ctrl+Z/Y 撤销/重做已在功能 1 中实现(列表级 K_ANY)。新增 Ctrl+N/S/F 作为对话框级的全局快捷键。
## 目标
添加三个全局快捷键:`Ctrl+N` 新建、`Ctrl+S` 保存、`Ctrl+F` 聚焦搜索框。
## 改动文件
| 文件 | 改动 |
|------|------|
| `include/controller/callbacks.h` | 声明 `dlg_k_any_cb` |
| `src/controller/callbacks_sys.c` | 实现 `dlg_k_any_cb` |
| `src/ui/main_window.c` | 对话框注册 `K_ANY` 回调 |
## 核心逻辑
```
dlg_k_any_cb(dlg, c):
if c == K_cN → btn_new_cb(dlg)
if c == K_cS → btn_ok_cb(dlg)
if c == K_cF → IupSetFocus(txt_search)
else → IUP_DEFAULT
```
## 快捷键传播
IUP 键盘事件从子控件向父控件传播。列表的 `list_k_any_cb`Ctrl+Z/Y/DEL)返回 `IUP_IGNORE` 阻止传播;未识别的键返回 `IUP_DEFAULT` 使事件继续传播到对话框的 `dlg_k_any_cb`
## 不做的事
- 不新增翻译条目
- 不修改 Lua 配置
- 列表级 `K_ANY` 保持不变
@@ -1,65 +0,0 @@
# 撤销/重做 UI 集成 — 设计文档
## 背景
撤销/重做后端(`src/core/undo_redo.c`)已完整实现,支持 7 种操作类型的记录与回滚。所有 UI 操作(新建/编辑/删除/上移/下移/清理)均已调用 `push_undo_record()` 写入历史。但 `undo()``redo()` 函数未被任何 UI 代码调用——用户无法触发撤销或重做。
## 目标
在界面上添加撤销/重做按钮,并绑定 Ctrl+Z / Ctrl+Y 快捷键,让用户可以回退和恢复操作。
## 改动文件
| 文件 | 改动内容 |
|------|---------|
| `include/utils/ui_constants.h` | 新增 `CTRL_BTN_UNDO``CTRL_BTN_REDO` 常量 |
| `src/ui/main_window.c` | 创建撤销/重做按钮,绑定回调,调整布局 |
| `src/controller/callbacks_nav.c` | 新增 `btn_undo_cb``btn_redo_cb``list_k_any_cb` 增加 Ctrl+Z/Y 检测 |
| `lua/config.lua` | 新增 `button.undo``button.redo` 配置项 |
| `locale/` 翻译文件 | 同步新增按钮的中英文翻译 |
## 核心逻辑
```
btn_undo_cb(dlg):
ctx = get_app_context_from_dlg(dlg)
if !can_undo(ctx->undo_redo_mgr): return
undo(ctx->undo_redo_mgr, &ctx->sys_paths, &ctx->user_paths)
sync both lists to UI
update undo/redo button enabled state
btn_redo_cb(dlg):
同上,调用 redo()
list_k_any_cb:
新增分支:
if c == K_cZ → btn_undo_cb
if c == K_cY → btn_redo_cb
```
## 按钮布局
撤销/重做按钮放在上移/下移按钮下方:
```
[新建] [编辑]
[浏览] [删除]
(分隔)
[一键清理]
(分隔)
[导入] [导出]
[上移] [下移]
[撤销] [重做] ← 新增
```
## 按钮状态
- `can_undo() == false` → 撤销按钮 `ACTIVE=NO`
- `can_redo() == false` → 重做按钮 `ACTIVE=NO`
- 每次 undo/redo 执行后刷新按钮状态
## 不做的事
- 不修改 `undo_redo.c` 后端代码(已完备)
- 不添加操作历史面板(保持简洁,通过按钮状态反馈即可)
- 不在保存后清空历史(当前设计由 `clear_undo_redo_history` 决定,保持现有行为)
@@ -1,170 +0,0 @@
# 1. 问题
这个问题位于系统 PATH 保存回调与注册表备份工具之间的交界处。当前实现虽然在保存前调用了备份,但备份结果没有进入成功失败判断,导致“先备份再写入”只剩下调用顺序,没有形成真正的安全护栏。
## 1.1. **备份没有成为保存前置条件**
`src/controller/callbacks_sys.c:25-34``btn_ok_cb` 在管理员校验后直接调用 `backup_registry()`,随后继续执行 `save_system_paths``save_user_paths`。这里最大的问题不是“没做备份”,而是“做了也等于没做校验”。
```c
if (!check_admin())
{
IupMessage("错误", "需要管理员权限才能保存更改!");
return IUP_DEFAULT;
}
backup_registry();
ErrorCode sys_ok = save_system_paths(&ctx->sys_paths);
ErrorCode user_ok = save_user_paths(&ctx->user_paths);
```
这会带来两个直接后果:
- 备份目录创建失败、备份文件打开失败、注册表读取失败时,保存流程仍然继续。
- 用户看到的只是“保存成功”或“保存失败”,看不到“备份失败但仍然写入”的中间状态。
对于修改系统 PATH 这类高风险操作,备份不是可有可无的副作用,而应该是明确的前置步骤。否则一旦写入结果不符合预期,用户既没有可靠回退副本,也很难知道问题发生在哪一段。
## 1.2. **备份函数的失败语义过于粗糙**
`src/utils/os_env.c:50-131``backup_registry` 把多个失败场景都折叠成 `ERR_FAILED`,而且只用一个 `success` 标记表示“系统 PATH 或用户 PATH 只要有一处写入成功即可”。这让调用方几乎无法做出正确反馈。
```c
if (SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, 0, appdata_path) != S_OK)
{
return ERR_FAILED;
}
FILE *fp = _wfopen(backup_file, L"w, ccs=UTF-8");
if (!fp)
return ERR_FAILED;
...
return success ? ERR_OK : ERR_FAILED;
```
这里的问题有两层:
- 调用方拿不到失败原因,界面层只能给出笼统提示。
- 备份结果粒度太粗,后续如果要区分“文件系统失败”“系统 PATH 读取失败”“用户 PATH 读取失败”,需要再次拆函数,改动会扩散。
这类错误语义粗糙的问题,短期看只是提示不够友好,长期看会让保存链路的错误处理越来越分散。
# 2. 收益
把备份纳入保存链路的显式校验后,PATH 修改流程会从“尽力而为”变成“可判断、可阻断、可回退”。
## 2.1. **降低误写后无法回退的风险**
当前最危险的情况,是备份失败但注册表写入成功。调整后,保存前先确认备份成功,能直接消除这条风险路径。对系统 PATH 这种会影响命令行、编译器和工具链的配置,这个收益是最核心的。
## 2.2. **让失败位置更容易定位**
现在保存链路里至少有 **3** 个备份失败点:目录准备、文件创建、注册表读取。把这些失败显式返回后,界面层和日志可以明确知道失败发生在保存前,而不是笼统归为“保存异常”。
## 2.3. **减少后续错误处理分散**
把备份检查集中到保存入口,可以让 `btn_ok_cb` 成为统一决策点。后续如果要加入“跳过备份需二次确认”或“导出备份路径”,只需要在一个入口扩展,而不是在多个保存分支里补判断。
# 3. 方案
总体思路是:把备份从隐式副作用改成显式的保存前置步骤,同时把备份失败原因转换成 UI 可消费的信息。
## 3.1. **引入保存前置校验:解决“备份没有成为保存前置条件”**
方案核心是为 `btn_ok_cb` 增加一个明确的“准备保存”阶段。只有备份成功,后续系统 PATH 和用户 PATH 的写入才允许继续。
实施步骤:
- 在控制层增加 `ErrorCode backup_ok = backup_registry();` 的显式判断。
- 当备份失败时,立即记录日志、更新状态栏并弹出错误提示,然后提前返回。
- 将“广播环境变量变更”和“保存成功提示”保留在备份成功且写入成功之后。
修改前:
```c
backup_registry();
ErrorCode sys_ok = save_system_paths(&ctx->sys_paths);
ErrorCode user_ok = save_user_paths(&ctx->user_paths);
```
修改后(实际实现):
```c
ErrorCode backup_result = backup_registry();
if (backup_result != ERR_OK)
{
log_error("Backup failed: error code %d", backup_result);
int choice = IupAlarm("警告", "备份失败!是否继续保存?\n(继续保存可能导致无法恢复)",
"继续保存", "取消", NULL);
if (choice != 1)
return IUP_DEFAULT;
}
ErrorCode sys_ok = save_system_paths(&ctx->sys_paths);
ErrorCode user_ok = save_user_paths(&ctx->user_paths);
```
实际实现采用了更灵活的策略:备份失败时给予用户选择权,而非直接阻断。这样既保留了安全护栏(默认提示风险),又允许用户在确认有其他备份时继续操作。
## 3.2. **细化备份结果表达:解决“备份函数的失败语义过于粗糙”**
短期内不必大改架构,建议先做一层轻量封装,把 `backup_registry` 的失败点映射到更明确的错误码或消息文本。这样可以在不改动主流程结构的前提下,把错误反馈补齐。
实施步骤:
- 为备份过程补充更细的返回值,至少区分“目录或路径获取失败”“文件打开失败”“注册表读取失败”。
- 若暂时不扩展 `ErrorCode` 枚举,可新增 `backup_registry_with_message(char *buf, size_t len)` 一类包装函数,专门给控制层提供可展示的失败原因。
- 保持注册表读写逻辑仍在工具层,避免控制层直接拼装底层错误文本。
修改前:
```c
if (!fp)
return ERR_FAILED;
...
return success ? ERR_OK : ERR_FAILED;
```
修改后(实际实现):
```c
// 使用 SHCreateDirectoryExW 递归创建目录,解决中间目录不存在的问题
SHCreateDirectoryExW(NULL, backup_dir, NULL);
// localtime_s 替代 localtime,提升线程安全性
struct tm tm_info;
localtime_s(&tm_info, &t);
wcsftime(timestamp, sizeof(timestamp) / sizeof(timestamp[0]), L"%Y%m%d_%H%M%S", &tm_info);
```
实际实现暂未扩展细粒度错误码,但已通过以下方式改进:
- 使用 `SHCreateDirectoryExW` 递归创建目录,解决中间目录不存在的问题
- 使用 `localtime_s` 替代 `localtime`,提升线程安全性
- 通过日志记录具体失败原因,便于问题定位
后续可考虑扩展 `ErrorCode` 枚举以区分不同失败场景。
# 4. 回归范围
这次调整影响的是“点击确定后的保存链路”,重点要从用户完整操作流程看:备份是否先发生、备份失败是否阻断写入、写入成功后是否仍能正常广播环境变量变更。
## 4.1. 主链路
- 正常编辑系统 PATH 和用户 PATH后点击“确定”。
- 预期先生成备份,再分别写入系统和用户 PATH。
- 写入成功后,状态栏、成功提示和环境变量广播行为保持不变。
建议重点检查以下业务结果:
- 备份文件是否实际生成。
- 成功提示是否只在备份成功且写入完成后出现。
- 写入后新开的命令行窗口是否能读取最新 PATH。
## 4.2. 边界情况
- 备份目录无法创建:应立即终止保存,注册表值保持不变,界面给出明确错误提示。
- 备份文件无法打开:应立即终止保存,避免出现“没有备份但已经写入”的状态。
- 系统或用户 PATH 读取失败:应视策略决定是否整体阻断。若当前目标是“完整备份后再保存”,则任一关键读取失败都应阻断。
- 仅保存阶段失败:备份已经成功生成时,仍要验证现有失败提示是否准确,避免把备份失败和写入失败混为一类。