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
-27
View File
@@ -1,27 +0,0 @@
#include "core/app_context.h"
#include <stdlib.h>
// 创建应用上下文
AppContext *create_app_context(void)
{
AppContext *ctx = (AppContext *)malloc(sizeof(AppContext));
if (ctx)
{
init_string_list(&ctx->sys_paths);
init_string_list(&ctx->user_paths);
ctx->undo_redo_mgr = create_undo_redo_manager(50);
}
return ctx;
}
// 销毁应用上下文
void destroy_app_context(AppContext *ctx)
{
if (ctx)
{
clear_string_list(&ctx->sys_paths);
clear_string_list(&ctx->user_paths);
destroy_undo_redo_manager(ctx->undo_redo_mgr);
free(ctx);
}
}
+208
View File
@@ -0,0 +1,208 @@
/**
* 导入导出模块 — 对应 C 版 import_export.c
* 支持 JSON、CSV、TXT 三种格式
*/
export type ExportFormat = 'json' | 'csv';
export interface ExportData {
system: string[];
user: string[];
}
/** 根据文件扩展名检测格式 */
export function detectExportFormat(filepath: string): ExportFormat {
if (filepath.toLowerCase().endsWith('.csv')) return 'csv';
return 'json';
}
// ── JSON 导出 ──
export function exportToJson(data: ExportData): string {
const obj = {
version: '1.0',
type: 'PathEditor',
exported: new Date().toISOString(),
system: data.system,
user: data.user,
};
return JSON.stringify(obj, null, 2);
}
// ── CSV 导出 ──
export function exportToCsv(data: ExportData): string {
const lines: string[] = [];
// UTF-8 BOM
lines.push('type,path');
for (const path of data.system) {
lines.push(`system,${escapeCsvField(path)}`);
}
for (const path of data.user) {
lines.push(`user,${escapeCsvField(path)}`);
}
return lines.join('\n') + '\n';
}
function escapeCsvField(field: string): string {
if (field.includes(',') || field.includes('"') || field.includes('\n')) {
return `"${field.replace(/"/g, '""')}"`;
}
return field;
}
// ── CSV 导入 ──
export interface ImportResult {
system: string[];
user: string[];
}
export function importFromCsv(content: string): ImportResult {
const result: ImportResult = { system: [], user: [] };
const lines = content.split(/\r?\n/);
let hasHeader = false;
for (const rawLine of lines) {
// 跳过 BOM
let line = rawLine;
if (line.startsWith('')) {
line = line.slice(1);
}
if (line.trim() === '') continue;
const fields = parseCsvLine(line);
if (fields.length < 2) continue;
// 检测头部行
if (!hasHeader && isHeaderRow(fields[0], fields[1])) {
hasHeader = true;
continue;
}
const type = fields[0].trim().toLowerCase();
const path = fields[1].trim();
if (path.length === 0) continue;
if (type === 'system') {
result.system.push(path);
} else if (type === 'user') {
result.user.push(path);
}
// 未知类型忽略
}
return result;
}
function isHeaderRow(col0: string, col1: string): boolean {
const c0 = col0.trim().toLowerCase();
const c1 = col1.trim().toLowerCase();
return c0 === 'type' && c1 === 'path';
}
function parseCsvLine(line: string): string[] {
const fields: string[] = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const ch = line[i];
if (inQuotes) {
if (ch === '"') {
if (i + 1 < line.length && line[i + 1] === '"') {
current += '"';
i++; // 跳过转义引号
} else {
inQuotes = false;
}
} else {
current += ch;
}
} else {
if (ch === '"') {
inQuotes = true;
} else if (ch === ',') {
fields.push(current);
current = '';
} else {
current += ch;
}
}
}
fields.push(current);
return fields;
}
// ── JSON 导入 ──
export function importFromJson(content: string): ImportResult {
const result: ImportResult = { system: [], user: [] };
const obj = JSON.parse(content);
if (Array.isArray(obj.system)) {
result.system = obj.system.filter(
(p: unknown) => typeof p === 'string' && p.trim().length > 0,
);
}
if (Array.isArray(obj.user)) {
result.user = obj.user.filter(
(p: unknown) => typeof p === 'string' && p.trim().length > 0,
);
}
return result;
}
// ── TXT 导入 ──
export function importFromTxt(content: string): string[] {
const paths: string[] = [];
const lines = content.split(/\r?\n/);
for (let line of lines) {
// 跳过 BOM
if (line.startsWith('')) line = line.slice(1);
const trimmed = line.trim();
if (trimmed.length === 0 || trimmed.startsWith('#')) continue;
paths.push(trimmed);
}
return paths;
}
// ── 自动检测导入 ──
export function importFromContent(
content: string,
filepath: string,
): ImportResult {
const ext = filepath.toLowerCase();
if (ext.endsWith('.csv')) {
return importFromCsv(content);
} else if (ext.endsWith('.json')) {
return importFromJson(content);
} else {
// TXT 文件:所有路径放入 system(用户后续可选择目标)
return { system: importFromTxt(content), user: [] };
}
}
/** 将 ImportResult 合并为单个路径数组 */
export function flattenImportResult(
result: ImportResult,
target: 'system' | 'user' | 'both',
): ExportData {
if (target === 'system') return { system: result.system, user: [] };
if (target === 'user') return { system: [], user: result.user };
return result; // both
}
-621
View File
@@ -1,621 +0,0 @@
#include "core/import_export.h"
#include "utils/os_env.h"
#include "utils/error_code.h"
#include "utils/logger.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
// 获取当前日期时间
static void get_current_datetime(char *buffer, int size)
{
time_t now = time(NULL);
struct tm tm_info;
localtime_s(&tm_info, &now);
strftime(buffer, size, "%Y-%m-%d %H:%M:%S", &tm_info);
}
// 转义 JSON 字符串中的特殊字符(符合 RFC 8259 规范)
static char *escape_json_string(const char *str)
{
if (!str)
return NULL;
int len = strlen(str);
// 最坏情况:每个字符都需要 \uXXXX 转义(6字节)
char *result = (char *)malloc(len * 6 + 1);
if (!result)
return NULL;
char *p = result;
for (int i = 0; i < len; i++)
{
unsigned char c = (unsigned char)str[i];
switch (c)
{
case '\\':
*p++ = '\\';
*p++ = '\\';
break;
case '"':
*p++ = '\\';
*p++ = '"';
break;
case '\n':
*p++ = '\\';
*p++ = 'n';
break;
case '\r':
*p++ = '\\';
*p++ = 'r';
break;
case '\t':
*p++ = '\\';
*p++ = 't';
break;
case '\b':
*p++ = '\\';
*p++ = 'b';
break;
case '\f':
*p++ = '\\';
*p++ = 'f';
break;
default:
if (c < 0x20) // 其他控制字符 (0x00-0x1F)
{
sprintf(p, "\\u%04x", c);
p += 6;
}
else
{
*p++ = str[i];
}
break;
}
}
*p = '\0';
return result;
}
// 转义 CSV 字段中的特殊字符
static char *escape_csv_field(const char *str)
{
if (!str)
return NULL;
int len = strlen(str);
// 需要转义双引号和包含逗号、引号、换行的字段
char *result = (char *)malloc(len * 2 + 3);
if (!result)
return NULL;
char *p = result;
*p++ = '"';
for (int i = 0; i < len; i++)
{
unsigned char c = (unsigned char)str[i];
switch (c)
{
case '"':
*p++ = '"';
*p++ = '"';
break;
default:
*p++ = str[i];
break;
}
}
*p++ = '"';
*p = '\0';
return result;
}
// 导出 PATH 到 JSON 文件
static ErrorCode export_paths_to_json(const ExportData *data, FILE *fp)
{
char datetime[64];
get_current_datetime(datetime, sizeof(datetime));
fprintf(fp, "{\n");
fprintf(fp, " \"version\": \"%s\",\n", EXPORT_VERSION);
fprintf(fp, " \"type\": \"ALL\",\n");
fprintf(fp, " \"exported\": \"%s\",\n", datetime);
fprintf(fp, " \"system\": [\n");
for (int i = 0; i < data->system.count; i++)
{
if (data->system.items[i])
{
char *escaped = escape_json_string(data->system.items[i]);
if (escaped)
{
fprintf(fp, " \"%s\"%s\n", escaped, (i < data->system.count - 1) ? "," : "");
free(escaped);
}
}
}
fprintf(fp, " ],\n");
fprintf(fp, " \"user\": [\n");
for (int i = 0; i < data->user.count; i++)
{
if (data->user.items[i])
{
char *escaped = escape_json_string(data->user.items[i]);
if (escaped)
{
fprintf(fp, " \"%s\"%s\n", escaped, (i < data->user.count - 1) ? "," : "");
free(escaped);
}
}
}
fprintf(fp, " ]\n");
fprintf(fp, "}\n");
return ERR_OK;
}
// 导出 PATH 到 CSV 文件
// 格式:type,path
// type: system 或 user
static ErrorCode export_paths_to_csv(const ExportData *data, FILE *fp)
{
// 写入 UTF-8 BOM
fprintf(fp, "\xEF\xBB\xBF");
// 写入 CSV 标题行
fprintf(fp, "type,path\n");
// 写入系统路径
for (int i = 0; i < data->system.count; i++)
{
if (data->system.items[i])
{
char *escaped = escape_csv_field(data->system.items[i]);
if (escaped)
{
fprintf(fp, "system,%s\n", escaped);
free(escaped);
}
}
}
// 写入用户路径
for (int i = 0; i < data->user.count; i++)
{
if (data->user.items[i])
{
char *escaped = escape_csv_field(data->user.items[i]);
if (escaped)
{
fprintf(fp, "user,%s\n", escaped);
free(escaped);
}
}
}
return ERR_OK;
}
// 导出 PATH 到文件
ErrorCode export_paths_to_file(const ExportData *data, const char *filepath)
{
if (!data || !filepath)
return ERR_NULL_PTR;
const char *ext = strrchr(filepath, '.');
if (ext && _stricmp(ext, ".csv") == 0)
{
return export_paths_to_format(data, filepath, EXPORT_CSV);
}
return export_paths_to_format(data, filepath, EXPORT_JSON);
}
// 导出 PATH 到指定格式的文件
ErrorCode export_paths_to_format(const ExportData *data, const char *filepath, ExportFormat format)
{
if (!data || !filepath)
return ERR_NULL_PTR;
FILE *fp = fopen(filepath, "w");
if (!fp)
{
log_error("Failed to open file for export: %s", filepath);
return ERR_FILE_NOT_FOUND;
}
ErrorCode result;
if (format == EXPORT_CSV)
result = export_paths_to_csv(data, fp);
else
result = export_paths_to_json(data, fp);
fclose(fp);
if (result == ERR_OK)
{
log_info("Exported paths to file: sys=%d, user=%d, format=%d, file=%s",
data->system.count, data->user.count, format, filepath);
}
return result;
}
// 移除字符串首尾的空格、制表符、换行符和回车符
static void trim_whitespace(char *str)
{
if (!str || *str == '\0')
return;
char *start = str;
while (*start == ' ' || *start == '\t')
start++;
char *end = str + strlen(str) - 1;
while (end >= start && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r'))
*end-- = '\0';
if (start != str)
memmove(str, start, strlen(start) + 1);
}
// 检查字符串是否为注释行或空行
static int is_comment_or_empty(const char *line)
{
while (*line == ' ' || *line == '\t')
line++;
if (*line == '#' || *line == '\0')
return 1;
return 0;
}
// 检查文件是否为 JSON 格式
static int is_json_file(const char *filepath)
{
const char *ext = strrchr(filepath, '.');
return ext && _stricmp(ext, ".json") == 0;
}
// 检查文件是否为 CSV 格式(通过扩展名)
static int is_csv_file(const char *filepath)
{
const char *ext = strrchr(filepath, '.');
return ext && _stricmp(ext, ".csv") == 0;
}
// 解析 CSV 字段(处理引号包围的字段)
// 返回值:指向下一个字段的指针,或 NULL
static const char *parse_csv_field(const char *line, char *field, int field_size)
{
if (!line || !field || field_size <= 0)
return NULL;
const char *p = line;
char *out = field;
char *end = field + field_size - 1;
if (*p == '"')
{
// 引号包围的字段
p++; // 跳过开始引号
while (*p && out < end)
{
if (*p == '"')
{
if (*(p + 1) == '"')
{
// 转义的引号 ""
*out++ = '"';
p += 2;
}
else
{
// 结束引号
p++; // 跳过结束引号
break;
}
}
else
{
*out++ = *p++;
}
}
// 跳过逗号分隔符
if (*p == ',') p++;
}
else
{
// 非引号字段(按逗号分隔)
while (*p && *p != ',' && out < end)
{
*out++ = *p++;
}
if (*p == ',') p++;
}
*out = '\0';
return p;
}
// 导入 CSV 格式的 PATH 文件
static ErrorCode import_paths_from_csv(const char *filepath, ExportData *data)
{
FILE *fp = fopen(filepath, "rb");
if (!fp)
{
log_error("Failed to open file for import: %s", filepath);
return ERR_FILE_NOT_FOUND;
}
char line[4096];
int line_num = 0;
int header_skipped = 0;
while (fgets(line, sizeof(line), fp))
{
line_num++;
trim_whitespace(line);
if (line[0] == '\0')
continue;
// 跳过 UTF-8 BOM
const char *start = line;
if ((unsigned char)start[0] == 0xEF &&
(unsigned char)start[1] == 0xBB &&
(unsigned char)start[2] == 0xBF)
{
start += 3;
}
// 智能检测标题行:第一行包含 "type" 和 "path" 则视为标题
if (!header_skipped)
{
header_skipped = 1;
char lower[256];
strncpy(lower, start, sizeof(lower) - 1);
lower[sizeof(lower) - 1] = '\0';
for (char *p = lower; *p; p++) *p = (char)tolower((unsigned char)*p);
if (strstr(lower, "type") && strstr(lower, "path"))
continue;
}
char type[32] = {0};
char path[4096] = {0};
start = parse_csv_field(start, type, sizeof(type));
if (!start)
continue;
parse_csv_field(start, path, sizeof(path));
if (path[0] == '\0')
continue;
if (_stricmp(type, "system") == 0)
add_string_list(&data->system, path);
else if (_stricmp(type, "user") == 0)
add_string_list(&data->user, path);
}
fclose(fp);
log_info("Imported paths from CSV file: sys=%d, user=%d, file=%s",
data->system.count, data->user.count, filepath);
return ERR_OK;
}
// 检查引号前是否有奇数个连续反斜杠(奇数个表示引号被转义)
static int is_quote_escaped(const char *quote_pos, const char *line_start)
{
int backslash_count = 0;
const char *p = quote_pos - 1;
while (p >= line_start && *p == '\\')
{
backslash_count++;
p--;
}
return (backslash_count % 2) == 1; // 奇数个反斜杠表示转义
}
// 从文件导入 PATH
ErrorCode import_paths_from_file(const char *filepath, ExportData *data)
{
if (!filepath || !data)
return ERR_NULL_PTR;
init_string_list(&data->system);
init_string_list(&data->user);
if (is_csv_file(filepath))
{
return import_paths_from_csv(filepath, data);
}
if (!is_json_file(filepath))
{
FILE *fp = fopen(filepath, "rb");
if (!fp)
{
log_error("Failed to open file for import: %s", filepath);
return ERR_FILE_NOT_FOUND;
}
StringList list;
init_string_list(&list);
char line[4096];
while (fgets(line, sizeof(line), fp))
{
trim_whitespace(line);
if (is_comment_or_empty(line))
continue;
add_string_list(&list, line);
}
fclose(fp);
data->system = list;
log_info("Imported paths from TXT file: %d paths, file=%s", list.count, filepath);
return ERR_OK;
}
FILE *fp = fopen(filepath, "rb");
if (!fp)
{
log_error("Failed to open file for import: %s", filepath);
return ERR_FILE_NOT_FOUND;
}
char buffer[8192];
int in_system = 0;
int in_user = 0;
int depth = 0;
int in_string = 0;
char key_buffer[256] = {0};
int key_len = 0;
while (fgets(buffer, sizeof(buffer), fp))
{
char *p = buffer;
while (*p)
{
// 处理字符串开始/结束
if (*p == '"')
{
if (!in_string)
{
// 字符串开始
in_string = 1;
key_len = 0; // 开始收集键名或字符串内容
}
else if (!is_quote_escaped(p, buffer))
{
// 字符串结束(未转义的引号)
in_string = 0;
// 在 depth 1 时,检查刚结束的字符串是否是键名
if (depth == 1)
{
key_buffer[key_len] = '\0';
if (strcmp(key_buffer, "system") == 0)
{
in_system = 1;
in_user = 0;
}
else if (strcmp(key_buffer, "user") == 0)
{
in_user = 1;
in_system = 0;
}
}
// 在 depth 2 时,如果在 system/user 数组内,提取路径
else if (depth == 2 && (in_system || in_user))
{
key_buffer[key_len] = '\0';
if (key_len > 0)
{
StringList *target = in_system ? &data->system : &data->user;
add_string_list(target, key_buffer);
}
}
}
else
{
// 转义的引号,作为内容的一部分
if (key_len < (int)sizeof(key_buffer) - 1)
key_buffer[key_len++] = *p;
}
}
else if (in_string)
{
// 在字符串内,收集内容
if (*p == '\\' && *(p + 1))
{
// 处理转义序列
p++;
char ch;
switch (*p)
{
case 'n': ch = '\n'; break;
case 'r': ch = '\r'; break;
case 't': ch = '\t'; break;
case 'b': ch = '\b'; break;
case 'f': ch = '\f'; break;
case '\\': ch = '\\'; break;
case '"': ch = '"'; break;
case '/': ch = '/'; break;
default: ch = *p; break;
}
if (key_len < (int)sizeof(key_buffer) - 1)
key_buffer[key_len++] = ch;
}
else
{
if (key_len < (int)sizeof(key_buffer) - 1)
key_buffer[key_len++] = *p;
}
}
else
{
// 不在字符串内
if (*p == '{' || *p == '[')
depth++;
else if (*p == '}' || *p == ']')
depth--;
}
p++;
}
}
fclose(fp);
log_info("Imported paths from JSON file: sys=%d, user=%d, file=%s",
data->system.count, data->user.count, filepath);
return ERR_OK;
}
// 验证路径格式是否有效
// 有效的 Windows 路径格式:
// - 绝对路径:C:\path\to\something
// - UNC 路径:\\server\share
// - 环境变量:%PATH%
// - 相对路径(带冒号后面跟着反斜杠或正斜杠的)
int is_valid_path_format(const char *path)
{
if (!path || *path == '\0')
return 0;
// 检查是否包含冒号(驱动器路径)
const char *colon = strchr(path, ':');
// 检查是否以 \\ 开头(UNC 路径)
if (path[0] == '\\' && path[1] == '\\')
return 1;
// 检查是否为驱动器路径(如 C:\)
if (colon && colon - path == 1)
{
char drive = path[0];
if ((drive >= 'A' && drive <= 'Z') || (drive >= 'a' && drive <= 'z'))
{
// 检查冒号后面是否是路径分隔符
const char *after_colon = colon + 1;
if (*after_colon == '\\' || *after_colon == '/' || *after_colon == '\0')
return 1;
}
}
// 检查是否包含环境变量(%...%)
if (strchr(path, '%'))
return 1;
// 检查路径是否包含反斜杠或正斜杠(相对路径)
if (strchr(path, '\\') || strchr(path, '/'))
return 1;
return 0;
}
-311
View File
@@ -1,311 +0,0 @@
#include "core/lua_config.h"
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static lua_State *G_L = NULL;
static int G_loaded = 0;
static const char *G_config_path = "lua/config.lua";
static const char *get_string_default(const char *section, const char *key)
{
if (strcmp(section, "app") == 0)
{
if (strcmp(key, "name") == 0)
return "PathEditor";
if (strcmp(key, "name_readonly") == 0)
return "PathEditor (只读模式)";
}
else if (strcmp(section, "dialog") == 0)
{
if (strcmp(key, "size") == 0)
return "800x800";
if (strcmp(key, "minsize") == 0)
return "800x800";
if (strcmp(key, "select_dir") == 0)
return "选择目录";
}
else if (strcmp(section, "list") == 0)
{
if (strcmp(key, "item_padding") == 0)
return "5x5";
if (strcmp(key, "backcolor") == 0)
return "255 255 255";
}
else if (strcmp(section, "button") == 0)
{
if (strcmp(key, "rastersize") == 0)
return "100x32";
if (strcmp(key, "new") == 0)
return "新建(N)";
if (strcmp(key, "edit") == 0)
return "编辑(E)";
if (strcmp(key, "browse") == 0)
return "浏览(B)...";
if (strcmp(key, "del") == 0)
return "删除(D)";
if (strcmp(key, "up") == 0)
return "上移(U)";
if (strcmp(key, "down") == 0)
return "下移(O)";
if (strcmp(key, "clean") == 0)
return "一键清理";
if (strcmp(key, "import") == 0)
return "导入(I)";
if (strcmp(key, "export") == 0)
return "导出(E)";
if (strcmp(key, "ok") == 0)
return "确定";
if (strcmp(key, "cancel") == 0)
return "取消";
if (strcmp(key, "help") == 0)
return "帮助(?)";
if (strcmp(key, "undo") == 0)
return "撤销";
if (strcmp(key, "redo") == 0)
return "重做";
if (strcmp(key, "darkmode") == 0)
return "深色模式";
if (strcmp(key, "lightmode") == 0)
return "浅色模式";
}
else if (strcmp(section, "label") == 0)
{
if (strcmp(key, "title") == 0)
return "环境变量编辑器:";
if (strcmp(key, "search_placeholder") == 0)
return "输入关键词搜索...";
if (strcmp(key, "tab_sys") == 0)
return "系统变量 (System)";
if (strcmp(key, "tab_user") == 0)
return "用户变量 (User)";
if (strcmp(key, "tab_merged") == 0)
return "合并预览";
if (strcmp(key, "export_title") == 0)
return "导出 PATH";
if (strcmp(key, "import_title") == 0)
return "导入 PATH";
}
else if (strcmp(section, "theme") == 0)
{
if (strcmp(key, "light_bg") == 0)
return "240 240 240";
if (strcmp(key, "light_list_bg") == 0)
return "255 255 255";
if (strcmp(key, "light_list_alt") == 0)
return "245 245 245";
if (strcmp(key, "light_fg") == 0)
return "0 0 0";
if (strcmp(key, "dark_bg") == 0)
return "30 30 30";
if (strcmp(key, "dark_list_bg") == 0)
return "40 40 40";
if (strcmp(key, "dark_list_alt") == 0)
return "50 50 50";
if (strcmp(key, "dark_fg") == 0)
return "220 220 220";
}
else if (strcmp(section, "layout") == 0)
{
if (strcmp(key, "vbox_gap") == 0)
return "5";
if (strcmp(key, "vbox_margin") == 0)
return "0x0";
if (strcmp(key, "vbox_all_margin") == 0)
return "10x10";
if (strcmp(key, "vbox_all_gap") == 0)
return "5";
if (strcmp(key, "hbox_gap") == 0)
return "10";
if (strcmp(key, "hbox_margin") == 0)
return "10x10";
if (strcmp(key, "hbox_alignment") == 0)
return "ACENTER";
}
else if (strcmp(section, "status") == 0)
{
if (strcmp(key, "normal") == 0)
return "状态: 就绪";
if (strcmp(key, "readonly") == 0)
return "状态: ⚠️ 只读模式 (无管理员权限)";
if (strcmp(key, "saving") == 0)
return "状态: 保存中...";
if (strcmp(key, "saved") == 0)
return "状态: ✓ 保存成功";
if (strcmp(key, "error") == 0)
return "状态: ✗ 保存失败";
if (strcmp(key, "deleted") == 0)
return "状态: 已删除选中项";
if (strcmp(key, "loaded") == 0)
return "状态: 已加载系统和用户变量";
if (strcmp(key, "drag_folder_only") == 0)
return "提示: 只能拖拽文件夹添加到 PATH";
if (strcmp(key, "admin_warning") == 0)
return "未检测到管理员权限,只能查看和导出 PATH,无法保存更改。";
}
return "";
}
int lua_config_init(void)
{
if (G_L != NULL)
{
return 0;
}
G_L = luaL_newstate();
if (G_L == NULL)
{
return -1;
}
luaL_openlibs(G_L);
if (luaL_dofile(G_L, G_config_path) != LUA_OK)
{
const char *err = lua_tostring(G_L, -1);
if (err)
{
fprintf(stderr, "[Lua Config] 加载配置文件失败: %s\n", err);
}
lua_settop(G_L, 0);
G_loaded = 0;
return 0;
}
lua_settop(G_L, 0);
G_loaded = 1;
return 0;
}
void lua_config_destroy(void)
{
if (G_L != NULL)
{
lua_close(G_L);
G_L = NULL;
}
G_loaded = 0;
}
const char *lua_config_get_string(const char *section, const char *key)
{
if (G_L == NULL || section == NULL || key == NULL)
{
return get_string_default(section, key);
}
lua_getglobal(G_L, "config");
if (!lua_istable(G_L, -1))
{
lua_settop(G_L, 0);
return get_string_default(section, key);
}
lua_getfield(G_L, -1, section);
if (!lua_istable(G_L, -1))
{
lua_settop(G_L, 0);
return get_string_default(section, key);
}
lua_getfield(G_L, -1, key);
if (!lua_isstring(G_L, -1))
{
lua_settop(G_L, 0);
return get_string_default(section, key);
}
const char *value = lua_tostring(G_L, -1);
lua_settop(G_L, 0);
return value ? value : get_string_default(section, key);
}
int lua_config_get_int(const char *section, const char *key, int default_value)
{
if (G_L == NULL || section == NULL || key == NULL)
{
return default_value;
}
lua_getglobal(G_L, "config");
if (!lua_istable(G_L, -1))
{
lua_settop(G_L, 0);
return default_value;
}
lua_getfield(G_L, -1, section);
if (!lua_istable(G_L, -1))
{
lua_settop(G_L, 0);
return default_value;
}
lua_getfield(G_L, -1, key);
if (!lua_isnumber(G_L, -1))
{
lua_settop(G_L, 0);
return default_value;
}
int value = (int)lua_tointeger(G_L, -1);
lua_settop(G_L, 0);
return value;
}
int lua_config_reload(void)
{
if (G_L != NULL)
{
lua_close(G_L);
G_L = NULL;
}
return lua_config_init();
}
int lua_config_is_loaded(void)
{
return G_loaded;
}
int lua_config_set_string(const char *section, const char *key, const char *value)
{
if (section == NULL || key == NULL || value == NULL)
{
return -1;
}
if (G_L == NULL)
{
return -1;
}
lua_getglobal(G_L, "config");
if (!lua_istable(G_L, -1))
{
lua_settop(G_L, 0);
return -1;
}
lua_getfield(G_L, -1, section);
if (!lua_istable(G_L, -1))
{
lua_pop(G_L, 1);
lua_createtable(G_L, 0, 4);
lua_setfield(G_L, -2, section);
lua_getfield(G_L, -1, section);
}
lua_pushstring(G_L, value);
lua_setfield(G_L, -2, key);
lua_settop(G_L, 0);
return 0;
}
+92
View File
@@ -0,0 +1,92 @@
/**
* 路径管理器 — 对应 C 版 path_manager.c
* 提供路径增删移清理等 CRUD 操作的纯逻辑
*/
import { StringList } from './string-list';
/** 删除指定索引的路径 */
export function pathRemoveAt(list: StringList, index: number): void {
list.removeAt(index);
}
/** 上移路径(调整优先级) */
export function pathMoveUp(list: StringList, index: number): boolean {
if (index <= 0 || index >= list.length) return false;
list.swap(index, index - 1);
return true;
}
/** 下移路径(调整优先级) */
export function pathMoveDown(list: StringList, index: number): boolean {
if (index < 0 || index >= list.length - 1) return false;
list.swap(index, index + 1);
return true;
}
/** 标记路径的有效性(调用方负责提供验证函数和展开 env vars) */
export interface PathValidation {
isValid: boolean;
isDuplicate: boolean;
isEnvVar: boolean;
}
/**
* 分析路径列表中各条目的状态
* validateFn: 验证路径是否有效(需调用 Rust validate_path
*/
export function analyzePaths(
list: StringList,
validateFn: (path: string) => boolean,
): PathValidation[] {
const result: PathValidation[] = [];
const seen = new Set<string>();
for (let i = 0; i < list.length; i++) {
const path = list.get(i)!;
const lower = path.toLowerCase();
const isDuplicate = seen.has(lower);
seen.add(lower);
result.push({
isValid: validateFn(path),
isDuplicate,
isEnvVar: path.includes('%'),
});
}
return result;
}
/**
* 批量删除选中的索引(从大到小排序以避免偏移)
*/
export function batchRemoveAt(list: StringList, indices: number[]): void {
const sorted = [...indices].sort((a, b) => b - a);
for (const idx of sorted) {
list.removeAt(idx);
}
}
/**
* 一键清理 — 移除无效路径和重复路径
* 从后往前操作以避免索引偏移
* 返回被移除的路径数量
*/
export function pathClean(
list: StringList,
validateFn: (path: string) => boolean,
): string[] {
const analysis = analyzePaths(list, validateFn);
const removed: string[] = [];
for (let i = analysis.length - 1; i >= 0; i--) {
const a = analysis[i];
// 移除无效或重复的路径
if (!a.isValid || a.isDuplicate) {
removed.unshift(list.get(i)!);
list.removeAt(i);
}
}
return removed;
}
-119
View File
@@ -1,119 +0,0 @@
#include "core/path_manager.h"
#include "utils/os_env.h"
#include "utils/error_code.h"
#include "utils/logger.h"
#include <stdlib.h>
#include <string.h>
// 删除指定索引的路径项
ErrorCode path_manager_remove_at(StringList *list, int index)
{
if (!list)
return ERR_NULL_PTR;
if (index < 0 || index >= list->count)
return ERR_INVALID_INDEX;
free(list->items[index]);
for (int i = index; i < list->count - 1; i++)
{
list->items[i] = list->items[i + 1];
}
list->items[list->count - 1] = NULL;
list->count--;
return ERR_OK;
}
// 向上移动路径项
ErrorCode path_manager_move_up(StringList *list, int index)
{
if (!list)
return ERR_NULL_PTR;
if (index <= 0 || index >= list->count)
return ERR_INVALID_INDEX;
char *temp = list->items[index];
list->items[index] = list->items[index - 1];
list->items[index - 1] = temp;
return ERR_OK;
}
// 向下移动路径项
ErrorCode path_manager_move_down(StringList *list, int index)
{
if (!list)
return ERR_NULL_PTR;
if (index < 0 || index >= list->count - 1)
return ERR_INVALID_INDEX;
char *temp = list->items[index];
list->items[index] = list->items[index + 1];
list->items[index + 1] = temp;
return ERR_OK;
}
// 清理无效路径项
// 算法:先标记需要删除的项,然后从后向前批量删除,减少内存移动
ErrorCode path_manager_clean(StringList *list)
{
if (!list) return ERR_NULL_PTR;
if (list->count == 0) return ERR_OK;
// 分配标记数组
char *marks = (char *)calloc(list->count, sizeof(char));
if (!marks) return ERR_OUT_OF_MEMORY;
int removed_count = 0;
// 第一遍:标记无效路径和重复路径
for (int i = list->count - 1; i >= 0; i--)
{
char *item = list->items[i];
if (!item)
{
marks[i] = 1;
removed_count++;
continue;
}
// 检查路径有效性
if (!is_path_valid(item))
{
marks[i] = 1;
removed_count++;
continue;
}
// 检查是否与前面的项重复(只检查未被标记的项)
for (int j = 0; j < i; j++)
{
if (!marks[j] && list->items[j] && _stricmp(item, list->items[j]) == 0)
{
marks[i] = 1;
removed_count++;
break;
}
}
}
// 第二遍:从后向前删除标记的项,避免多次内存移动
for (int i = list->count - 1; i >= 0; i--)
{
if (marks[i])
{
free(list->items[i]);
// 移动后续元素
for (int j = i; j < list->count - 1; j++)
{
list->items[j] = list->items[j + 1];
}
list->items[list->count - 1] = NULL;
list->count--;
}
}
free(marks);
log_info("Cleaned paths: removed %d invalid/duplicate paths, remaining %d",
removed_count, list->count);
return ERR_OK;
}
-148
View File
@@ -1,148 +0,0 @@
#include "core/registry_service.h"
#include "utils/string_ext.h"
#include "utils/error_code.h"
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#define REG_PATH_SYS L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"
#define REG_PATH_USER L"Environment"
#define REG_VALUE L"Path"
// 内部辅助函数:加载单个列表
static ErrorCode load_single_path(HKEY hKeyRoot, const wchar_t *regPath, StringList *list)
{
clear_string_list(list);
HKEY hKey;
LONG res = RegOpenKeyExW(hKeyRoot, regPath, 0, KEY_READ, &hKey);
if (res != ERROR_SUCCESS)
{
return ERR_REGISTRY_FAILED;
}
DWORD type, size;
res = RegQueryValueExW(hKey, REG_VALUE, NULL, &type, NULL, &size);
if (res == ERROR_SUCCESS)
{
wchar_t *buffer = (wchar_t *)malloc(size + 2);
if (!buffer)
{
RegCloseKey(hKey);
return ERR_OUT_OF_MEMORY;
}
memset(buffer, 0, size + 2);
if (RegQueryValueExW(hKey, REG_VALUE, NULL, &type, (LPBYTE)buffer, &size) == ERROR_SUCCESS)
{
wchar_t *current = buffer;
wchar_t *next_semicolon = NULL;
while (*current)
{
next_semicolon = wcschr(current, L';');
if (next_semicolon)
*next_semicolon = L'\0';
if (wcslen(current) > 0)
{
char *utf8_str = wide_to_utf8(current);
if (utf8_str)
{
add_string_list(list, utf8_str);
free(utf8_str);
}
}
if (next_semicolon)
current = next_semicolon + 1;
else
break;
}
}
free(buffer);
}
RegCloseKey(hKey);
return ERR_OK;
}
// 加载系统环境变量路径
ErrorCode load_system_paths(StringList *list)
{
return load_single_path(HKEY_LOCAL_MACHINE, REG_PATH_SYS, list);
}
// 加载用户环境变量路径
ErrorCode load_user_paths(StringList *list)
{
return load_single_path(HKEY_CURRENT_USER, REG_PATH_USER, list);
}
// 内部辅助函数:保存单个列表
static ErrorCode save_single_path(HKEY hKeyRoot, const wchar_t *regPath, const StringList *list)
{
if (!list)
return ERR_NULL_PTR;
// 计算大小
size_t total_len = 0;
for (int i = 0; i < list->count; i++)
{
if (list->items[i])
{
wchar_t *witem = utf8_to_wide(list->items[i]);
if (witem)
{
total_len += wcslen(witem) + 1;
free(witem);
}
}
}
total_len += 1;
wchar_t *buffer = (wchar_t *)malloc(total_len * sizeof(wchar_t));
if (!buffer)
return ERR_OUT_OF_MEMORY;
buffer[0] = L'\0';
for (int i = 0; i < list->count; i++)
{
if (list->items[i])
{
wchar_t *witem = utf8_to_wide(list->items[i]);
if (witem)
{
wcscat(buffer, witem);
if (i < list->count - 1)
wcscat(buffer, L";");
free(witem);
}
}
}
HKEY hKey;
ErrorCode result = ERR_PERMISSION_DENIED;
if (RegOpenKeyExW(hKeyRoot, regPath, 0, KEY_WRITE, &hKey) == ERROR_SUCCESS)
{
DWORD size = (DWORD)((wcslen(buffer) + 1) * sizeof(wchar_t));
if (RegSetValueExW(hKey, REG_VALUE, 0, REG_EXPAND_SZ, (LPBYTE)buffer, size) == ERROR_SUCCESS)
{
result = ERR_OK;
}
RegCloseKey(hKey);
}
free(buffer);
return result;
}
// 保存系统环境变量路径
ErrorCode save_system_paths(const StringList *list)
{
return save_single_path(HKEY_LOCAL_MACHINE, REG_PATH_SYS, list);
}
// 保存用户环境变量路径
ErrorCode save_user_paths(const StringList *list)
{
return save_single_path(HKEY_CURRENT_USER, REG_PATH_USER, list);
}
+84
View File
@@ -0,0 +1,84 @@
/**
* StringList — 纯 TypeScript 的字符串列表数据结构
* 对应 C 版 include/utils/string_ext.h 的 StringList
*/
export class StringList {
private items: string[] = [];
/** 追加字符串 */
add(str: string): void {
this.items.push(str);
}
/** 在指定索引处插入 */
insertAt(index: number, str: string): void {
this.items.splice(index, 0, str);
}
/** 删除指定索引处的元素 */
removeAt(index: number): void {
this.items.splice(index, 1);
}
/** 读取索引处元素 */
get(index: number): string | undefined {
return this.items[index];
}
/** 设置索引处元素 */
set(index: number, str: string): void {
this.items[index] = str;
}
/** 不区分大小写查找是否包含 */
contains(str: string): boolean {
return this.items.some((item) => item.toLowerCase() === str.toLowerCase());
}
/** 查找不区分大小写的索引,未找到返回 -1 */
indexOfIgnoreCase(str: string): number {
const lower = str.toLowerCase();
return this.items.findIndex((item) => item.toLowerCase() === lower);
}
/** 交换两个索引的元素 */
swap(i: number, j: number): void {
const tmp = this.items[i];
this.items[i] = this.items[j];
this.items[j] = tmp;
}
/** 清空所有元素 */
clear(): void {
this.items = [];
}
/** 深拷贝 */
clone(): StringList {
const list = new StringList();
list.items = [...this.items];
return list;
}
/** 转换为普通数组(传给 Rust 后端) */
toArray(): string[] {
return [...this.items];
}
/** 从数组初始化 */
static fromArray(arr: string[]): StringList {
const list = new StringList();
list.items = [...arr];
return list;
}
/** 元素数量 */
get length(): number {
return this.items.length;
}
/** 只读数组 */
get all(): readonly string[] {
return this.items;
}
}
+180
View File
@@ -0,0 +1,180 @@
/**
* 撤销/重做管理器 — 对应 C 版 undo_redo.c
* 支持 8 种操作类型的完整撤销/重做
*/
import { StringList } from './string-list';
export const OperationType = {
ADD: 0, // 新增路径
DELETE: 1, // 删除路径
EDIT: 2, // 编辑路径
MOVE_UP: 3, // 上移
MOVE_DOWN: 4, // 下移
CLEAN: 5, // 一键清理
CLEAR: 6, // 清空
IMPORT: 7, // 导入
} as const;
export type OperationType = (typeof OperationType)[keyof typeof OperationType];
export const TargetType = {
SYSTEM: 0,
USER: 1,
} as const;
export type TargetType = (typeof TargetType)[keyof typeof TargetType];
export interface OpRecord {
type: OperationType;
target: TargetType;
index: number;
count: number;
oldPaths: string[];
newPaths: string[];
}
const DEFAULT_MAX_SIZE = 50;
export class UndoRedoManager {
private records: OpRecord[] = [];
private current: number = -1;
private readonly maxSize: number;
constructor(maxSize: number = DEFAULT_MAX_SIZE) {
this.maxSize = maxSize;
}
/** 推送新操作记录,推送后截断重做分支 */
push(record: OpRecord): void {
// 截断重做分支
this.records = this.records.slice(0, this.current + 1);
// 如果已满,移除最旧的记录
if (this.records.length >= this.maxSize) {
this.records.shift();
}
this.records.push(record);
this.current = this.records.length - 1;
}
/** 撤销当前操作 */
undo(sysPaths: StringList, userPaths: StringList): boolean {
if (this.current < 0) return false;
const rec = this.records[this.current];
this.current--;
const target = rec.target === TargetType.SYSTEM ? sysPaths : userPaths;
switch (rec.type) {
case OperationType.ADD:
// 撤销添加 — 删除最后 count 个元素
for (let i = 0; i < rec.count; i++) {
target.removeAt(target.length - 1);
}
break;
case OperationType.DELETE:
// 撤销删除 — 逐个恢复
for (let i = 0; i < rec.count; i++) {
target.insertAt(rec.index + i, rec.oldPaths[i]);
}
break;
case OperationType.EDIT:
target.set(rec.index, rec.oldPaths[0]);
break;
case OperationType.MOVE_UP:
target.swap(rec.index - 1, rec.index);
break;
case OperationType.MOVE_DOWN:
target.swap(rec.index, rec.index + 1);
break;
case OperationType.CLEAN:
case OperationType.IMPORT:
// 恢复到操作前的完整列表
target.clear();
for (const path of rec.oldPaths) {
target.add(path);
}
break;
case OperationType.CLEAR:
for (const path of rec.oldPaths) {
target.add(path);
}
break;
}
return true;
}
/** 重做下一个操作 */
redo(sysPaths: StringList, userPaths: StringList): boolean {
if (this.current >= this.records.length - 1) return false;
this.current++;
const rec = this.records[this.current];
const target = rec.target === TargetType.SYSTEM ? sysPaths : userPaths;
switch (rec.type) {
case OperationType.ADD:
for (let i = 0; i < rec.count; i++) {
target.add(rec.newPaths[i]);
}
break;
case OperationType.DELETE:
// 从后往前删,避免索引偏移
for (let i = rec.count - 1; i >= 0; i--) {
target.removeAt(rec.index + i);
}
break;
case OperationType.EDIT:
target.set(rec.index, rec.newPaths[0]);
break;
case OperationType.MOVE_UP:
target.swap(rec.index - 1, rec.index);
break;
case OperationType.MOVE_DOWN:
target.swap(rec.index, rec.index + 1);
break;
case OperationType.CLEAN:
case OperationType.IMPORT:
target.clear();
for (const path of rec.newPaths) {
target.add(path);
}
break;
case OperationType.CLEAR:
target.clear();
break;
}
return true;
}
canUndo(): boolean {
return this.current >= 0;
}
canRedo(): boolean {
return this.current < this.records.length - 1;
}
clear(): void {
this.records = [];
this.current = -1;
}
get historyLength(): number {
return this.records.length;
}
}
-296
View File
@@ -1,296 +0,0 @@
#include "core/undo_redo.h"
#include "core/path_manager.h"
#include <stdlib.h>
#include <string.h>
#include "utils/safe_string.h"
#include "utils/logger.h"
#define DEFAULT_MAX_UNDO_RECORDS 50
static char *copy_string(const char *str)
{
if (!str)
return NULL;
return _strdup(str);
}
static void free_string_array(char **arr, int count)
{
if (!arr)
return;
for (int i = 0; i < count; i++)
{
if (arr[i])
free(arr[i]);
}
free(arr);
}
static char **copy_string_array(const char **arr, int count)
{
if (!arr || count <= 0)
return NULL;
char **copy = (char **)malloc(count * sizeof(char *));
if (!copy)
return NULL;
for (int i = 0; i < count; i++)
{
copy[i] = copy_string(arr[i]);
}
return copy;
}
static void init_op_record(OpRecord *record)
{
memset(record, 0, sizeof(OpRecord));
}
static void free_op_record(OpRecord *record)
{
if (record->old_paths)
free_string_array(record->old_paths, record->count);
if (record->new_paths)
free_string_array(record->new_paths, record->count);
init_op_record(record);
}
UndoRedoManager *create_undo_redo_manager(int max_size)
{
if (max_size <= 0)
max_size = DEFAULT_MAX_UNDO_RECORDS;
UndoRedoManager *mgr = (UndoRedoManager *)malloc(sizeof(UndoRedoManager));
if (!mgr)
return NULL;
mgr->records = (OpRecord *)malloc(max_size * sizeof(OpRecord));
if (!mgr->records)
{
free(mgr);
return NULL;
}
mgr->max_size = max_size;
mgr->current = -1;
mgr->count = 0;
for (int i = 0; i < max_size; i++)
init_op_record(&mgr->records[i]);
return mgr;
}
void destroy_undo_redo_manager(UndoRedoManager *mgr)
{
if (!mgr)
return;
for (int i = 0; i < mgr->count; i++)
free_op_record(&mgr->records[i]);
free(mgr->records);
free(mgr);
}
int push_undo_record(UndoRedoManager *mgr, const OpRecord *record)
{
if (!mgr || !record)
return -1;
// 如果 current 不是在最新位置(已经撤销过),清除重做历史
while (mgr->count > mgr->current + 1)
{
mgr->count--;
free_op_record(&mgr->records[mgr->count]);
}
// 如果已满,移除最旧的记录
if (mgr->count >= mgr->max_size)
{
// 移除第一条记录
free_op_record(&mgr->records[0]);
for (int i = 0; i < mgr->max_size - 1; i++)
mgr->records[i] = mgr->records[i + 1];
init_op_record(&mgr->records[mgr->max_size - 1]);
mgr->current--;
}
int pos = mgr->count;
mgr->records[pos] = *record;
mgr->records[pos].old_paths = copy_string_array((const char **)record->old_paths, record->count);
mgr->records[pos].new_paths = copy_string_array((const char **)record->new_paths, record->count);
mgr->current = pos;
mgr->count = pos + 1;
return 0;
}
int undo(UndoRedoManager *mgr, StringList *sys_paths, StringList *user_paths)
{
if (!mgr || !can_undo(mgr))
return -1;
OpRecord *rec = &mgr->records[mgr->current];
StringList *target = (rec->target == TARGET_SYSTEM) ? sys_paths : user_paths;
switch (rec->type)
{
case OP_ADD:
// 撤销添加:删除刚添加的路径
if (rec->count > 0 && target->count > 0)
{
// 删除最后添加的那条
free(target->items[target->count - 1]);
target->count--;
}
break;
case OP_DELETE:
// 撤销删除:恢复被删除的路径到原始位置
for (int i = 0; i < rec->count; i++)
{
if (rec->old_paths[i])
string_list_insert_at(target, rec->index + i, rec->old_paths[i]);
}
break;
case OP_EDIT:
// 撤销编辑:恢复到原值
if (rec->old_paths[0])
string_list_set(target, rec->index, rec->old_paths[0]);
break;
case OP_MOVE_UP:
case OP_MOVE_DOWN:
// 撤销移动:反向移动一次
if (rec->type == OP_MOVE_UP)
path_manager_move_down(target, rec->index - 1);
else
path_manager_move_up(target, rec->index + 1);
break;
case OP_CLEAN:
case OP_IMPORT:
// 撤销清理/导入:恢复到原列表
clear_string_list(target);
for (int i = 0; i < rec->count; i++)
{
if (rec->old_paths[i])
add_string_list(target, rec->old_paths[i]);
}
break;
case OP_CLEAR:
// 撤销清空:恢复所有路径
for (int i = 0; i < rec->count; i++)
{
if (rec->old_paths[i])
add_string_list(target, rec->old_paths[i]);
}
break;
default:
break;
}
mgr->current--;
return 0;
}
int redo(UndoRedoManager *mgr, StringList *sys_paths, StringList *user_paths)
{
if (!mgr || !can_redo(mgr))
return -1;
mgr->current++;
OpRecord *rec = &mgr->records[mgr->current];
StringList *target = (rec->target == TARGET_SYSTEM) ? sys_paths : user_paths;
switch (rec->type)
{
case OP_ADD:
// 重做添加:重新添加路径
for (int i = 0; i < rec->count; i++)
{
if (rec->new_paths[i])
add_string_list(target, rec->new_paths[i]);
}
break;
case OP_DELETE:
// 重做删除:从高索引到低索引删除,避免索引偏移
for (int i = rec->count - 1; i >= 0; i--)
{
path_manager_remove_at(target, rec->index + i);
}
break;
case OP_EDIT:
// 重做编辑:应用新值
if (rec->new_paths[0])
string_list_set(target, rec->index, rec->new_paths[0]);
break;
case OP_MOVE_UP:
case OP_MOVE_DOWN:
// 重做移动:再次移动
if (rec->type == OP_MOVE_UP)
path_manager_move_up(target, rec->index);
else
path_manager_move_down(target, rec->index);
break;
case OP_CLEAN:
case OP_IMPORT:
// 重做清理/导入:应用新列表
clear_string_list(target);
if (rec->new_paths)
{
for (int i = 0; i < rec->count; i++)
{
if (rec->new_paths[i])
add_string_list(target, rec->new_paths[i]);
}
}
break;
case OP_CLEAR:
// 重做清空:清空列表
clear_string_list(target);
break;
default:
break;
}
return 0;
}
int can_undo(const UndoRedoManager *mgr)
{
if (!mgr)
return 0;
return mgr->current >= 0;
}
int can_redo(const UndoRedoManager *mgr)
{
if (!mgr)
return 0;
return mgr->current < mgr->count - 1;
}
void clear_undo_redo_history(UndoRedoManager *mgr)
{
if (!mgr)
return;
for (int i = 0; i < mgr->count; i++)
free_op_record(&mgr->records[i]);
mgr->current = -1;
mgr->count = 0;
}
+35
View File
@@ -0,0 +1,35 @@
/**
* 路径格式验证 — 对应 C 版 import_export.c:is_valid_path_format()
*/
/** 检查路径是否符合 Windows 路径格式 */
export function is_valid_path_format(path: string): boolean {
if (!path || path.trim() === '') return false;
// UNC 路径: \\server\share
if (path.startsWith('\\\\') || path.startsWith('//')) return true;
// 驱动器字母: C:\... 或 C:/
if (/^[a-zA-Z]:[/\\]/.test(path)) return true;
// 环境变量: %VAR%
if (path.includes('%')) return true;
// 包含路径分隔符的相对路径
if (path.includes('/') || path.includes('\\')) return true;
return false;
}
/** 连接 PATH 字符串(用分号) */
export function join_path(paths: string[]): string {
return paths.join(';');
}
/** 分割 PATH 字符串 */
export function split_path(raw: string): string[] {
return raw
.split(';')
.map((s) => s.trim())
.filter((s) => s.length > 0);
}