mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 18:15:55 +08:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user