54 Commits

Author SHA1 Message Date
Serendipity 720ebb535d feat(ui): 添加深色模式支持
- 新增深色/浅色模式切换按钮,位于主窗口底部
- 在配置文件中定义主题颜色(浅色/深色背景、列表背景、前景色)
- 更新 UI 工具函数以支持动态主题切换,包括列表斑马纹适配
- 添加翻译条目(Dark Mode/Light Mode)并更新编译脚本
- 修改主窗口创建逻辑,集成主题切换回调
- 调整列表背景色属性从 BACKCOLOR 改为 BGCOLOR 以保持一致性
2026-05-02 01:32:56 +08:00
Serendipity 3df2988915 feat(merge): 添加合并预览 Tab 页
- 新增第三个 Tab「合并预览」,只读展示系统+用户 PATH 完整列表
- 选项卡切换时自动刷新合并列表
- 支持无效/重复路径高亮
- 新增中英文翻译

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 23:54:53 +08:00
Serendipity c5c5517ded feat(multi-select): 支持列表多选及批量删除
- 列表控件启用 MULTIPLE=YES 属性
- btn_del_cb 支持批量删除:解析分号分隔的多选索引,从大到小删除
- 批量删除推送一条合并的 undo record,支持一步撤销

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 23:49:44 +08:00
Serendipity 485d16a180 feat(tooltip): 添加环境变量展开预览悬停提示
- 新增 expand_env_vars 函数,调用 ExpandEnvironmentStringsA 展开 %VAR%
- sync_string_list_to_ui 中对含 % 的路径设置 ITEMTIP 属性
- 鼠标悬停时显示展开后的完整路径

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 23:45:58 +08:00
Serendipity e5d24389b4 feat(shortcuts): 添加全局快捷键 Ctrl+N/S/F
- Ctrl+N: 新建路径
- Ctrl+S: 保存(确定)
- Ctrl+F: 聚焦搜索框
- 对话框级 K_ANY 回调,列表级 K_ANY 保持 Ctrl+Z/Y/DEL

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 23:17:14 +08:00
Serendipity 5bb0ac66cf docs: 添加全局快捷键设计文档
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 23:16:05 +08:00
Serendipity ec0ca5a3f6 fix(undo): 修复撤销按钮状态不刷新及空指针防护
- 将 refresh_undo_redo_buttons 提升为公共函数(声明在 callbacks_internal.h,实现在 callbacks.c)
- 在所有 push_record 的操作回调末尾调用 refresh_undo_redo_buttons,确保按钮状态实时更新
- 修复 redo() 中 OP_CLEAN/OP_IMPORT 的 new_paths 空指针风险
- 移除 undo_redo.c 中废弃的 apply_record 函数

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 23:12:42 +08:00
Serendipity bbcfc25aea feat(undo): 重新编译 .mo 翻译文件以包含撤销/重做文本
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 23:04:24 +08:00
Serendipity a8002aeba4 feat(undo): 添加撤销/重做的中英文翻译
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 23:00:47 +08:00
Serendipity aac4cc4b54 feat(undo): 在 Lua 配置中添加撤销/重做按钮文本 2026-05-01 22:54:20 +08:00
Serendipity c8d9b32c70 feat(undo): 在 UI 中添加撤销/重做按钮并集成布局
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 22:53:18 +08:00
Serendipity f9628f6e8c feat(undo): 实现撤销/重做按钮回调及 Ctrl+Z/Y 快捷键
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 22:50:39 +08:00
Serendipity e0af409ef5 feat(undo): 声明撤销/重做按钮回调函数
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 22:48:14 +08:00
Serendipity 256e793ee4 feat(undo): 添加撤销/重做按钮的 UI 常量
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 22:47:14 +08:00
Serendipity 8bd8c0a0aa docs: 添加撤销/重做 UI 集成的实现计划
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 22:45:06 +08:00
Serendipity 1f48551199 feat: 实现撤销/重做功能和CSV导出支持
- 添加撤销/重做管理器,支持添加、删除、编辑、移动等操作的撤销/重做
- 在应用上下文中集成撤销/重做管理器,最大支持50条历史记录
- 为所有基本操作(新建、编辑、删除、上移、下移、清理)添加撤销记录
- 扩展导出功能,支持CSV格式导出(除原有JSON格式外)
- 添加路径格式验证函数,确保导入数据的有效性
- 更新UI文件对话框过滤器以包含CSV格式选项
2026-05-01 22:42:56 +08:00
Serendipity 06e4c15b5c docs: 添加撤销/重做 UI 集成设计文档
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 22:39:39 +08:00
Serendipity 32e6bb262b feat(test): 添加单元测试框架并完善国际化支持
- 新增 CMake 测试框架配置,支持 safe_string、string_ext 和 path_manager 模块的单元测试
- 实现 Windows API Mock 机制,便于测试编码转换函数
- 添加 error_code 模块的字符串表示函数,支持英文错误日志
- 在 UI 回调函数中集成国际化翻译,覆盖新建、编辑、导入导出等操作提示
- 扩展 string_list 功能,新增重复路径检查函数
- 更新翻译文件,同步所有用户界面的中英文文本
2026-04-30 11:38:34 +08:00
Serendipity cf19a37a97 feat(test): 添加单元测试框架并完善国际化支持
- 新增 CMake 测试框架配置,支持 safe_string、string_ext 和 path_manager 模块的单元测试
- 实现 Windows API Mock 机制,便于测试编码转换函数
- 添加 error_code 模块的字符串表示函数,支持英文错误日志
- 在 UI 回调函数中集成国际化翻译,覆盖新建、编辑、导入导出等操作提示
- 扩展 string_list 功能,新增重复路径检查函数
- 更新翻译文件,同步所有用户界面的中英文文本
2026-04-30 11:38:05 +08:00
Serendipity ceed90aea8 feat(backup): 支持自定义备份目录和备份跳过选项
- 在 Lua 配置中新增 backup.dir 配置项,允许设置默认备份路径
- 修改 backup_registry() 函数,接受自定义备份路径参数,优先级为:传入参数 > Lua 配置 > 默认 %APPDATA% 路径
- 在保存操作前,通过对话框让用户选择“使用默认路径”、“自定义目录”或“跳过备份”
- 更新 README 文档,说明新的备份功能和多语言支持等架构细节
2026-04-29 16:08:58 +08:00
Serendipity 3bc2f00cb1 feat(backup): 增强备份失败时的用户提示和错误处理
- 在备份失败时显示具体原因(如无法获取AppData路径、创建备份目录失败等)
- 改进备份函数的错误处理,添加详细的日志记录
- 备份失败时允许用户选择是否继续保存操作
- 修复备份目录创建失败时的错误码返回
- 添加备份过程的详细文档说明
2026-04-28 22:40:50 +08:00
Serendipity e777b26879 fix: 修复JSON导入、备份目录创建和内存安全等问题
修复JSON导入时转义字符处理不完整的问题,添加对\b、\f等控制字符的转义
改进备份目录创建逻辑,使用SHCreateDirectoryExW递归创建目录
修复内存分配失败处理,避免空指针解引用
修正选项卡标题设置位置,从Dialog改为Tabs控件
增强导入功能,支持TXT文件导入时选择目标变量类型
优化清理无效路径算法,使用标记数组减少内存移动
修复宽字符环境变量设置,使用_wputenv_s替代putenv
添加导入数据初始化,防止未初始化内存访问
改进文件属性检查,使用宽字符API支持Unicode路径
2026-04-28 22:21:06 +08:00
Serendipity 7908bad1f4 refactor(controller): 拆分回调函数到模块化文件并添加字符串列表访问器
- 将 callbacks.c 拆分为多个按功能分类的文件(callbacks_basic.c、callbacks_nav.c、callbacks_search.c、callbacks_io.c、callbacks_sys.c)
- 新增 callbacks_internal.h 提供内部辅助函数声明,减少代码重复
- 在 string_ext 模块中添加 string_list_get 和 string_list_set 安全访问器函数
- 更新 CMakeLists.txt 和 ui_utils.c 以使用新的模块结构和访问器
- 重构旨在提高代码可维护性和可读性,便于后续功能扩展
2026-04-28 21:54:47 +08:00
Serendipity ea3d678d22 feat(ui): 统一管理IUP控件名称常量,增强可维护性
refactor(core): 移除未使用的get_app_context函数
refactor(os_env): 修改backup_registry函数以返回错误码并备份到指定目录
refactor(path_manager): 增强路径管理函数的错误处理
2026-04-28 21:01:52 +08:00
Serendipity 86792012e2 feat(i18n): 为UI添加gettext国际化支持
- 将配置中的中文文本改为英文原文,作为翻译源字符串
- 在C代码中为所有UI字符串添加_()包装以启用gettext翻译
- 更新对话框、主窗口和配置文件的文本处理方式
2026-03-26 22:06:05 +08:00
Serendipity 4fe7dc47e4 feat(i18n): 添加多语言支持功能
- 新增国际化系统,支持中英文切换
- 添加语言选择对话框和语言切换按钮回调
- 扩展配置系统以支持语言设置存储
- 创建语言文件目录结构和占位文件
- 更新主窗口支持UI文本动态刷新
2026-03-26 20:44:22 +08:00
Serendipity 9a78b88c4a refactor(core): 统一使用ErrorCode并添加错误日志记录
- 将path_manager和import_export模块的返回值从int/void改为ErrorCode
- 在关键操作中添加日志记录,便于调试和错误追踪
- 更新调用代码以检查错误码并记录错误
- 修改运行命令以管理员权限启动程序
2026-03-26 18:27:38 +08:00
Serendipity 3af0e96060 refactor: 移除未使用的布局配置并增强日志与安全字符串
- 删除未使用的 layout_config.h 头文件及其引用
- 在 main.c 和 callbacks.c 中添加日志记录以追踪程序启动、关闭和关键操作
- 将多处 strncpy 调用替换为安全的 safe_strcpy 函数
- 在 dialogs.c 中引入 safe_string.h 以使用安全字符串函数
2026-03-26 13:20:34 +08:00
Serendipity 6ba7e702f2 refactor(registry): 使用ErrorCode类型替换int作为错误返回值
- 将registry_service.h/c中的函数返回值从int改为ErrorCode枚举
- 更新callbacks.c中的错误检查逻辑,使用ERR_OK常量进行比较
- 在内部辅助函数中返回具体的错误码(ERR_REGISTRY_FAILED等)
- 提高代码类型安全性和错误处理可读性
2026-03-26 13:02:14 +08:00
Serendipity 9aa1e208ba docs: 更新 README 中的架构与工具库说明
更新“架构与二次开发”章节的图标,并补充了项目中包含的开发工具库列表,如统一错误码系统、安全字符串函数等,以便开发者更全面地了解项目结构。
2026-03-26 12:50:29 +08:00
Serendipity d934d21323 feat: 新增安全字符串处理和日志系统模块
- 添加 safe_string 模块,提供安全的字符串复制、拼接和复制功能
- 添加 logger 模块,支持多级别日志记录和文件输出
- 添加 error_code 模块,定义统一的错误代码枚举
- 添加 layout_config 模块,定义布局配置结构
- 更新 CMakeLists.txt 包含新增的源文件
2026-03-26 12:47:44 +08:00
Serendipity 8767271e96 feat(导入导出): 支持同时导出和导入系统与用户PATH变量
- 重构导出功能,将系统变量和用户变量合并到单个JSON文件中
- 重构导入功能,支持解析包含系统变量和用户变量的JSON文件
- 在导入时提供选项让用户选择导入目标(系统变量、用户变量或全部)
- 更新UI交互逻辑,适配新的导入导出数据结构
- 改进JSON文件格式,包含版本信息和导出时间戳
2026-03-26 12:16:48 +08:00
Serendipity 55ff64b92d docs: 更新 README 以反映新增的导入导出功能
更新功能列表,新增“导入导出”模块的说明,包括 JSON 备份/恢复和旧版 TXT 格式兼容。
更新架构说明,将“统一配置中心”修改为“热配置系统”,以反映从 C++ 头文件到 Lua 配置文件的变更。
2026-03-25 19:56:10 +08:00
Serendipity 55d0f80743 feat: 新增导入导出功能,支持备份和恢复 PATH 配置
- 添加 import_export 模块,实现 JSON 格式的导入导出
- 在界面中添加导入和导出按钮,并配置回调函数
- 更新配置文件,增加相关文本和状态提示
- 在非管理员权限下禁用导入按钮
2026-03-25 19:49:47 +08:00
Serendipity ce232cb024 feat: 引入 Lua 配置系统实现 UI 参数热更新
- 添加 Lua 5.5 库支持,包含头文件和动态链接库
- 新增 lua_config 模块,提供配置初始化、获取字符串/整型值等功能
- 创建 config.lua 配置文件,集中管理所有 UI 文本、尺寸和布局参数
- 移除原有的硬编码 config.h,将 UI 常量迁移至 Lua 配置
- 修改主窗口、对话框和回调函数,动态读取 Lua 配置值
- 更新 CMakeLists.txt,添加 Lua 库依赖和 DLL 复制步骤
- 删除过时的 Makefile,统一使用 CMake 构建
2026-03-25 19:18:23 +08:00
Serendipity bd1b05be55 chore: 更新运行命令注释以反映最新的可执行文件路径 2026-03-19 22:21:33 +08:00
Serendipity a769a6b9b3 refactor: 重构项目为 MVC 架构并移除全局变量
- 将原有扁平目录结构重构为 MVC 分层架构:
  * src/core/: 核心业务逻辑(Model),完全独立于 UI 框架
  * src/ui/: 界面组件构建(View),负责纯视觉展示
  * src/controller/: 用户交互处理(Controller),连接数据与界面
  * src/utils/: 底层工具函数,专注于系统调用和字符串处理
- 引入 AppContext 结构体统一管理应用状态,替代原有的全局变量模式
- 重命名并重新组织头文件,按功能模块划分到对应子目录
- 更新 CMakeLists.txt 以适配新的目录结构
- 同步更新 README.md 文档,说明新的架构设计
2026-03-19 20:58:41 +08:00
Serendipity 6509ef98e4 chore: 清理旧版IUP库文件并更新头文件路径
删除旧的iup-3.31_Win64_dllw6_lib目录下的所有DLL和静态库文件
将IUP头文件从旧目录迁移到新的libs/IUP/include统一路径
更新CMakeLists.txt中的包含路径和库链接配置
简化DLL复制逻辑,只复制核心iup.dll文件
2026-03-19 20:14:06 +08:00
Serendipity c928c271e8 chore: 移除构建安装程序前的DLL复制步骤
不再需要手动复制IUP DLL文件,因为安装脚本已直接引用库目录。
2026-03-19 13:23:03 +08:00
Serendipity 02e702b285 fix(构建): 修复IUP DLL复制命令的路径变量
将CMAKE_SOURCE_DIR更改为CMAKE_CURRENT_SOURCE_DIR以确保在子目录中也能正确找到DLL文件。
移除不必要的条件判断,使复制命令始终执行。
2026-03-19 12:40:01 +08:00
Serendipity af3138c146 build: 重构 CMakeLists.txt 以使用现代 CMake 最佳实践
- 将项目声明更新为包含版本和语言
- 启用 RC 语言以正确处理资源文件
- 使用 target_* 命令替代全局命令(如 include_directories、link_directories)
- 将资源文件直接加入源文件列表,简化构建定义
- 优化 DLL 复制逻辑,使用 file(GLOB) 和 copy_if_different
- 改进编译器选项的条件设置,增强跨编译器兼容性
2026-03-19 12:37:41 +08:00
Serendipity 6e6adf3b85 chore: 迁移构建系统并清理遗留的二进制文件
- 删除 bin/ 目录下遗留的 DLL 和可执行文件,它们现在由 CMake 构建过程自动复制
- 更新 CMakeLists.txt,明确设置 C17 标准并优化编译选项
- 更新 Inno Setup 安装脚本,使其从 build/ 目录获取构建产物
- 更新 main.c 中的编译说明,反映当前基于 CMake 的构建流程
2026-03-19 12:32:54 +08:00
Serendipity e84b33c5ca build: 迁移项目构建系统至 CMake
- 新增 CMakeLists.txt 文件,定义项目构建规则、依赖和编译选项。
- 更新 README.md 文档,推荐使用 CMake 进行构建,并说明新旧构建方式。
- 保留原有的 Makefile 支持以保持向后兼容。
2026-03-19 12:07:01 +08:00
Serendipity ac6b409f3a feat: 为只读模式添加专用应用程序标题
在非管理员权限下运行时,将对话框标题从硬编码字符串改为使用配置文件中定义的 APP_NAME_READONLY 宏。这提高了代码的可维护性和一致性,使标题文本集中管理,便于未来修改。
2026-03-18 22:37:33 +08:00
Serendipity 1bbe95582a refactor: 将应用程序名称提取为配置常量
- 在 config.h 中定义 APP_NAME 常量,提高可维护性
- 将 main.c 中的对话框标题硬编码替换为使用 APP_NAME
2026-03-18 22:33:25 +08:00
Serendipity 3ecf35963d feat(ui): 增加对话框最小尺寸并调整默认大小
- 将对话框默认大小从500x400调整为800x800
- 添加MINSIZE属性确保对话框不可缩小
- 清理ui.c中的多余空白字符
- 在main.c中添加编译和打包说明注释
2026-03-18 22:03:57 +08:00
Serendipity 276d2c5fe3 docs(config.h): 为UI配置常量添加注释说明 2026-03-18 21:09:13 +08:00
Serendipity a9339f9b9f style(config): 统一宏定义格式并更新文档
调整 config.h 中 UI_DLG_SIZE 宏定义的对齐格式以保持代码风格一致。
在 README.md 中新增“架构与二次开发”章节,说明项目的模块化设计和配置管理方式。
2026-03-18 21:06:55 +08:00
Serendipity 7fac2aab35 refactor: 重构代码以提取配置和全局变量
- 将 Windows 消息常量和 UI 配置常量分别提取到 globals.h 和 config.h 头文件中,提高可维护性
- 将全局变量和控件定义从 main.c 移至独立的 globals.c 源文件,实现关注点分离
- 更新 Makefile 以包含新的源文件 globals.c
- 在 ui.c 和 main.c 中引用 config.h,使用配置常量替代硬编码的 UI 参数
2026-03-18 21:01:50 +08:00
Serendipity 7db190306c docs: 更新 README 项目描述与功能亮点
重写项目概述,使其更具吸引力和信息性。新增对目标用户、核心优势(如双视图、智能检测、自动备份)的说明,以更好地向潜在用户展示工具价值。
2026-03-16 20:32:16 +08:00
Serendipity 575fcca5c4 refactor: 提取UI组件到独立模块并改进拖拽支持
- 将列表、按钮等UI创建代码从main.c移至ui.c/ui.h
- 添加Windows UIPI消息过滤以支持管理员模式下的文件拖拽
- 更新Makefile和构建脚本以包含新的UI模块
- 清理旧的备份注册表文件并更新README文档
2026-03-16 20:15:10 +08:00
Serendipity 39d06e20e0 feat: 新增系统/用户变量分离、搜索、拖拽和清理功能
- 将单一列表拆分为系统和用户变量两个标签页
- 新增搜索框支持实时过滤路径
- 支持拖拽文件夹直接添加到列表
- 新增一键清理功能移除无效和重复路径
- 增加注册表备份机制和删除确认
- 优化界面布局和权限提示逻辑
2026-03-16 19:58:41 +08:00
Serendipity f21d302565 refactor: 移除未使用的Windows平台头文件包含
清理main.c中已不再需要的条件编译头文件引入,简化代码结构。
2026-03-16 18:49:47 +08:00
Serendipity adbbe099b9 docs: 添加 MIT 许可证文件
为项目添加标准的 MIT 许可证,明确软件的使用、复制、修改和分发权限。
2026-03-16 18:48:19 +08:00
2487 changed files with 136407 additions and 823 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(cmake --build build)"
]
}
}
+136
View File
@@ -0,0 +1,136 @@
# 定义项目信息
cmake_minimum_required(VERSION 3.10)
project(PathEditor VERSION 3.0 LANGUAGES C)
# 选项:是否构建测试
option(BUILD_TESTS "Build unit tests" OFF)
# 启用资源编译器以处理 .rc 文件
enable_language(RC)
# 设置 C 标准
set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF) # 禁用特定编译器的扩展(如 gnu17),强制使用标准 C17
# 定义源文件
set(SOURCES
src/main.c
src/utils/string_ext.c
src/utils/os_env.c
src/utils/safe_string.c
src/utils/logger.c
src/utils/i18n.c
src/utils/error_code.c
src/ui/ui_utils.c
src/ui/dialogs.c
src/ui/main_window.c
src/core/registry_service.c
src/core/path_manager.c
src/core/app_context.c
src/core/lua_config.c
src/core/import_export.c
src/core/undo_redo.c
src/controller/callbacks.c
src/controller/callbacks_basic.c
src/controller/callbacks_nav.c
src/controller/callbacks_search.c
src/controller/callbacks_io.c
src/controller/callbacks_sys.c
ico/resources.rc
)
# 创建 GUI 可执行文件(WIN32 属性会自动添加 -mwindows 参数)
add_executable(${PROJECT_NAME} WIN32 ${SOURCES})
# 添加宏定义
target_compile_definitions(${PROJECT_NAME} PRIVATE
_WIN32
UNICODE
_UNICODE
)
# 添加编译选项
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(${PROJECT_NAME} PRIVATE
-Wall
-O2
-fexec-charset=UTF-8
)
endif()
# 设置头文件搜索路径
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/include/core
${CMAKE_SOURCE_DIR}/include/ui
${CMAKE_SOURCE_DIR}/include/controller
${CMAKE_SOURCE_DIR}/include/utils
${CMAKE_SOURCE_DIR}/libs/IUP/include
${CMAKE_SOURCE_DIR}/libs/lua/include
${CMAKE_SOURCE_DIR}/libs/gettext/include
)
# 设置库文件搜索路径
target_link_directories(${PROJECT_NAME} PRIVATE
${CMAKE_SOURCE_DIR}/libs/IUP
${CMAKE_SOURCE_DIR}/libs/lua
${CMAKE_SOURCE_DIR}/libs/gettext/lib
)
# 链接所需库
target_link_libraries(${PROJECT_NAME} PRIVATE
lua55
iup
iupcd
gdi32
comdlg32
comctl32
uuid
ole32
advapi32
intl
iconv
)
# 编译完成后,复制程序实际需要的核心 DLL 文件到构建输出目录
set(IUP_REQUIRED_DLLS "${CMAKE_CURRENT_SOURCE_DIR}/libs/IUP/iup.dll")
set(LUA_REQUIRED_DLLS "${CMAKE_CURRENT_SOURCE_DIR}/libs/lua/lua55.dll")
set(GETTEXT_REQUIRED_DLLS "${CMAKE_CURRENT_SOURCE_DIR}/libs/gettext/bin/libintl-8.dll")
set(ICONV_REQUIRED_DLLS "${CMAKE_CURRENT_SOURCE_DIR}/libs/gettext/bin/libiconv-2.dll")
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${IUP_REQUIRED_DLLS}
"$<TARGET_FILE_DIR:${PROJECT_NAME}>"
COMMENT "Copying required DLLs to build directory..."
)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${LUA_REQUIRED_DLLS}
"$<TARGET_FILE_DIR:${PROJECT_NAME}>"
COMMENT "Copying Lua DLL to build directory..."
)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${GETTEXT_REQUIRED_DLLS}
"$<TARGET_FILE_DIR:${PROJECT_NAME}>"
COMMENT "Copying gettext DLL to build directory..."
)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${ICONV_REQUIRED_DLLS}
"$<TARGET_FILE_DIR:${PROJECT_NAME}>"
COMMENT "Copying iconv DLL to build directory..."
)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/locale
"$<TARGET_FILE_DIR:${PROJECT_NAME}>/locale"
COMMENT "Copying locale directory to build directory..."
)
# 测试支持
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
+21
View File
@@ -0,0 +1,21 @@
# MIT License
Copyright (c) 2026 LHY
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-54
View File
@@ -1,54 +0,0 @@
CC = gcc
WINDRES = windres
# Paths - specific to user environment
IUP_DIR = libs/iup-3.31_Win64_dllw6_lib
INCLUDE_DIR = $(IUP_DIR)/include
LIB_DIR = $(IUP_DIR)
LOCAL_INCLUDE_DIR = include
# Output Directories
OBJ_DIR = obj
BIN_DIR = bin
# Flags
# -mwindows: Create GUI app (no console)
# -DUNICODE -D_UNICODE: Use Wide Character API
CFLAGS = -Wall -O2 -I$(INCLUDE_DIR) -I$(LOCAL_INCLUDE_DIR) -D_WIN32 -DUNICODE -D_UNICODE -fexec-charset=UTF-8
LDFLAGS = -L$(LIB_DIR) -liup -liupcd -lgdi32 -lcomdlg32 -lcomctl32 -luuid -lole32 -ladvapi32 -mwindows
# Source
SRC = src/main.c src/utils.c src/registry.c src/callbacks.c
RES = ico/resources.rc
OBJ = $(OBJ_DIR)/main.o $(OBJ_DIR)/utils.o $(OBJ_DIR)/registry.o $(OBJ_DIR)/callbacks.o $(OBJ_DIR)/resources.o
EXE = $(BIN_DIR)/PathEditor.exe
all: $(BIN_DIR) $(OBJ_DIR) $(EXE)
$(BIN_DIR):
if not exist $(BIN_DIR) mkdir $(BIN_DIR)
$(OBJ_DIR):
if not exist $(OBJ_DIR) mkdir $(OBJ_DIR)
$(EXE): $(OBJ)
$(CC) -o $@ $^ $(LDFLAGS)
$(OBJ_DIR)/main.o: src/main.c
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJ_DIR)/utils.o: src/utils.c
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJ_DIR)/registry.o: src/registry.c
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJ_DIR)/callbacks.o: src/callbacks.c
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJ_DIR)/resources.o: ico/resources.rc
$(WINDRES) -i $< -o $@
clean:
if exist $(OBJ_DIR)\*.o del /Q $(OBJ_DIR)\*.o
if exist $(BIN_DIR)\*.exe del /Q $(BIN_DIR)\*.exe
+81 -15
View File
@@ -1,18 +1,71 @@
# Path Editor (系统环境变量编辑器)
一个简单、轻量级的 Windows 系统环境变量(PATH编辑器,基于 C 语言和 IUP 图形库开发。
* Path Editor 是一个专为 Windows 用户设计的系统环境变量(PATH管理工具。它基于原生 C 语言和 IUP 图形库开发,旨在替代 Windows 自带的简陋编辑界面
* 相比系统自带的编辑器,Path Editor 提供了更加直观的双视图(系统/用户变量)界面、智能的路径有效性检测、自动备份机制以及便捷的拖拽操作,让环境变量的管理变得安全、高效且轻松。无论您是开发者还是系统管理员,它都是您配置开发环境的得力助手。
## ✨ 功能特点
* **可视化编辑**:直观地查看和管理系统 PATH 环境变量。
* **安全操作**:必须以管理员身份运行才能保存更改,防止误操作
* **🛡️ 安全第一**
* **自动备份**:每次保存前自动备份,支持自定义备份目录(默认 `%APPDATA%/PathEditor/backups/`
* **只读模式**:非管理员运行时自动切换到只读模式,防止误操作。
* **权限检测**:智能检测当前运行权限。
* **📑 双视图管理**
* 完美支持 **System (系统变量)****User (用户变量)** 的分离查看与编辑。
* 清晰的 Tab 标签页切换。
* **🔴 智能诊断与维护**
* **无效路径高亮**:自动检测路径是否存在,不存在的显示为红色。
* **重复路径高亮**:自动检测重复项,重复的显示为橙色。
* **一键清理**:智能移除所有无效和重复的路径,保持环境整洁。
* **📂 高效交互**
* **拖拽支持**:直接将文件夹拖入窗口即可添加(支持管理员模式下的 UIPI 穿透)。
* **实时搜索**:顶部搜索框支持不区分大小写的实时过滤查找。
* **快捷键**:支持 Delete 键快速删除选中项。
* **🔄 导入导出**
* **导出备份**:将 PATH 导出为 JSON 文件,方便备份和迁移。
* **导入恢复**:从 JSON 文件导入路径配置。
* **格式兼容**:支持旧版 TXT 格式导入。
* **🌍 多语言支持**
* 内置中文和英文界面,支持运行时切换。
* 基于 gettext 国际化框架,易于扩展其他语言。
* **便捷管理**
* **新建**:添加新路径到列表。
* 📂 **浏览**:直接从文件资源管理器选择目录添加。
* ✏️ **编辑**:修改现有路径。
* ✏️ **编辑**双击或点击按钮修改现有路径。
* 🗑️ **删除**:移除不需要的路径。
* ⬆️⬇️ **排序**:上移/下移调整路径优先级。
* **轻量级**:原生 C 语言编写,运行速度快,占用资源少。
* **轻量级**:原生 C 语言编写,无臃肿依赖,运行速度极快。
## 🛠️ 架构与二次开发
本项目注重代码的模块化和可维护性,采用了经典的 **MVC 分层架构**,非常适合作为 C 语言桌面程序开发的参考:
* **分层设计**
* `src/core/` (Model): 核心数据与业务逻辑,完全脱离 UI 框架(无任何 `<iup.h>` 依赖)。
* `src/ui/` (View): 负责界面布局与组件的纯视觉展示。
* `src/controller/` (Controller): 负责连接用户交互与底层数据,已按功能拆分为 6 个模块:
* `callbacks.c` - 辅助函数与上下文管理
* `callbacks_basic.c` - 基础 CRUD 操作(新建、编辑、浏览、删除)
* `callbacks_nav.c` - 导航操作(上移、下移、清理)
* `callbacks_search.c` - 搜索过滤与拖拽
* `callbacks_io.c` - 导入导出
* `callbacks_sys.c` - 系统操作(保存、取消、帮助、语言切换)
* `src/utils/` (Utils): 纯粹的底层工具类封装(系统级调用、字符串处理)。
* **热配置系统**:所有 UI 参数(窗口大小、按钮文本、布局间距等)均通过 `lua/config.lua` 配置,修改无需重新编译即可生效。
* **国际化支持**:基于 gettext 框架,支持中英文运行时切换,语言文件位于 `locale/` 目录。
* **清晰的应用状态**:摒弃了脆弱的全局变量模式,采用 `AppContext` 统一管理应用运行时的上下文状态,通过指针传递,安全可靠。
* **开发工具库**
* 统一错误码系统 (`utils/error_code.h`) - 11 种细分错误码
* 安全字符串函数 (`utils/safe_string.h`)
* 字符串列表封装 (`utils/string_ext.h`) - 带访问器函数的安全动态数组
* 日志系统 (`utils/logger.h`) - 支持 DEBUG/INFO/WARN/ERROR 四个级别
* 控件名称常量 (`utils/ui_constants.h`) - 集中管理所有 IUP 控件名称
## 📦 下载与安装
@@ -28,10 +81,13 @@
* Windows 操作系统
* GCC 编译器 (推荐 MinGW-w64)
* Make 工具
* CMake 工具 (推荐使用 CMake 构建)
* IUP 库 (已包含在 `libs` 目录下)
* Inno Setup 6 (仅打包需要)
### 编译步骤
### 编译步骤 (推荐使用 CMake)
本项目已迁移至 CMake 构建系统,支持生成更标准的构建文件并集成到各大 IDE。
1. 克隆仓库:
@@ -40,30 +96,40 @@
cd PathEditor
```
2. 编译项目
2. 使用 CMake 配置和编译:
```bash
mingw32-make
# 生成构建系统 (以 MinGW 为例)
cmake -B build -G "MinGW Makefiles"
# 编译项目
cmake --build build
```
3. 运行:
编译成功后,可执行文件位于 `bin/PathEditor.exe`。
编译成功后,可执行文件位于 `build/PathEditor.exe`。
### 打包 (可选)
本项目使用 Inno Setup 生成安装包。
1. 确保已安装 [Inno Setup 6](https://jrsoftware.org/isdl.php)。
2. 编译项目生成 exe 文件
3. 使用 Inno Setup 编译 `dist/installer.iss` 脚本
2. 运行根目录下的 `build_installer.bat` 脚本
3. 生成的安装包将位于 `dist/dist/PathEditorSetup.exe`
## 📝 使用说明
1. **启动**:右键点击程序图标,选择“以管理员身份运行”。
2. **查看**:程序启动后会自动加载当前的系统 PATH 变量。
3. **修改**:使用右侧按钮栏进行添加、删除、移动等操作
4. **保存**:操作完成后,务必点击底部的【确定】按钮保存更改
5. **生效**保存后,某些正在运行的程序可能需要重启才能识别新的环境变量。CMD 或 PowerShell 窗口需要重新打开
* **红色**条目表示路径不存在
* **橙色**条目表示路径重复
3. **搜索**在顶部输入关键词可快速筛选
4. **修改**
* 拖拽文件夹到列表可直接添加。
* 使用右侧按钮栏进行常规操作。
* 点击“一键清理”可自动删除无效和重复项。
5. **保存**:操作完成后,务必点击底部的【确定】按钮保存更改。
6. **生效**:保存后,某些正在运行的程序可能需要重启才能识别新的环境变量。CMD 或 PowerShell 窗口需要重新打开。
## 👤 作者信息
Binary file not shown.
+12
View File
@@ -0,0 +1,12 @@
@echo off
echo Building Installer...
"D:\Program Files (x86)\Inno Setup 6\ISCC.exe" "dist\installer.iss"
if %ERRORLEVEL% NEQ 0 (
echo Installer build failed!
exit /b %ERRORLEVEL%
)
echo Done! Installer is in dist\dist\
pause
+2 -2
View File
@@ -37,8 +37,8 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: "d:\Code\doing_exercises\programs\PathEditor\bin\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "d:\Code\doing_exercises\programs\PathEditor\bin\*.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "d:\Code\doing_exercises\programs\PathEditor\build\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "d:\Code\doing_exercises\programs\PathEditor\build\*.dll"; DestDir: "{app}"; Flags: ignoreversion
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
+275
View File
@@ -0,0 +1,275 @@
# 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*
@@ -0,0 +1,422 @@
# 撤销/重做 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. 语言切换后按钮文本正确切换
@@ -0,0 +1,37 @@
# 全局快捷键 — 设计文档
## 背景
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` 保持不变
@@ -0,0 +1,65 @@
# 撤销/重做 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` 决定,保持现有行为)
@@ -0,0 +1,170 @@
# 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 读取失败:应视策略决定是否整体阻断。若当前目标是“完整备份后再保存”,则任一关键读取失败都应阻断。
- 仅保存阶段失败:备份已经成功生成时,仍要验证现有失败提示是否准确,避免把备份失败和写入失败混为一类。
-20
View File
@@ -1,20 +0,0 @@
#ifndef CALLBACKS_H
#define CALLBACKS_H
#include <iup.h>
// 按钮回调
int btn_new_cb(Ihandle* self);
int btn_edit_cb(Ihandle* self);
int btn_browse_cb(Ihandle* self);
int btn_del_cb(Ihandle* self);
int btn_up_cb(Ihandle* self);
int btn_down_cb(Ihandle* self);
int btn_ok_cb(Ihandle* self);
int btn_cancel_cb(Ihandle* self);
int btn_help_cb(Ihandle* self);
// 双击回调
int list_dblclick_cb(Ihandle* self, int item, char* text);
#endif // CALLBACKS_H
+42
View File
@@ -0,0 +1,42 @@
#ifndef CALLBACKS_H
#define CALLBACKS_H
#include <iup.h>
// 按钮回调
int btn_new_cb(Ihandle *self);
int btn_edit_cb(Ihandle *self);
int btn_browse_cb(Ihandle *self);
int btn_del_cb(Ihandle *self);
int btn_up_cb(Ihandle *self);
int btn_down_cb(Ihandle *self);
int btn_clean_cb(Ihandle *self);
int btn_import_cb(Ihandle *self);
int btn_export_cb(Ihandle *self);
int btn_ok_cb(Ihandle *self);
int btn_cancel_cb(Ihandle *self);
int btn_help_cb(Ihandle *self);
int btn_lang_cb(Ihandle *self);
int darkmode_cb(Ihandle *self);
// 撤销/重做回调
int btn_undo_cb(Ihandle *self);
int btn_redo_cb(Ihandle *self);
// 搜索回调
int txt_search_cb(Ihandle *self);
// 双击回调
int list_dblclick_cb(Ihandle *self, int item, char *text);
// 拖拽回调
int list_dropfiles_cb(Ihandle *self, const char *filename, int num, int x, int y);
// 键盘按键回调
int list_k_any_cb(Ihandle *self, int c);
int dlg_k_any_cb(Ihandle *self, int c);
// 载入数据与更新UI
void load_all_paths(void);
#endif // CALLBACKS_H
+29
View File
@@ -0,0 +1,29 @@
#ifndef CALLBACKS_INTERNAL_H
#define CALLBACKS_INTERNAL_H
#include <iup.h>
#include "core/app_context.h"
#include "utils/i18n.h"
// 内部辅助函数声明(供各 callbacks_*.c 文件共享)
// 这些函数不对外暴露,仅在 controller 层内部使用
// 获取主对话框句柄
Ihandle *get_main_dlg(void);
// 从对话框获取应用上下文
AppContext *get_app_context_from_dlg(Ihandle *dlg);
// 获取当前活动的数据列表(根据 Tab 页切换)
StringList *get_current_raw_data(Ihandle *dlg);
// 获取当前活动的列表 UI 控件
Ihandle *get_current_list(Ihandle *dlg);
// 刷新撤销/重做按钮的启用状态
void refresh_undo_redo_buttons(Ihandle *dlg);
// 同步合并预览列表
void sync_merged_list(Ihandle *dlg);
#endif // CALLBACKS_INTERNAL_H
+20
View File
@@ -0,0 +1,20 @@
#ifndef APP_CONTEXT_H
#define APP_CONTEXT_H
#include "utils/string_ext.h"
#include "core/undo_redo.h"
// 应用上下文结构体,用于存储应用运行时的状态
typedef struct {
StringList sys_paths;
StringList user_paths;
UndoRedoManager *undo_redo_mgr; // 撤销/重做管理器
} AppContext;
// 创建应用上下文
AppContext* create_app_context(void);
// 销毁应用上下文
void destroy_app_context(AppContext* ctx);
#endif // APP_CONTEXT_H
+35
View File
@@ -0,0 +1,35 @@
#ifndef IMPORT_EXPORT_H
#define IMPORT_EXPORT_H
#include "utils/string_ext.h"
#include "utils/error_code.h"
#define EXPORT_VERSION "1.0"
// 导出数据类型
typedef enum {
EXPORT_JSON, // JSON 格式
EXPORT_CSV // CSV 格式
} ExportFormat;
// 导出数据结构
// 注意:此结构体用于导出时是只读的,items 指针指向外部 StringList 的数据
// 不要对 ExportData 调用 clear_string_list,会破坏原始数据
typedef struct {
StringList system;
StringList user;
} ExportData;
// 导出 PATH 到文件(自动根据扩展名选择格式)
ErrorCode export_paths_to_file(const ExportData *data, const char *filepath);
// 导出 PATH 到指定格式的文件
ErrorCode export_paths_to_format(const ExportData *data, const char *filepath, ExportFormat format);
// 从文件导入 PATH
ErrorCode import_paths_from_file(const char *filepath, ExportData *data);
// 验证路径格式是否有效
int is_valid_path_format(const char *path);
#endif // IMPORT_EXPORT_H
+41
View File
@@ -0,0 +1,41 @@
#ifndef LUA_CONFIG_H
#define LUA_CONFIG_H
#include <lua.h>
// 初始化 Lua 配置系统
// 返回值: 0 成功, -1 失败
int lua_config_init(void);
// 销毁 Lua 配置系统
void lua_config_destroy(void);
// 获取字符串配置值
// section: 配置章节名 (如 "app", "dialog", "button")
// key: 配置键名 (如 "name", "size", "rastersize")
// 返回值: 配置值字符串, 失败时返回 NULL
const char* lua_config_get_string(const char* section, const char* key);
// 获取整型配置值
// section: 配置章节名
// key: 配置键名
// default_value: 默认值 (当配置不存在或转换失败时返回)
// 返回值: 配置值或默认值
int lua_config_get_int(const char* section, const char* key, int default_value);
// 重新加载配置文件
// 返回值: 0 成功, -1 失败
int lua_config_reload(void);
// 获取配置加载状态
// 返回值: 1 已加载, 0 未加载
int lua_config_is_loaded(void);
// 设置字符串配置值
// section: 配置章节名
// key: 配置键名
// value: 配置值
// 返回值: 0 成功, -1 失败
int lua_config_set_string(const char *section, const char *key, const char *value);
#endif // LUA_CONFIG_H
+19
View File
@@ -0,0 +1,19 @@
#ifndef PATH_MANAGER_H
#define PATH_MANAGER_H
#include "utils/string_ext.h"
#include "utils/error_code.h"
// 移除列表中指定索引的项
ErrorCode path_manager_remove_at(StringList *list, int index);
// 上移指定索引的项
ErrorCode path_manager_move_up(StringList *list, int index);
// 下移指定索引的项
ErrorCode path_manager_move_down(StringList *list, int index);
// 清理无效和重复的路径
ErrorCode path_manager_clean(StringList *list);
#endif // PATH_MANAGER_H
+15
View File
@@ -0,0 +1,15 @@
#ifndef REGISTRY_SERVICE_H
#define REGISTRY_SERVICE_H
#include "utils/string_ext.h"
#include "utils/error_code.h"
// 加载系统变量和用户变量到字符串列表
ErrorCode load_system_paths(StringList *list);
ErrorCode load_user_paths(StringList *list);
// 从字符串列表保存系统变量和用户变量
ErrorCode save_system_paths(const StringList *list);
ErrorCode save_user_paths(const StringList *list);
#endif // REGISTRY_SERVICE_H
+66
View File
@@ -0,0 +1,66 @@
#ifndef UNDO_REDO_H
#define UNDO_REDO_H
#include "utils/string_ext.h"
// 操作类型
typedef enum {
OP_ADD, // 添加路径
OP_DELETE, // 删除路径
OP_EDIT, // 编辑路径
OP_MOVE_UP, // 上移
OP_MOVE_DOWN, // 下移
OP_CLEAN, // 清理(批量删除)
OP_CLEAR, // 清空列表
OP_IMPORT // 导入
} OperationType;
// 目标类型(哪个列表)
typedef enum {
TARGET_SYSTEM, // 系统变量
TARGET_USER // 用户变量
} TargetType;
// 单个操作记录
typedef struct {
OperationType type;
TargetType target;
int index;
int count; // 用于批量操作(如清理、导入)
char **old_paths; // 操作前的路径列表(用于撤销)
char **new_paths; // 操作后的路径列表(用于重做)
} OpRecord;
// 撤销/重做管理器
typedef struct {
OpRecord *records;
int max_size; // 最大历史记录数
int current; // 当前指针位置(-1表示最新操作之后)
int count; // 实际记录数
} UndoRedoManager;
// 创建撤销/重做管理器
UndoRedoManager *create_undo_redo_manager(int max_size);
// 销毁撤销/重做管理器
void destroy_undo_redo_manager(UndoRedoManager *mgr);
// 添加操作记录
int push_undo_record(UndoRedoManager *mgr, const OpRecord *record);
// 执行撤销
int undo(UndoRedoManager *mgr, StringList *sys_paths, StringList *user_paths);
// 执行重做
int redo(UndoRedoManager *mgr, StringList *sys_paths, StringList *user_paths);
// 检查是否可以撤销
int can_undo(const UndoRedoManager *mgr);
// 检查是否可以重做
int can_redo(const UndoRedoManager *mgr);
// 清空历史记录
void clear_undo_redo_history(UndoRedoManager *mgr);
#endif // UNDO_REDO_H
-24
View File
@@ -1,24 +0,0 @@
#ifndef GLOBALS_H
#define GLOBALS_H
#include <iup.h>
// 注册表路径常量
#define REG_PATH L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"
#define REG_VALUE L"Path"
// 全局控件句柄声明
extern Ihandle *dlg; // 主对话框句柄
extern Ihandle *list_path; // 路径列表控件句柄
extern Ihandle *lbl_status; // 状态标签句柄
extern Ihandle *btn_new; // 新增按钮句柄
extern Ihandle *btn_edit; // 编辑按钮句柄
extern Ihandle *btn_browse; // 浏览按钮句柄
extern Ihandle *btn_del; // 删除按钮句柄
extern Ihandle *btn_up; // 上移按钮句柄
extern Ihandle *btn_down; // 下移按钮句柄
extern Ihandle *btn_ok; // 确认按钮句柄
extern Ihandle *btn_cancel; // 取消按钮句柄
extern Ihandle *btn_help; // 帮助按钮句柄
#endif // GLOBALS_H
-10
View File
@@ -1,10 +0,0 @@
#ifndef REGISTRY_H
#define REGISTRY_H
// 从注册表加载PATH到列表控件
void load_path();
// 将列表控件中的PATH保存回注册表
void save_path();
#endif // REGISTRY_H
+12
View File
@@ -0,0 +1,12 @@
#ifndef DIALOGS_H
#define DIALOGS_H
// 自定义输入对话框
// 返回值:0-取消,1-确认
int custom_input_dialog(const char *title, const char *label_text, char *buffer, int buffer_size);
// 语言选择对话框
// 返回值:0-取消,1-确认
int language_select_dialog(void);
#endif // DIALOGS_H
+12
View File
@@ -0,0 +1,12 @@
#ifndef MAIN_WINDOW_H
#define MAIN_WINDOW_H
#include <iup.h>
// 创建主窗口
Ihandle* create_main_window(void);
// 刷新 UI 文本(语言切换时调用)
void refresh_main_window_ui(Ihandle *main_dlg);
#endif // MAIN_WINDOW_H
+24
View File
@@ -0,0 +1,24 @@
#ifndef UI_UTILS_H
#define UI_UTILS_H
#include <iup.h>
#include "utils/string_ext.h"
// 刷新单个列表框样式
// 功能说明:
// 1. 路径有效性检查:无效路径显示红色前景色 (255 0 0)
// 2. 重复检查:重复路径显示橙色前景色 (255 128 0),只检查当前项之前的项
// 3. 斑马纹背景:奇偶行交替显示不同背景色 (白/灰)
// 注意: 该函数需要IUP控件已设置NAME属性
void refresh_single_list_style(Ihandle *list);
// 同步字符串列表到 UI 列表框
// 将StringList中的所有项同步到IUP FlatList控件中
// 会先清空列表然后重新添加所有项,最后刷新样式
void sync_string_list_to_ui(Ihandle *list_ui, const StringList *str_list);
// 深色模式状态管理
void set_dark_mode(int enabled);
int get_dark_mode(void);
#endif // UI_UTILS_H
-19
View File
@@ -1,19 +0,0 @@
#ifndef UTILS_H
#define UTILS_H
#include <windows.h>
#include <wchar.h>
// 宽字符转UTF-8
char* wide_to_utf8(const wchar_t* wstr);
// UTF-8转宽字符
wchar_t* utf8_to_wide(const char* str);
// 检查管理员权限
int check_admin();
// 刷新列表样式(斑马纹)
void refresh_list_style();
#endif // UTILS_H
+21
View File
@@ -0,0 +1,21 @@
#ifndef ERROR_CODE_H
#define ERROR_CODE_H
typedef enum {
ERR_OK = 0, // 成功
ERR_FAILED = -1, // 失败
ERR_NULL_PTR = -2, // 空指针
ERR_OUT_OF_MEMORY = -3, // 内存不足
ERR_FILE_NOT_FOUND = -4, // 文件不存在
ERR_PERMISSION_DENIED = -5, // 权限拒绝
ERR_INVALID_FORMAT = -6, // 无效格式
ERR_REGISTRY_FAILED = -7, // 注册表操作失败
ERR_NOT_FOUND = -8, // 未找到
ERR_EXISTS = -9, // 已存在
ERR_INVALID_INDEX = -10 // 无效索引
} ErrorCode;
// 获取错误码的字符串表示(英文,用于日志)
const char* error_code_to_string(ErrorCode code);
#endif // ERROR_CODE_H
+23
View File
@@ -0,0 +1,23 @@
#ifndef I18N_H
#define I18N_H
#include <libintl.h>
#include <locale.h>
#ifndef _
#define _(s) gettext(s)
#endif
// 初始化国际化系统
void i18n_init(const char* default_lang);
// 检测系统语言并返回语言代码
const char* i18n_detect_system_language(void);
// 切换语言
void i18n_change_language(const char* lang);
// 获取当前语言
const char* i18n_get_current_language(void);
#endif // I18N_H
+33
View File
@@ -0,0 +1,33 @@
#ifndef LOGGER_H
#define LOGGER_H
// 日志级别
typedef enum {
LOG_LEVEL_DEBUG, // 调试日志级别
LOG_LEVEL_INFO, // 信息日志级别
LOG_LEVEL_WARN, // 警告日志级别
LOG_LEVEL_ERROR // 错误日志级别
} LogLevel;
// 初始化日志系统
void log_init(const char *log_file, LogLevel level);
// 销毁日志系统
void log_destroy(void);
// 日志函数
void log_debug(const char *fmt, ...);
// 信息日志函数
void log_info(const char *fmt, ...);
// 警告日志函数
void log_warn(const char *fmt, ...);
// 错误日志函数
void log_error(const char *fmt, ...);
// 设置日志级别
void log_set_level(LogLevel level);
#endif // LOGGER_H
+16
View File
@@ -0,0 +1,16 @@
#ifndef OS_ENV_H
#define OS_ENV_H
#include "utils/error_code.h"
// 检查是否以管理员权限运行
int check_admin(void);
// 检查路径是否有效
int is_path_valid(const char *path);
// 备份注册表
// 参数 backup_path: 自定义备份目录路径,传 NULL 使用 Lua 配置中的默认路径
ErrorCode backup_registry(const char *backup_path);
#endif // OS_ENV_H
+15
View File
@@ -0,0 +1,15 @@
#ifndef SAFE_STRING_H
#define SAFE_STRING_H
#include <stddef.h>
// 安全字符串操作函数
char* safe_strcpy(char *dst, size_t dst_size, const char *src);
// 安全字符串拼接函数
char* safe_strcat(char *dst, size_t dst_size, const char *src);
// 安全字符串复制函数
char* safe_strdup(const char *src);
#endif // SAFE_STRING_H
+37
View File
@@ -0,0 +1,37 @@
#ifndef STRING_EXT_H
#define STRING_EXT_H
#include <wchar.h>
// 简单字符串列表结构
typedef struct
{
char **items;
int count;
int capacity;
} StringList;
// 字符串列表
void init_string_list(StringList *list);
void add_string_list(StringList *list, const char *str);
void clear_string_list(StringList *list);
// 访问器函数 - 安全访问内部数据
// 获取指定索引的字符串(只读),越界返回 NULL
const char *string_list_get(const StringList *list, int index);
// 设置指定索引的字符串(会复制新字符串并释放旧字符串),越界返回 -1,成功返回 0
int string_list_set(StringList *list, int index, const char *str);
// 字符串转换函数
char *wide_to_utf8(const wchar_t *wstr);
wchar_t *utf8_to_wide(const char *str);
char *stristr(const char *haystack, const char *needle);
// 检查字符串列表中是否存在指定路径(不区分大小写)
int string_list_contains(const StringList *list, const char *str);
// 展开环境变量(如 %JAVA_HOME%\bin → C:\Java\bin
// 返回 malloc 分配的字符串,调用者负责释放;无变量返回 NULL
char *expand_env_vars(const char *path);
#endif // STRING_EXT_H
+44
View File
@@ -0,0 +1,44 @@
#ifndef UI_CONSTANTS_H
#define UI_CONSTANTS_H
// 缓冲区大小常量
#define PATH_BUFFER_SIZE 4096
// 控件名称常量 - 统一管理所有IUP控件名称字符串
// 使用这些常量替代硬编码字符串,便于维护和减少拼写错误
// 列表控件
#define CTRL_LIST_SYS "LIST_SYS"
#define CTRL_LIST_USER "LIST_USER"
#define CTRL_LIST_MERGED "LIST_MERGED"
// 选项卡
#define CTRL_TABS_MAIN "TABS_MAIN"
// 搜索框
#define CTRL_TXT_SEARCH "TXT_SEARCH"
// 状态标签
#define CTRL_LBL_STATUS "LBL_STATUS"
// 操作按钮
#define CTRL_BTN_NEW "BTN_NEW"
#define CTRL_BTN_EDIT "BTN_EDIT"
#define CTRL_BTN_BROWSE "BTN_BROWSE"
#define CTRL_BTN_DEL "BTN_DEL"
#define CTRL_BTN_UP "BTN_UP"
#define CTRL_BTN_DOWN "BTN_DOWN"
#define CTRL_BTN_CLEAN "BTN_CLEAN"
#define CTRL_BTN_IMPORT "BTN_IMPORT"
#define CTRL_BTN_EXPORT "BTN_EXPORT"
#define CTRL_BTN_OK "BTN_OK"
#define CTRL_BTN_CANCEL "BTN_CANCEL"
#define CTRL_BTN_HELP "BTN_HELP"
#define CTRL_BTN_LANG "BTN_LANG"
#define CTRL_BTN_DARKMODE "BTN_DARKMODE"
// 撤销/重做按钮
#define CTRL_BTN_UNDO "BTN_UNDO"
#define CTRL_BTN_REDO "BTN_REDO"
#endif // UI_CONSTANTS_H
View File
View File
View File

Some files were not shown because too many files have changed in this diff Show More