diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 000000000..aa1a60524 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,86 @@ +# .clang-tidy — naming & style checks for clangd / manual clang-tidy. +# +# Status: CONFIG-ONLY. Not run in pre-commit or CI (see AGENT.md "Code Quality +# Discipline"). clangd reads this automatically for in-editor hints; to silence +# naming noise temporarily, comment out `readability-identifier-naming` in Checks. +# Naming rules mirror AGENT.md "Code Style": PascalCase types, camelCase methods, +# snake_case files/variables, UPPER_CASE constants, lower_case namespaces. +--- +Checks: > + -*, + readability-identifier-naming, + bugprone-assert-side-effect, + bugprone-bool-pointer-implicit-conversion, + bugprone-copy-constructor-init, + bugprone-dangling-handle, + bugprone-fold-init-type, + bugprone-forwarding-reference-overload, + bugprone-inaccurate-cast, + bugprone-incorrect-roundings, + bugprone-infinite-loop, + bugprone-integer-division, + bugprone-misplaced-widening-cast, + bugprone-move-forwarding-reference, + bugprone-multiple-statement-macro, + bugprone-string-constructor, + bugprone-suspicious-enum-usage, + bugprone-suspicious-memset-usage, + bugprone-undefined-memory-manipulation, + bugprone-use-after-move, + bugprone-virtual-near-miss, + modernize-deprecated-headers, + modernize-loop-convert, + modernize-make-shared, + modernize-make-unique, + modernize-pass-by-value, + modernize-redundant-void-arg, + modernize-replace-auto-ptr, + modernize-replace-random-shuffle, + modernize-return-braced-init-list, + modernize-shrink-to-fit, + modernize-unary-static-assert, + modernize-use-bool-literals, + modernize-use-default-member-init, + modernize-use-emplace, + modernize-use-equals-default, + modernize-use-equals-delete, + modernize-use-noexcept, + modernize-use-nullptr, + modernize-use-override, + modernize-use-transparent-functors, + modernize-use-using, + readability-braces-around-statements, + readability-container-size-empty, + readability-delete-null-pointer, + readability-else-after-return, + readability-function-size, + readability-non-const-parameter, + readability-qualified-auto, + readability-redundant-control-flow, + readability-redundant-string-cstr, + readability-redundant-string-init, + readability-simplify-boolean-expr, + readability-static-definition-in-anonymous-namespace, + readability-string-compare, + readability-uniqueptr-delete-release, + readability-uppercase-literal-suffix, +WarningsAsErrors: '' +HeaderFilterRegex: '.*' +CheckOptions: + - { key: readability-identifier-naming.ClassCase, value: CamelCase } + - { key: readability-identifier-naming.StructCase, value: CamelCase } + - { key: readability-identifier-naming.UnionCase, value: CamelCase } + - { key: readability-identifier-naming.EnumCase, value: CamelCase } + - { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE } + - { key: readability-identifier-naming.TypedefCase, value: CamelCase } + - { key: readability-identifier-naming.TypeAliasCase, value: CamelCase } + - { key: readability-identifier-naming.FunctionCase, value: camelBack } + - { key: readability-identifier-naming.MethodCase, value: camelBack } + - { key: readability-identifier-naming.ParameterCase, value: camelBack } + - { key: readability-identifier-naming.VariableCase, value: camelBack } + - { key: readability-identifier-naming.PrivateMemberPrefix, value: m_ } + - { key: readability-identifier-naming.PrivateMemberCase, value: camelBack } + - { key: readability-identifier-naming.ConstantCase, value: UPPER_CASE } + - { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE } + - { key: readability-identifier-naming.NamespaceCase, value: lower_case } + - { key: readability-identifier-naming.FileIgnoringCase, value: lower_case } diff --git a/.claude/commands/architecture.md b/.claude/commands/architecture.md new file mode 100644 index 000000000..8eee9f286 --- /dev/null +++ b/.claude/commands/architecture.md @@ -0,0 +1,97 @@ +# /architecture — 架构一致性守护 + +检查代码是否符合 CFDesktop 三层架构规则。 + +## 层级定义 + +### Layer 1: base/ (基础层) +- **路径**: `base/` +- **CMake targets**: `cfbase`, `cfbase_headers`, `cfbase_cpu`, `cfbase_memory`, `cfbase_gpu`, `cfbase_network`, `cfbase_console` +- **允许依赖**: Qt6::Core, OS APIs (POSIX, Win32) +- **职责**: 硬件探测、工具库、平台抽象 +- **禁止**: 不可感知 UI 或桌面环境概念 + +### Layer 2: ui/ (UI 框架层) +- **路径**: `ui/` +- **CMake targets**: `cfui`, `cf_ui_base`, `cf_ui_core`, `cf_ui_widget_material`, `cf_ui_components_material` +- **允许依赖**: Qt6::Core, Qt6::Gui, CFDesktop::base +- **职责**: Material Design 3 组件、主题引擎、动画框架 +- **禁止**: 不可感知桌面环境或窗口管理概念 + +### Layer 3: desktop/ (桌面环境层) +- **路径**: `desktop/` +- **CMake targets**: `CFDesktop_shared`, `CFDesktopMain`, `CFDesktopUi`, `cffilesystem`, `cfconfig`, `cflogger` +- **允许依赖**: Qt6::Core, Qt6::Gui, Qt6::Widgets, cfbase, cflogger, cfui +- **职责**: 桌面环境实现、窗口管理、显示后端、配置管理 + +## 依赖规则 (STRICT) + +``` +base/ → 禁止 include ui/ 或 desktop/ 的任何头文件 +ui/ → 禁止 include desktop/ 的任何头文件 +desktop/ → 可以 include ui/ 和 base/ 的头文件 +``` + +## 检查流程 + +### Step 1: 确定审查范围 + +根据用户指定的模块/文件/目录,确定需要检查的文件列表。 + +### Step 2: 检查 #include 指令 + +对每个源文件和头文件: +1. 提取所有 `#include` 行 +2. 将每个 include 映射到其所属层级 (base/ui/desktop/外部) +3. 标记任何向上依赖(低层级 include 高层级) + +**检测模式**: +- base 层违规: `#include` 匹配 `ui/` 或 `desktop/` 或 `cfui` 或 `CFDesktop` +- ui 层违规: `#include` 匹配 `desktop/` 或 `CFDesktop_shared` + +### Step 3: 检查 CMake 依赖 + +对每个 `CMakeLists.txt`: +1. 检查 `target_link_libraries` 只引用同层或更低层 +2. base: 只允许 Qt 和系统库 +3. ui: Qt + CFDesktop::base +4. desktop: Qt + cfbase + cfui + 自身静态库 + +### Step 4: 检查接口驱动设计 + +- 抽象接口应在 `components/` 或 `include/` 目录 +- 具体实现在 `platform/` 或 `private/` 目录 +- 运行时平台选择使用工厂模式 +- 行为变化使用策略模式 + +### Step 5: 检查平台抽象隔离 + +平台特定代码必须隔离在 `private/_impl/` 目录中: +- `base/system/*/private/linux_impl/` — Linux 实现 +- `base/system/*/private/win_impl/` — Windows 实现 +- `desktop/ui/platform/windows/` — Windows 桌面后端 +- `desktop/ui/platform/linux_wsl/` — Linux/WSL 后端 + +共享代码(ui/core, ui/widget, base/include)中不得出现平台特定代码。 + +## 输出格式 (中文) + +```markdown +# 架构一致性报告: + +## 依赖关系图 +(ASCII 图示实际依赖关系) + +## 违规项 +| 文件 | 行号 | 违规类型 | 说明 | +|------|------|----------|------| + +## 符合项 +(确认观察到的正确模式) + +## 建议修改 +(针对每个违规的具体修复方案) +``` + +如果无违规: +> 架构一致性检查通过,未发现层级依赖违规。 diff --git a/.claude/commands/cross-platform.md b/.claude/commands/cross-platform.md new file mode 100644 index 000000000..d68f0b6bb --- /dev/null +++ b/.claude/commands/cross-platform.md @@ -0,0 +1,116 @@ +# /cross-platform — 跨平台兼容性检查 + +检查代码的跨平台兼容性,确保正确的平台抽象和隔离。 + +## 触发方式 + +- `/cross-platform <模块路径>` — 检查跨平台兼容性 +- Review 流程内部调用 + +## 支持平台 + +| 平台 | API | 编译器 | +|------|-----|--------| +| Windows 10/11 | Win32, DWM | MSVC, MinGW | +| Linux/WSL | POSIX, X11, Wayland | GCC, Clang | +| Embedded ARM | EGLFS, LinuxFB (规划中) | GCC交叉编译 | + +## 检查流程 + +### Step 1: 条件编译检查 + +#### 合法的平台宏 (优先使用) +- `Q_OS_WIN` — Windows 平台 (Qt 宏,**首选**) +- `Q_OS_LINUX` — Linux 平台 (Qt 宏,**首选**) +- `Q_OS_MAC` — macOS 平台 (Qt 宏) +- `_WIN32` / `_MSC_VER` — Windows 特定低层代码(可接受) +- `WIN32` — CMake 级别(可接受) + +#### 禁止的宏 +- `__linux__` — 应使用 `Q_OS_LINUX` +- `__APPLE__` — 应使用 `Q_OS_MAC` +- `#ifdef _WIN64` — 应使用 `Q_OS_WIN` + +#### 路径处理 +- 硬编码路径分隔符 (`"\"` 或 `"/"`) — 应使用 `QDir`/`QFileInfo` +- 硬编码行尾 — 应使用 `QFile` 自动处理 +- 硬编码临时目录 — 应使用 `QStandardPaths` + +### Step 2: 平台抽象目录结构检查 + +验证平台特定代码的隔离模式: + +**正确模式** (base/system/): +``` +base/system/cpu/ +├── cfcpu.h ← 公共头文件(平台无关接口) +├── cfcpu.cpp ← 公共实现(调用 host.h) +└── private/ + ├── cpu_host.h ← 平台选择器 + ├── linux_impl/ ← Linux 实现 + │ └── cpu_linux.cpp + └── win_impl/ ← Windows 实现 + └── cpu_win.cpp +``` + +**正确模式** (desktop/ui/platform/): +``` +desktop/ui/platform/ +├── interface.h ← 平台无关接口 +├── windows/ ← Windows 实现 +└── linux_wsl/ ← Linux/WSL 实现 +``` + +### Step 3: 共享层纯度检查 + +以下位置**禁止**包含平台特定代码: +- `ui/core/` — 主题引擎 +- `ui/widget/` — MD3 控件 +- `ui/components/` — 动画框架 +- `base/include/base/` — 工具头文件 + +平台特定代码**必须**隔离在: +- `base/system/*/private/` +- `desktop/ui/platform/windows/` +- `desktop/ui/platform/linux_wsl/` + +### Step 4: DLL 导出宏检查 + +验证正确的导出宏使用: +- base: `CF_BASE_EXPORT` +- ui: `CF_UI_EXPORT` +- desktop: `CF_DESKTOP_EXPORT` + +每个导出宏应处理 Windows `__declspec` 和 Linux `__attribute__((visibility))`。 + +### Step 5: 输出报告 (中文) + +```markdown +# 跨平台兼容性报告: + +## 条件编译检查 +### 使用的平台宏 +| 文件 | 行号 | 宏 | 合规 | +|------|------|-----|------| + +### 不符合规范的宏 +(需修改的项,含建议替换) + +## 平台抽象层检查 +### 正确隔离的代码 +- ... + +### 泄漏到共享层的平台代码 +- ... + +## 路径处理检查 +- 合规项: ... +- 违规项: ... + +## DLL 导出检查 +- 合规项: ... +- 缺失导出宏: ... + +## 修复建议 +(按优先级排列的具体修改方案) +``` diff --git a/.claude/commands/docs.md b/.claude/commands/docs.md new file mode 100644 index 000000000..ba4acc989 --- /dev/null +++ b/.claude/commands/docs.md @@ -0,0 +1,108 @@ +# /docs — 文档审查与补全 + +审查指定模块的文档准确性,补全缺失文档。 + +## 触发方式 + +- `/docs <模块路径>` — 审查模块文档 +- 用户说"审查/改进 XXX 模块文档" + +## 三大文档源 + +1. **Doxygen 源码注释** — 在 `.h`/`.hpp` 文件中的 `/** */` 或 `///` 注释 +2. **MkDocs 文档** — `document/HandBook/` 和 `document/design_stage/` 目录 +3. **模块 README** — 各模块目录下的 README 文件 + +## 审查流程 + +### Step 1: 确定审查范围 + +- 用户指定模块、文件或目录 +- 列出头文件和源文件清单 +- 识别对应的文档文件(`document/HandBook/` 下) + +### Step 2: Doxygen 合规性检查 + +参考规范:`document/DOXYGEN_REQUEST.md`(权威标准) + +对每个头文件检查: + +#### 文件级检查 +- `@file` 头部存在且路径正确 +- `@brief`, `@author`, `@date`, `@version`, `@since`, `@ingroup` 标签完整 +- 行宽 ≤ 100 字符 + +#### 类型注释检查 +- 每个公共 `class`/`struct`/`enum` 前有 `@brief` +- `@details` 描述生命周期和所有权(如有) +- `@ingroup` 模块归属正确 + +#### 函数注释检查 +- `@brief` 使用第三人称现在时 +- `@param` 包含方向标记 `[in]/[out]/[in,out]` +- `@param` 顺序与函数签名一致 +- 非 void 函数有 `@return`,void 函数**没有** `@return` +- `@throws`, `@note`, `@warning`, `@since`, `@ingroup` 标签存在 +- 模板参数有 `@tparam` + +#### 风格一致性检查 +- 同一文件内统一使用 `/** */` 或 `///` +- 行宽 ≤ 100 字符 +- 无第一人称("we", "I", "our") + +### Step 3: 代码 vs 文档准确性 + +对每个已文档化的 API: +1. `@param` 名称是否匹配函数签名 +2. `@return` 是否匹配实际返回类型 +3. `@throws` 是否匹配实际异常行为 +4. 描述的行为是否匹配实现 +5. 标记所有 `@note FIXME` 条目 + +### Step 4: MkDocs 文档检查 + +1. 验证 `document/` 文件引用的路径是否存在 +2. 架构描述是否匹配当前代码结构 +3. 文档中的代码示例是否可编译且为最新 +4. `document/design_stage/` 的完成度描述是否准确 + +### Step 5: 输出报告 (中文) + +```markdown +# 文档审查报告: + +## Doxygen 合规性 +### 合规文件 +- `path/to/file.h` — 完全合规 + +### 违规文件 +- `path/to/file.h` — 具体违规项: + 1. 缺少文件级 @file 头 + 2. 函数 foo() 缺少 @param 方向标记 + 3. ... + +### 缺失文档的公共 API +- `Class::method()` — 完全无文档 +- `Class::anotherMethod()` — 仅有 @brief,缺少标签 + +## 文档准确性 +### 不一致列表 +| 文件 | API | 文档描述 | 实际行为 | +|------|-----|----------|----------| + +## MkDocs 文档状态 +- 过时内容: ... +- 缺失页面: ... +- 路径失效引用: ... + +## 修复优先级 +1. **[高]** ... +2. **[中]** ... +3. **[低]** ... +``` + +## 集成现有基建 + +- Doxygen 修复遵循 `AGENT.md` 的 5 步流程 +- 验证运行 `python3 scripts/doxygen/lint.py` +- MkDocs 导航结构参考 `mkdocs.yml` diff --git a/.claude/commands/next-step.md b/.claude/commands/next-step.md new file mode 100644 index 000000000..07f8a7356 --- /dev/null +++ b/.claude/commands/next-step.md @@ -0,0 +1,75 @@ +# /next-step — 开发指引 + +根据项目阶段文档和当前进度,推荐下一步开发任务。 + +## 触发方式 + +- `/next-step` — 推荐下一步开发任务 +- 用户说"下一步开发什么" + +## 工作流程 + +### Step 1: 评估当前状态 + +1. 读取 `document/status/current.md`(项目进度唯一事实来源)获取当前进度与下一步路线 +2. 识别哪些 Phase / 模块未完成 + +### Step 2: 映射设计文档 + +对每个未完成阶段,读取对应的设计文档获取具体任务列表: + +| Phase | 设计文档 | +|-------|----------| +| 1 | `document/design_stage/01_phase1_hardware_probe.md` | +| 2 | `document/design_stage/02_phase2_base_library.md` | +| 3 | `document/design_stage/03_phase3_input_layer.md` | +| 6 | `document/design_stage/04_phase6_simulator.md` | +| 8 | `document/design_stage/05_phase8_testing.md` | + +### Step 3: 优先排序 + +按以下顺序推荐: + +1. **阻塞依赖项** — 其他模块依赖此任务才能推进 +2. **接近完成的模块** (90%+) — 收尾效率最高 +3. **当前活跃阶段** — 保持开发连续性 +4. **测试覆盖缺口** — 为已完成的代码补充测试 + +### Step 4: 输出格式 (中文) + +对每个推荐任务: + +```markdown +## 推荐任务 N: <任务名称> + +**优先级**: 高/中/低 +**所属阶段**: Phase X — <阶段名> + +### 目标文件 +- `path/to/file.h` — 修改说明 +- `path/to/file.cpp` — 修改说明 +- `path/to/new_file.h` — 新建说明 + +### 改动要点 +1. ... +2. ... + +### 风险评估 +- **风险等级**: 低/中/高 +- **风险说明**: ... +- **缓解措施**: ... + +### 依赖关系 +- 前置依赖: ... +- 后续可解锁: ... + +### 参考文档 +- `document/design_stage/XX_phaseX.md` — 具体章节 +``` + +## 规则 + +- 推荐前验证目标文件是否存在(避免推荐已完成的任务) +- 不推荐违反层级依赖规则的任务 +- 每次推荐 2-4 个任务,按优先级排序 +- 如果有近完成的模块,优先推荐收尾 diff --git a/.claude/commands/optimize.md b/.claude/commands/optimize.md new file mode 100644 index 000000000..e20b98fed --- /dev/null +++ b/.claude/commands/optimize.md @@ -0,0 +1,93 @@ +# /optimize — 代码优化 + +基于 C++23 和现代 C++ 工程实践,对代码进行逼近零开销的小改动优化。 + +## 触发方式 + +- `/optimize <文件/函数>` — 优化指定代码 +- Review 输出对接 — 用户基于审查报告请求优化 + +## 硬约束 (MUST) + +- 每项优化改动 **1-5 行** +- **禁止**改变公共 API 签名(破坏 ABI) +- **禁止**改变行为(只做优化) +- **禁止**大规模重构 +- **必须**通过 `-Wall -Wextra -Wpedantic` 编译 +- **必须**通过现有测试 +- 每文件每轮最多 **10 项优化** +- 不确定时加 `@note FIXME` 而非猜测 + +## 优化流程 + +### Step 1: 读取目标代码 + +- 读取指定文件完整内容 +- 读取相关头文件和测试文件 +- 理解类层次和使用上下文 + +### Step 2: 优化检查清单 + +#### 2.1 constexpr 提升 +- 编译期可计算的函数 → `constexpr` +- 魔数 → `constexpr` 常量 +- 类型特征和辅助工具 → `constexpr` 优先于模板元编程 +- `if constexpr` 替代运行时分支(类型分发场景) + +#### 2.2 移动语义 +- 按值返回 vs 显式 `std::move`(NRVO 场景不要 move,会阻碍优化) +- Sink 参数用按值传递 + `std::move` 进成员 +- 构造函数中用 `std::move` 初始化成员 +- **不要**对返回局部变量 `std::move`(阻碍 RVO/NRVO) + +#### 2.3 零开销抽象 +- 编译期可确定的多态 → 模板参数替代虚分派 +- 小函数隐式内联(头文件定义) +- CRTP 替代虚分派(静态多态) +- 固定大小容器 → `std::array` 替代 `std::vector` + +#### 2.4 [[nodiscard]] 标注 +- 所有 const getter +- 返回所有权的工厂函数 +- 返回 `expected` 或错误码的函数 +- 忽略返回值会导致 bug 的函数 + +#### 2.5 智能指针优化 +- 独占所有权 → `std::unique_ptr`(零开销) +- 共享所有权 → `std::shared_ptr`(仅在必要时) +- `std::make_unique` / `std::make_shared` 优于裸 `new` +- 观察者模式 → 裸指针/引用,不用智能指针 + +#### 2.6 Qt 特定优化 +- `QString` → 使用 `QStringView` / `qsizetype` +- Signal/Slot → 新式连接 (`&Class::method`) +- 避免 `QString::fromStdString` 不必要转换 +- 像素操作 → `QImage` 优于 `QPixmap` +- `Q_FOREACH` → 范围 for 循环 + +### Step 3: 输出格式 (中文) + +```markdown +# 优化建议: + +## 优化 1: <标题> +- **类别**: constexpr/移动语义/零开销/[[nodiscard]]/智能指针/Qt +- **当前代码**: + ```cpp + // file:line + ``` +- **优化后代码**: + ```cpp + // 修改后 + ``` +- **原理**: ... +- **影响范围**: 低/中 +- **验证方法**: 编译 + 运行测试 + +## 优化 2: ... + +## 总结 +- 优化项数: N +- 预估性能提升: ... +- 风险等级: 整体低/中 +``` diff --git a/.claude/commands/review.md b/.claude/commands/review.md new file mode 100644 index 000000000..6190b34ff --- /dev/null +++ b/.claude/commands/review.md @@ -0,0 +1,123 @@ +# /review — 代码审查 + +对指定模块进行结构化代码审查,输出中文评审报告。 + +## 触发方式 + +- `/review <模块/文件路径>` — 审查指定模块或文件 +- 用户说"审查 XXX 子模块" + +## 审查流程 + +### Step 1: 确认审查范围 + +1. 根据用户指定,搜索相关源文件 (`.h`, `.hpp`, `.cpp`) +2. 列出完整文件清单(含行数) +3. **等待用户确认**后再继续 +4. 如果范围过大(>20 个文件),建议拆分为子模块逐个审查 + +### Step 2: 读取全部代码 + +- 读取范围内所有 `.h`, `.hpp`, `.cpp` 文件 +- 读取对应 `CMakeLists.txt` 了解构建上下文 +- 查找并读取对应测试文件(`test/` 目录下) + +### Step 3: 五维分析 + +#### 3.1 性能关注点 + +- 不必要的拷贝(缺少 `std::move`,大类型按值传递) +- 多余的 `QString`/`QByteArray` 转换 +- 过多的堆分配(优先栈分配、`std::array`、small buffer optimization) +- 虚调度开销(可用静态分派替代的场景) +- Signal/Slot 连接类型(auto vs direct vs queued 的合理性) +- 循环中的重复计算 +- 不必要的 `Q_OBJECT` 宏(非 QObject 子类使用) + +#### 3.2 模块间依赖检查 + +参考 `.claude/commands/architecture.md` 的层级规则: +- base 不得 include ui/desktop +- ui 不得 include desktop +- CMake `target_link_libraries` 依赖方向正确 + +#### 3.3 类级耦合分析 + +| 检查项 | 阈值 | 说明 | +|--------|------|------| +| 继承深度 | > 3 | 标记过深的继承链 | +| friend 使用 | 任何 | 需要合理理由,否则标记 | +| Signal 数量 | > 10 | 过多 Signal 可能是设计异味 | +| 类行数 | > 500 | 考虑拆分为多个类 | +| 公共方法数 | > 30 | 检查是否违反接口隔离原则 | +| 依赖类数量 | > 8 | 高耦合信号 | + +#### 3.4 C++23 现代性 + +- 缺少 `[[nodiscard]]`(const getter、工厂函数、返回 `expected` 的函数) +- 可 `constexpr` 化但未标注的函数和变量 +- 裸指针(可用智能指针或引用替代的场景) +- 缺少 `override`/`final`(重写虚方法时) +- 缺少 `noexcept`(不抛异常的移动构造/赋值) +- 可用 `std::span` 替代指针+长度参数 +- 可用 `using enum` 简化枚举使用 +- 可用结构化绑定简化返回值处理 + +#### 3.5 文档完整性 + +对照 `document/DOXYGEN_REQUEST.md` 规范: +- 未文档化的公共 API +- `@param` 方向标记缺失 +- `@return` 与实际返回类型不匹配 +- 风格不一致(`/** */` 和 `///` 混用) + +### Step 4: 输出报告 (中文) + +```markdown +# 代码审查报告: + +## 审查范围 +| 文件 | 类型 | 行数 | +|------|------|------| +| ... | .h/.cpp | N | + +模块层次位置: base/ui/desktop — Layer N + +## 性能关注点 +1. **[严重/中等/建议]** 文件:行号 — 问题描述 + +## 依赖违规 +(层级违规详情,或 "无违规") + +## 类级耦合分析 +- 继承深度: ... +- friend 使用: ... +- Signal/Slot 合理性: ... +- 类职责评估: ... +- 上帝类检测: ... + +## 现代 C++ 改进建议 +1. **[类别]** 文件:行号 — 具体建议 + +## 文档完整性 +- 缺失文档的公共 API: ... +- Doxygen 标签问题: ... + +## 综合评估 +- 健康度评分: X/10 +- 主要风险: ... +- 优先改进项: + 1. ... + 2. ... + 3. ... + +## 改进指南 +(可对接 /optimize 的具体改进项列表) +``` + +## 规则 + +- 始终读取实际代码,不做假设 +- 引用具体文件路径和行号 +- 区分严重问题、中等问题和建议 +- 报告中的严重问题必须附带修复方案 diff --git a/.claude/commands/status.md b/.claude/commands/status.md new file mode 100644 index 000000000..17fe00c14 --- /dev/null +++ b/.claude/commands/status.md @@ -0,0 +1,72 @@ +# /status — 项目现状快照 + +合成并输出项目当前状态的精炼摘要,让任何人或新 AI 会话 **30 秒看懂**「现在到哪了、下一步干什么」。 + +## 触发方式 + +- `/status` — 输出项目现状快照 +- 用户说「项目现状 / 现在什么进度 / 快速了解项目 / onboarding / 帮我上手」 + +## 工作流程(全部自动合成,禁止臆造) + +### Step 1: 读取进度真相源 + +1. 读取 `document/status/current.md`(项目进度的唯一事实来源) +2. 抽取:当前阶段、三层进度状态、Phase 状态、下一步路线 + +### Step 2: 采集近期变更 + +```bash +git log -10 --oneline # 最近 10 次提交 +git rev-parse --abbrev-ref HEAD # 当前分支 +``` + +### Step 3: 验证架构健康度 + +```bash +grep -r '#include.*\(ui/\|desktop/\)' base/ # 期望 0(base 不可向上依赖) +grep -r '#include.*desktop/' ui/ # 期望 0(ui 不可依赖 desktop) +``` + +- 任一返回非 0 = 三层依赖违规,必须在输出中标注 ⚠️ 并列出违规文件。 + +### Step 4: 合成输出 + +按下方「输出格式」拼装,保持一屏可读。 + +## 输出格式(中文) + +```markdown +# CFDesktop 现状快照 + +**版本 / 分支**: 0.19.0 / +**校准**: + +## 当前阶段 +<取自 current.md「当前阶段」> + +## 进度状态(定性) +### 三层架构 + +### Phase +<取自 current.md> + +## 下一步路线 +<取自 current.md「下一步路线」,列出 MS2-MS5 与首要任务> + +## 最近变更(最近 10 提交) + + +## 架构健康度 +- 三层依赖: ✅ 0 违规 / ❌ N 处违规(列出文件) + +## 新人入口 +<指向 README / CLAUDE.md / current.md 下一步路线> +``` + +## 规则 + +- **数据来源强制**:所有状态必须取自 `document/status/current.md` 或实时 `git` / `grep`,**禁止凭记忆臆造**,**禁止编造百分比**(current.md 本身用定性状态)。 +- **断链报警**:若 `current.md` 导航表指向的文件不存在,必须在输出中以 ⚠️ 显式报警(这正是本命令要守护的「进度漂移 / 指针断裂」问题)。 +- **精炼优先**:一屏可读,不展开 `CLAUDE.md` 的构建命令细节,指向即可。 +- **只读操作**:本命令只读取与合成,不修改任何文件。 diff --git a/.claude/commands/testing.md b/.claude/commands/testing.md new file mode 100644 index 000000000..e7e779dc6 --- /dev/null +++ b/.claude/commands/testing.md @@ -0,0 +1,129 @@ +# /testing — 测试覆盖建议 + +为新增或修改的代码建议测试用例,参考现有测试模式。 + +## 触发方式 + +- `/testing <模块路径>` — 建议测试用例 +- `/next-step` 或 `/optimize` 衔接 + +## 工作流程 + +### Step 1: 分析目标代码 + +1. 读取源文件,识别公共 API 接口: + - 公共方法(public methods) + - 信号和槽(signals/slots) + - 枚举和常量 + - 模板函数/类 + +2. 识别边界情况: + - 空输入、零值、最大值 + - 无效输入、越界访问 + - 资源限制(内存、文件句柄) + +3. 识别线程安全问题: + - 锁自由数据结构 + - 异步操作 + - 共享状态访问 + +### Step 2: 检查现有测试覆盖 + +1. 在 `test///` 查找已有测试文件 +2. 读取已有测试,理解当前覆盖范围 +3. 识别未覆盖的代码路径 + +### Step 3: 参考现有测试模式 + +**文件模式** (`test///_test.cpp`): +```cpp +/** + * @file _test.cpp + * @brief Test coverage for . + */ +#include +#include ".h" + +TEST(Suite, BasicFunctionality) { + // Arrange + // Act + // Assert +} +``` + +**CMake 模式** (`test//CMakeLists.txt`): +```cmake +add_gtest_executable(_test + /_test.cpp + LINK_LIBRARIES GTest::gtest GTest::gtest_main + LABELS "module;unit;component" +) +``` + +### Step 4: 测试建议分级 + +#### P0 — 必须有 (Critical Path) +- 构造函数/析构函数基本功能 +- 主要用例(happy path) +- 错误处理(无效输入) + +#### P1 — 应该有 (Boundary) +- 空输入、零值、最大值 +- Off-by-one 边界 +- 资源限制 + +#### P2 — 可选有 (Integration/Regression) +- Qt 框架交互(Signal/Slot) +- 跨组件交互 +- 线程安全测试 +- 性能回归测试 + +### Step 5: 输出格式 (中文) + +```markdown +# 测试建议: + +## 现有测试状态 +- 已有测试文件: (列出或 "无") +- 覆盖率评估: X% +- 未覆盖的关键路径: ... + +## 建议新增测试 + +### P0 — 必须 +```cpp +// TEST(Suite, ConstructorInitializesCorrectly) +TEST(Suite, ConstructorInitializesCorrectly) { + // Arrange & Act + obj{/* params */}; + // Assert + EXPECT_EQ(obj.(), expected); +} +``` + +### P1 — 应该 +(测试用例骨架) + +### P2 — 可选 +(测试用例骨架) + +## 测试文件位置 +- 建议路径: `test///_test.cpp` +- CMakeLists.txt 修改: +```cmake +add_gtest_executable(_test ...) +``` + +## 预估工作量 +- P0 测试数量: N +- P1 测试数量: N +- P2 测试数量: N +- 预估耗时: X minutes +``` + +## 模块特定注意事项 + +- **base/ 工具库**: header-only,链接 `cfbase_headers` + `GTest` +- **ui/ 控件**: 需要 `QApplication`,链接 `cfui` + `Qt6::Widgets` + `Qt6::Gui` +- **desktop/**: 需要完整 `CFDesktop_shared` + `Qt6::Widgets` +- **Signal/Slot 测试**: 需要 `Qt6::Test`,使用 `QSignalSpy` diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..baef58202 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(bash scripts/build_helpers/linux_configure.sh)", + "Bash(bash scripts/build_helpers/linux_fast_develop_build.sh)", + "Bash(bash scripts/build_helpers/linux_develop_build.sh)", + "Bash(bash scripts/build_helpers/linux_run_tests.sh)", + "Bash(python3 scripts/doxygen/lint.py)" + ] + } +} diff --git a/.gitignore b/.gitignore index e8c058458..a45cf02ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,15 @@ # ignores .cache .ccache/ -.claude -CLAUDE.md + +# AI agent — personal/private config (NOT shared). Shared AI assets +# (AGENT.md / CLAUDE.md / AGENTS.md and .claude/commands/) ARE tracked. +.claude/settings.local.json MEMORY.md +BLUEPRINT.md # aqtinstall log aqtinstall.log -# privates -BLUEPRINT.md # native builds out/ diff --git a/AGENT.md b/AGENT.md index 036484912..19c10d73a 100644 --- a/AGENT.md +++ b/AGENT.md @@ -1,51 +1,208 @@ -# AGENT.md — Project Conventions for AI Agents +# AGENT.md — CFDesktop Project Conventions for AI Agents -## Build System - -- **Language**: C++23 / CMake (minimum 3.16), Qt 6 -- **Build directory**: `out/build_develop/` -- **Configure**: `bash scripts/build_helpers/linux_configure.sh` -- **Build (no re-configure)**: `bash scripts/build_helpers/linux_fast_develop_build.sh` -- **Build (full clean + build + tests)**: `bash scripts/build_helpers/linux_develop_build.sh` - -## Doxygen Fix Workflow - -### 1. Read the spec - -Read `document/DOXYGEN_REQUEST.md` in full — it is the authoritative Doxygen style guide. - -### 2. Read the violations +> **Single source of truth** for project conventions, shared across all AI agents +> (Claude Code, OpenAI Codex CLI). Tool-specific entry points — `CLAUDE.md`, `AGENTS.md` — +> reference this file and add only tool-specific notes. **Edit conventions here; do not +> duplicate them** in the entry points. -Read `FAILED_DOXYGEN.md` for the current list of violations, grouped by file. +## Project Overview -### 3. Read the linter +- **Name**: CFDesktop — Cross-platform Desktop Environment Framework +- **Version**: 0.19.0 +- **Tech**: C++23 / CMake 3.16+ / Qt 6.8.3 / Material Design 3 +- **Targets**: Windows 10/11, Linux/WSL, Embedded ARM +- **Repo**: https://github.com/Charliechen114514/CFDesktop +- **Current focus / progress**: [`document/status/current.md`](document/status/current.md) — single source of truth for status -Skim `scripts/doxygen/lint.py` to understand the exact checks (file header, function blocks, return tags, param directions, language rules, etc.). +## Architecture: Three-Layer Strict Dependency -### 4. Fix by file - -For each flagged file, read the source, then: - -1. **File header** — add `/** @file ... */` at top if missing (see DOXYGEN_REQUEST.md Section 2). -2. **Type comments** — add `/** @brief ... */` before any undocumented public enum/struct/class. -3. **Function comments** — add a Doxygen block before each flagged function. Key rules: - - Every `@param` needs a direction: `[in]`, `[out]`, or `[in,out]`. - - Non-void functions **must** have `@return`. Void functions **must not**. - - Tags to always include: `@brief`, `@throws` (or `None`), `@note` (or `None`), `@warning` (or `None`), `@since` (`N/A`), `@ingroup` (`none`). -4. **Style consistency** — use `/** */` block style or `///` line style consistently within a file. -5. **Language** — third-person present tense only. No "will", "we", "I", "our", "my". - -### 5. Validate - -```bash -python3 scripts/doxygen/lint.py +``` +desktop/ (Layer 3) → ui/ (Layer 2) → base/ (Layer 1) → Qt/OS API ``` -Iterate up to 3 passes if violations remain. +**Rules (STRICT single-direction):** +- `base/` MUST NOT `#include` from `ui/` or `desktop/` +- `ui/` MUST NOT `#include` from `desktop/` +- `desktop/` MAY `#include` from `ui/` and `base/` +- Verify: `grep -r '#include.*\(ui/\|desktop/\)' base/` must return nothing +- Verify: `grep -r '#include.*desktop/' ui/` must return nothing -## Key Constraints +## Build System -- Only edit Doxygen comments — never change code logic. -- All comments in English. -- Comment lines must be ≤ 100 characters. -- When uncertain about behavior, use `@note FIXME: ...` rather than guessing. +| Action | Command | +|--------|---------| +| Configure | `bash scripts/build_helpers/linux_configure.sh` | +| Fast build | `bash scripts/build_helpers/linux_fast_develop_build.sh` | +| Full build | `bash scripts/build_helpers/linux_develop_build.sh` | +| Run tests | `bash scripts/build_helpers/linux_run_tests.sh` | +| Doxygen lint | `python3 scripts/doxygen/lint.py` | +| Build dir | `out/build_develop/` | + +Windows equivalents use `.ps1` scripts in the same directory. + +## Code Style + +- **Formatter**: LLVM-based `.clang-format`, 100 char line width, 4-space indent +- **Classes**: PascalCase (`ThemeManager`) +- **Methods**: camelCase (`setThemeTo`) +- **Files**: snake_case (`theme_manager.h`) +- **Namespace**: `cf::` for base utilities +- **Export macros**: `CF_BASE_EXPORT`, `CF_UI_EXPORT`, `CF_DESKTOP_EXPORT` +- **Shared libs**: `cfbase` (DLL), `cfui` (DLL), `CFDesktop_shared` (DLL) +- **CMake targets**: `cfbase_*`, `cf_ui_*`, `cf_desktop_*` + +## Coding Taste + +Hard rules live in Code Style above; these are *style preferences* distilled from +the codebase — write new code to match. Not lint-enforced; they keep the codebase +coherent and help AI tools generate on-style code. + +- **Error handling**: prefer [`cf::expected`](base/include/base/) for failable + operations in new code; reduce exceptions. Existing exception-based code is + tolerated. +- **Ownership**: default to `std::unique_ptr` + `std::make_unique`; use + `std::shared_ptr` only for genuine shared ownership; raw `new` is discouraged + (acceptable for Qt parent-ownership like `new QWidget(parent)`, not general + allocation). +- **Modern C++ (heavily used — match this)**: `auto` for local deduction, + `constexpr` liberally, `std::string_view` for non-owning string params. + `concepts` / `std::span` are early-stage — adoptable, not yet idiomatic. +- **Interface-driven design**: cross-layer seams are pure-virtual interfaces + (`IWindow`, `IStatusBar`, `IPanel`…) with implementations in `platform/` / + `private/`; always mark overrides with `override`. `final` is not enforced. +- **Qt**: use `Q_OBJECT` / `emit` normally; `Q_DISABLE_COPY` is not used in this + project (prefer explicit `= delete` if copy-protection is needed). + +## Module Map + +### base/ → cfbase +Hardware probes and foundation utilities. +- `system/cpu/` — CPU detection (features, freq, temp, usage) +- `system/memory/` — RAM detection (physical, virtual, process) +- `system/gpu/` — GPU detection and capabilities +- `system/network/` — Network interface and connectivity +- `system/hardware_tier/` — Hardware tier assessment (CPU/GPU/Memory/Display scoring, capability flags, policy-based override) +- `device/console/` — Console device abstraction with policy chains +- `include/base/` — Header-only utilities (scope_guard, singleton, factory, weak_ptr, expected, policy_chain) + +### ui/ → cfui +Material Design 3 UI framework (5-layer pipeline): +- Layer 1: Math & Utility (`CFColor`, `GeometryHelper`, `Easing`, `DevicePixel`) +- Layer 2: Theme Engine (`ThemeManager`, `MaterialFactory`, token system) +- Layer 3: Animation Engine (`CFMaterialAnimationFactory`, animation strategies) +- Layer 4: Material Behavior (`StateMachine`, `RippleHelper`, `MdElevationController`) +- Layer 5: Widget Adapter (19 MD3 widgets: Button, TextField, Slider, etc.) + +### desktop/ → CFDesktop_shared +Desktop environment implementation. +- `main/` — DAG-based initialization chain + entry point +- `base/config_manager/` — 4-layer ConfigStore (Temp/App/User/System) with JSON backend +- `base/logger/` — Async multi-sink logging (lock-free MPSC queue) +- `ui/components/` — Core interfaces (`IWindow`, `IDisplayServerBackend`) +- `ui/platform/` — Platform backends (Windows, WSL X11, Wayland planned) + +## Phase System + +Per-phase **progress status** (done / in-progress / not-started) lives in +[`document/status/current.md`](document/status/current.md) — the single source of truth. +This table is only a Phase index (no percentages). + +| Phase | Description | +|-------|-------------| +| 0 | Project skeleton | +| 1 | Hardware probe | +| 2 | Base library | +| 3 | Input abstraction | +| 4 | Multi-platform simulator | +| 6 | UI framework + controls | +| 8 | Testing | + +Reference design docs: `document/design_stage/` (Phase → design-doc mapping in `/next-step`). + +## Doxygen Conventions + +- **Spec**: `document/DOXYGEN_REQUEST.md` — authoritative style guide +- **Linter**: `scripts/doxygen/lint.py` — automated validation +- **Rules**: Third-person present tense, `@param` directions `[in]/[out]/[in,out]`, consistent `/** */` or `///` per file +- **Tags required**: `@brief`, `@param`, `@return`, `@throws`, `@note`, `@warning`, `@since`, `@ingroup` + +### Doxygen Fix Workflow + +1. **Read the spec** — `document/DOXYGEN_REQUEST.md` in full. +2. **Read the violations** — `FAILED_DOXYGEN.md` for the current list, grouped by file. +3. **Read the linter** — skim `scripts/doxygen/lint.py` to understand the exact checks (file header, function blocks, return tags, param directions, language rules). +4. **Fix by file** — for each flagged file: + 1. **File header** — add `/** @file ... */` at top if missing. + 2. **Type comments** — add `/** @brief ... */` before undocumented public enum/struct/class. + 3. **Function comments** — add a Doxygen block before each flagged function: + - Every `@param` needs a direction: `[in]`, `[out]`, or `[in,out]`. + - Non-void functions **must** have `@return`. Void functions **must not**. + - Always include: `@brief`, `@throws` (or `None`), `@note` (or `None`), `@warning` (or `None`), `@since` (`N/A`), `@ingroup` (`none`). + 4. **Style consistency** — use `/** */` or `///` consistently within a file. + 5. **Language** — third-person present tense only. No "will", "we", "I", "our", "my". +5. **Validate** — `python3 scripts/doxygen/lint.py`. Iterate up to 3 passes. + +**Constraints**: Only edit Doxygen comments — never change code logic. All comments in English. Comment lines ≤ 100 chars. When uncertain about behavior, use `@note FIXME: ...` rather than guessing. + +## Testing Conventions + +- **Framework**: GoogleTest v1.14.0 +- **Pattern**: `test///_test.cpp` +- **CMake helper**: `add_gtest_executable()` +- **Labels**: `"module;unit;component"` +- **Qt signal tests**: link `Qt6::Test`, use `QSignalSpy` + +## Code Quality Discipline + +Automated checks enforce the conventions above. They run on commit via the +pre-commit hook and in-editor via clangd. + +| Concern | Authority | Enforced by | +|---|---|---| +| Code format | [`.clang-format`](.clang-format) | pre-commit (clang-format, auto-formats staged files) | +| Doxygen comments | [`document/DOXYGEN_REQUEST.md`](document/DOXYGEN_REQUEST.md) | pre-commit (`scripts/doxygen/lint.py`) | +| Three-layer dependency | Architecture rules above | pre-commit (blocks `base→ui/desktop`, `ui→desktop`) | +| Naming | [`.clang-tidy`](.clang-tidy) | clangd / manual (config-only; **not** in pre-commit/CI) | + +**Doxygen — two tiers** (deliberate): + +- **Enforced floor**: [`scripts/doxygen/lint.py`](scripts/doxygen/lint.py) (runs in pre-commit) — `@file` header, `@brief`, `@param` direction, `@return`, `@throws`, `@since`, `@ingroup`, enum/struct docs, function blocks, ≤100-char lines. The codebase currently passes this. +- **Aspirational target**: [`document/DOXYGEN_REQUEST.md`](document/DOXYGEN_REQUEST.md) adds stricter items (class `@code` example, `@author`/`@date`) that lint does **not** check. New code should aim for these; they won't block commits. + +**Hook setup**: `git config core.hooksPath scripts/release/hooks` is set +**automatically** at configure time by `cmake/install_hooks.cmake` — no manual +install needed after `clone`. Hooks live in [`scripts/release/hooks/`](scripts/release/hooks/) +(version-controlled). Bypass with `git commit --no-verify`. + +**HandBook sync (manual discipline)**: when changing a public API or component, +update the corresponding page in [`document/HandBook/`](document/HandBook/) (the +detail truth source). Not automated — a maintainer responsibility. + +## Slash Commands (Claude Code) + +Reusable workflows live in [`.claude/commands/`](.claude/commands/): + +| Command | Purpose | +|---------|---------| +| `/status` | Project status snapshot (source: `current.md` + git + dependency check) | +| `/next-step` | Recommend next dev task from phase docs | +| `/review` | Code review (performance, coupling, docs) | +| `/optimize` | C++23 zero-overhead optimization | +| `/docs` | Documentation accuracy review | +| `/architecture` | Three-layer dependency guard | +| `/cross-platform` | Platform compatibility check | +| `/testing` | Test coverage suggestions | + +(Codex CLI users see `AGENTS.md` for the equivalent checklist.) + +## Documentation Map + +| Want to know | See | +|---|---| +| Current progress / next steps (single source) | [`document/status/current.md`](document/status/current.md) | +| Project intro (for humans) | [`README.md`](README.md) | +| Component / API usage details (single source) | [`document/HandBook/`](document/HandBook/) | +| Script tooling docs (single source) | [`document/scripts/`](document/scripts/) | +| Per-phase design details | [`document/design_stage/`](document/design_stage/) | +| Module TODO boards | [`document/todo/`](document/todo/) | +| Completed-phase archive | [`document/todo/done/SUMMARY.md`](document/todo/done/SUMMARY.md) | diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..d2ca7e862 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,28 @@ +# AGENTS.md — Codex CLI Entry Point + +> Full project conventions live in [AGENT.md](AGENT.md) — the single source of truth +> shared with Claude Code. This file adds **only** Codex-specific notes; it does **not** +> duplicate conventions. + +## At a glance + +- **Stack**: C++23 / CMake 3.16+ / Qt 6.8 / Material Design 3 (Windows, Linux/WSL, embedded ARM) +- **Configure**: `bash scripts/build_helpers/linux_configure.sh` +- **Fast build**: `bash scripts/build_helpers/linux_fast_develop_build.sh` +- **Full build + tests**: `bash scripts/build_helpers/linux_develop_build.sh` +- **Run tests**: `bash scripts/build_helpers/linux_run_tests.sh` +- **Current progress / next steps**: [`document/status/current.md`](document/status/current.md) + +## Read first + +Before working in this repo, read [AGENT.md](AGENT.md) for: + +- the **three-layer strict dependency** rules (`base` → `ui` → `desktop`) and how to verify them with `grep`, +- the module map, code style, and phase system, +- the **Doxygen conventions** and the 5-step fix workflow, +- testing conventions. + +## Codex-specific + +- Codex has no `.claude/commands/` equivalent. Use the "Slash Commands" table in [AGENT.md](AGENT.md) as a **checklist** of review / optimize / docs workflows to invoke manually when relevant. +- Respect the same dependency and Doxygen constraints documented in AGENT.md — they apply identically here. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..2d2667cd0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,16 @@ +# CLAUDE.md — Claude Code Entry Point + +> Project conventions live in **AGENT.md** — the single source of truth shared with +> Codex CLI. It is imported below. This file adds **only** Claude-specific notes; do not +> duplicate conventions here. + +@AGENT.md + +## Claude-specific + +- **Slash commands**: see `.claude/commands/` (indexed in AGENT.md). Use `/status` for an instant project snapshot, `/next-step` for the next task, `/architecture` to guard the three-layer rules. +- **Settings**: `.claude/settings.json` holds the shared permission allowlist; personal overrides go in `.claude/settings.local.json` (git-ignored). +- **Workflow output language**: Chinese (technical terms remain in English). +- **Code comments**: English, per the Doxygen spec in AGENT.md. +- **Commit messages**: English. +- **Documentation under `document/`**: Chinese with English technical terms. diff --git a/CMakeLists.txt b/CMakeLists.txt index bae2d248e..88fb969fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,9 @@ project(CFDesktop # ============================================================ include(cmake/check_pre_configure.cmake) +# Configure git hooks (core.hooksPath) so pre-commit/pre-push work after configure +include(cmake/install_hooks.cmake) + # ============================================================ # Generate Meta Information Headers # ============================================================ diff --git a/README.md b/README.md index 2ba6ae1c9..012ececde 100644 --- a/README.md +++ b/README.md @@ -33,56 +33,23 @@ --- -## 项目进度 +## AI Agent 支持 + +本项目内置 AI agent 协作支持(**Claude Code** 与 **Codex CLI**),项目约定集中在 [`AGENT.md`](AGENT.md)(单一来源,`CLAUDE.md` / `AGENTS.md` 为各工具入口)。使用 AI 时据此快速理解架构与规范;**不使用 AI 可安全忽略** `AGENT.md` / `CLAUDE.md` / `AGENTS.md` 与 `.claude/` 目录,它们不影响构建。 -### 已完成 ✅ +--- -| 阶段 | 模块 | 完成度 | 说明 | -|:---|:---|:---:|:---| -| Phase 0 | 工程骨架 | 100% | CMake 构建系统、代码规范、CI/CD、Docker 多架构构建 | -| Phase 1 | 硬件探针 | 90% | CPU/Memory/GPU/网络检测完成,缺HWTier/Policy | -| Phase 2 | Base 库核心 | 85% | ConfigStore(85%)、Logger(90%)、DPI基础转换(ui/base)、ASCII Art、File Operations | -| Phase 5 | 测试体系 | 65% | Google Test/CTest 集成,本地基线 47 个测试通过 | -| Phase 6 | UI 框架核心 | 95% | Material Design 3 分层架构 (Layer 1-4 全部完成),缺布局/手势 | -| Phase 6 | P0 核心控件 | 100% | Button, TextField, TextArea, Label, CheckBox, RadioButton, GroupBox | -| Phase 6 | P1 控件 | 100% | Slider, ProgressBar, Switch, ToggleButton, etc. (12个) | -| Desktop | 桌面模块 | 90% | 配置中心、日志系统、启动初始化、文件操作、显示后端架构 | -| Display Backend | Windows+WSL X11 | 70% | Windows 后端(100%)、WSL X11 后端(100%),Wayland/嵌入式待实现 | +## 项目进度 -### 进行中 🚧 +> 项目进度以 [`document/status/current.md`](document/status/current.md) 为唯一事实来源。以下为对外概览,可能滞后,精确状态请查阅该文件。 -| 阶段 | 模块 | 完成度 | 说明 | -|:---|:---|:---:|:---| -| Phase 2 | 配置日志增强 | 80% | DPI基础转换已有(ui/base),缺自动检测/版本控制/迁移/验证/网络日志 | -| Desktop | 渲染后端实现 | 30% | RenderBackend 接口设计完成,具体实现待开发 | +**当前焦点**:跑通「看到桌面 → 点图标 → 开应用」最小闭环。 -### 待开始 ⬜ +- ✅ **base** 硬件探针层(含 HWTier)完成 +- 🚧 **ui / desktop** 核心就绪,最小闭环控件与后端开发中 +- ⬜ 输入抽象、模拟器、Wayland / 嵌入式后端待开始 -| 阶段 | 模块 | 说明 | -|:---|:---|:---| -| Phase 1 | 硬件探针完善 | HWTier 档位、CapabilityPolicy | -| Phase 6 | 布局系统 | Grid、Stack、ConstraintLayout | -| Phase 6 | 手势识别 | 触摸/手势统一接口 | -| Phase 3 | 输入抽象层 | 触摸/按键/旋钮/手势统一接口 | -| Phase 4 | 多平台模拟器 | 开发调试用模拟器、设备配置、DPI 注入 | -| Display Backend | Wayland/嵌入式 | Wayland 合成器后端、EGLFS/LinuxFB 直驱后端 | -| Phase 6 | P2 控件 | 27个高级控件 (DatePicker, MenuBar, Dialog, etc.) | -| Phase 6 | P3 控件 | 25个专业控件 (SplitView, ChartView, etc.) | -| Phase 5 | 测试完善 | desktop 模块、性能基准、UI 自动化 | -| 文档 | VitePress 重排 | 已迁移到 VitePress,API 自动文档二期处理 | - -### 快速统计 (2026-03-27 更新) - -| 类别 | 完成度 | 说明 | -|:---|:---:|:---| -| UI 控件 (P0+P1) | 100% (19个) | 完整实现 | -| UI 控件 (P2+P3) | 0% (52个) | 待开发 | -| 文档覆盖 | 60% | ~268个文档 | -| 示例覆盖 | 50% | ~80个示例 | -| 测试覆盖 | 65% | CTest 47 项通过,P0/P1 Widget 已有基础测试 | - -📋 **完整待办清单**: [document/todo/](document/todo/) -📊 **当前状态**: [document/status/current.md](document/status/current.md) +下一步路线与各 Phase 细节见 [`document/status/current.md`](document/status/current.md),待办清单见 [`document/todo/`](document/todo/)。 --- diff --git a/base/include/base/lockfree/mpsc_queue.hpp b/base/include/base/lockfree/mpsc_queue.hpp index 354c2a415..48f48562f 100644 --- a/base/include/base/lockfree/mpsc_queue.hpp +++ b/base/include/base/lockfree/mpsc_queue.hpp @@ -179,10 +179,11 @@ template class MpscQueue { * @ingroup base_lockfree */ bool tryPop(T& out) noexcept { - Cell* cell = &buffer_[readPos_ & (Capacity - 1)]; + const size_type rp = readPos_.load(std::memory_order_relaxed); + Cell* cell = &buffer_[rp & (Capacity - 1)]; size_type seq = cell->sequence.load(std::memory_order_acquire); - if (seq != readPos_ + 1) { + if (seq != rp + 1) { return false; // Empty } @@ -190,10 +191,13 @@ template class MpscQueue { cell->ptr()->~T(); // Destroy the object in the cell // Publish slot availability for next round - // Set to readPos + Capacity so producer at pos = readPos + Capacity can use it - cell->sequence.store(readPos_ + Capacity, std::memory_order_release); + // Set to rp + Capacity so producer at pos = rp + Capacity can use it + cell->sequence.store(rp + Capacity, std::memory_order_release); - ++readPos_; + // Advance the consumer cursor with release ordering so that producer threads + // reading the approximate size()/empty() observe a consistent value instead of + // racing with this write. + readPos_.store(rp + 1, std::memory_order_release); return true; } @@ -263,19 +267,20 @@ template class MpscQueue { size_type popped = 0; for (; popped < max_count; ++popped) { - Cell* cell = &buffer_[readPos_ & (Capacity - 1)]; + const size_type rp = readPos_.load(std::memory_order_relaxed); + Cell* cell = &buffer_[rp & (Capacity - 1)]; size_type seq = cell->sequence.load(std::memory_order_acquire); - if (seq != readPos_ + 1) { + if (seq != rp + 1) { break; // No more available } out[popped] = std::move(*cell->ptr()); cell->ptr()->~T(); - cell->sequence.store(readPos_ + Capacity, std::memory_order_release); + cell->sequence.store(rp + Capacity, std::memory_order_release); - ++readPos_; + readPos_.store(rp + 1, std::memory_order_release); } return popped; @@ -294,7 +299,10 @@ template class MpscQueue { * This is an approximate check, not a thread-safe guarantee. * @ingroup base_lockfree */ - bool empty() const noexcept { return readPos_ >= writePos_.load(std::memory_order_acquire); } + bool empty() const noexcept { + return readPos_.load(std::memory_order_acquire) >= + writePos_.load(std::memory_order_acquire); + } /** * @brief Gets the approximate current size. @@ -307,10 +315,11 @@ template class MpscQueue { */ size_type size() const noexcept { size_type writePos = writePos_.load(std::memory_order_acquire); - if (writePos < readPos_) { + size_type rp = readPos_.load(std::memory_order_acquire); + if (writePos < rp) { return 0; } - size_type size = writePos - readPos_; + size_type size = writePos - rp; return size > Capacity ? Capacity : size; } @@ -349,10 +358,11 @@ template class MpscQueue { std::array buffer_; ///< Ring buffer storage std::atomic writePos_{0}; ///< Current write position (multi-producer) - size_type readPos_{0}; ///< Current read position (single-consumer) + std::atomic readPos_{0}; ///< Current read position (single-consumer) // Padding to prevent false sharing between producer and consumer - char padding_[64 - sizeof(std::atomic) - sizeof(size_type) - sizeof(buffer_) % 64]; + char padding_[64 - sizeof(std::atomic) - sizeof(std::atomic) - + sizeof(buffer_) % 64]; }; } // namespace lockfree diff --git a/cmake/install_hooks.cmake b/cmake/install_hooks.cmake new file mode 100644 index 000000000..c814c5b46 --- /dev/null +++ b/cmake/install_hooks.cmake @@ -0,0 +1,20 @@ +# cmake/install_hooks.cmake +# ============================================================================= +# Configure-time: point `git core.hooksPath` at the version-controlled hooks +# directory so pre-commit / pre-push take effect right after `cmake configure`. +# No manual `install_hooks.sh` needed — the hooks live in the repo and git runs +# them directly. Idempotent and non-fatal (skips if not a git repo). +# ============================================================================= +if(EXISTS "${CMAKE_SOURCE_DIR}/.git") + execute_process( + COMMAND git config core.hooksPath scripts/release/hooks + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + RESULT_VARIABLE _cf_hooks_result + OUTPUT_QUIET ERROR_QUIET + ) + if(_cf_hooks_result EQUAL 0) + message(STATUS "Git hooks: core.hooksPath -> scripts/release/hooks (auto-configured)") + else() + message(STATUS "Git hooks: skipped (git config unavailable; non-fatal)") + endif() +endif() diff --git a/desktop/CMakeLists.txt b/desktop/CMakeLists.txt index 0b0f6fcee..a238d3585 100644 --- a/desktop/CMakeLists.txt +++ b/desktop/CMakeLists.txt @@ -75,10 +75,14 @@ target_link_libraries(CFDesktop_shared PUBLIC Qt6::Widgets ) -# Link shared library dependencies (NOT in whole-archive) -target_link_libraries(CFDesktop_shared PRIVATE +# Link shared library dependencies (NOT in whole-archive). +# cfui is PUBLIC so the thin EXE can include the QApplication subclass +# (MaterialApplication) it instantiates in main.cpp. +target_link_libraries(CFDesktop_shared +PRIVATE cfbase cflogger +PUBLIC cfui ) diff --git a/desktop/base/logger/src/async_queue/async_queue.cpp b/desktop/base/logger/src/async_queue/async_queue.cpp index 753a6e083..fcd91e124 100644 --- a/desktop/base/logger/src/async_queue/async_queue.cpp +++ b/desktop/base/logger/src/async_queue/async_queue.cpp @@ -19,9 +19,16 @@ void AsyncPostQueue::start() { void AsyncPostQueue::stop() { if (running_.exchange(false)) { cv_.notify_all(); - flush_completed_.store(flush_token_.load(std::memory_order_acquire), - std::memory_order_release); - flush_completed_cv_.notify_all(); + // Publish the final completed token and wake blocked flush_sync() callers under + // flush_completed_mu_ for the same lost-wakeup reason as the worker loop. The wait + // predicate also returns once running_ is false, but only if the caller is actually + // woken, so the notify must not race past an about-to-sleep caller. + { + std::lock_guard lock(flush_completed_mu_); + flush_completed_.store(flush_token_.load(std::memory_order_acquire), + std::memory_order_release); + flush_completed_cv_.notify_all(); + } if (worker_thread_.joinable()) { worker_thread_.join(); } @@ -136,11 +143,19 @@ void AsyncPostQueue::worker_loop() { sink->flush(); } } - // Update completed token to current flush_token_ - // This unblocks all flush_sync() calls with tokens <= this value + // Update the completed token to the current flush_token_, unblocking every + // flush_sync() caller whose token is <= this value. flush_completed_mu_ MUST be + // held across the store and the notify: flush_sync() blocks on + // flush_completed_cv_ with no timeout, so a notify that races past a caller + // which has already evaluated its predicate (and is about to enter the futex) + // is lost forever and hangs that caller. Holding the lock serialises this + // update against the caller's predicate check, closing the lost-wakeup window. uint64_t current_token = flush_token_.load(std::memory_order_acquire); - flush_completed_.store(current_token, std::memory_order_release); - flush_completed_cv_.notify_all(); + { + std::lock_guard lock(flush_completed_mu_); + flush_completed_.store(current_token, std::memory_order_release); + flush_completed_cv_.notify_all(); + } } std::unique_lock lock(wakeMu_); diff --git a/desktop/main.cpp b/desktop/main.cpp index 3baf5cb9e..a3c2afabf 100644 --- a/desktop/main.cpp +++ b/desktop/main.cpp @@ -1,7 +1,9 @@ #include "desktop_entry.h" -#include +#include "ui/widget/material/application/material_application.h" int main(int argc, char* argv[]) { - QApplication cf_desktop_app(argc, argv); + // MaterialApplication registers the MD3 themes (light/dark) into ThemeManager + // on construction, so panels and widgets resolve real theme tokens at runtime. + cf::ui::widget::material::MaterialApplication cf_desktop_app(argc, argv); return cf::desktop::run_desktop_session(); } diff --git a/desktop/ui/CFDesktopEntity.cpp b/desktop/ui/CFDesktopEntity.cpp index 8fbae72a8..665b4f5c1 100644 --- a/desktop/ui/CFDesktopEntity.cpp +++ b/desktop/ui/CFDesktopEntity.cpp @@ -8,6 +8,7 @@ #include "components/DisplayServerBackendFactory.h" #include "components/IDisplayServerBackend.h" #include "components/PanelManager.h" +#include "components/statusbar/status_bar.h" #include "platform/DesktopPropertyStrategyFactory.h" #include "platform/display_backend_helper.h" #include "platform/shell_layer_helper.h" @@ -100,6 +101,12 @@ CFDesktopEntity::RunsSetupResult CFDesktopEntity::run_init(RunsSetupMethod m) { QObject::connect(panel_mgr, &PanelManager::availableGeometryChanged, desktop_entity_, [shell](const QRect& r) { shell->onAvailableGeometryChanged(r); }); + // ── Status bar: top-edge panel (clock + system icons) ── + auto* status_bar = new cf::desktop::desktop_component::StatusBar(desktop_entity_); + panel_mgr->registerPanel(status_bar->GetWeak()); + status_bar->show(); + panel_mgr->relayout(); + // Show the desktop full-screen desktop_entity_->showFullScreen(); diff --git a/desktop/ui/components/CMakeLists.txt b/desktop/ui/components/CMakeLists.txt index f5cfb8795..0c9f1c081 100644 --- a/desktop/ui/components/CMakeLists.txt +++ b/desktop/ui/components/CMakeLists.txt @@ -31,5 +31,6 @@ PUBLIC PRIVATE cfdesktop_shell_layer_impl cfdesktop_wallpaper + cfdesktop_statusbar Qt6::Core Qt6::Gui Qt6::Widgets ) diff --git a/desktop/ui/components/PanelManager.cpp b/desktop/ui/components/PanelManager.cpp index 774e381c5..ea0a2d7ca 100644 --- a/desktop/ui/components/PanelManager.cpp +++ b/desktop/ui/components/PanelManager.cpp @@ -19,6 +19,7 @@ PanelManager::RegisterFeedback PanelManager::registerPanel(WeakPtr panel return RegisterFeedback::DuplicatePanel; } + panels.push_back(panel); return RegisterFeedback::OK; } diff --git a/desktop/ui/components/statusbar/CMakeLists.txt b/desktop/ui/components/statusbar/CMakeLists.txt index e69de29bb..f7be5a730 100644 --- a/desktop/ui/components/statusbar/CMakeLists.txt +++ b/desktop/ui/components/statusbar/CMakeLists.txt @@ -0,0 +1,21 @@ +# Status bar implementation (QWidget-based top-edge panel). +add_library(cfdesktop_statusbar STATIC + status_bar.cpp + statusbar_icons.qrc +) + +target_include_directories(cfdesktop_statusbar PUBLIC + $ + $ + $ +) + +target_link_libraries( + cfdesktop_statusbar +PUBLIC + cfui # ThemeManager, color/typography tokens, ICFTheme +PRIVATE + Qt6::Widgets + cfbase # WeakPtr / WeakPtrFactory + cflogger # Diagnostic logging for icon-mask loading +) diff --git a/desktop/ui/components/statusbar/IStatusBar.h b/desktop/ui/components/statusbar/IStatusBar.h index 54992a703..482faf83f 100644 --- a/desktop/ui/components/statusbar/IStatusBar.h +++ b/desktop/ui/components/statusbar/IStatusBar.h @@ -2,13 +2,14 @@ * @file IStatusBar.h * @brief Abstract status bar panel interface. * - * IStatusBar extends IPanel to define the status bar contract. - * Concrete implementations provide the actual status bar widget - * and content. + * IStatusBar extends IPanel to define the status bar contract: a panel + * anchored to the top edge that renders the clock and system icons. + * Concrete implementations (e.g. StatusBar) provide the widget and the + * behavior behind these accessors. * * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) * @date 2026-03-29 - * @version 0.1 + * @version 0.2 * @since 0.11 * @ingroup components */ @@ -18,15 +19,105 @@ #include "components/IPanel.h" namespace cf::desktop::desktop_component { +/** + * @brief Visual layout variant for a status bar. + * + * Controls how the clock and system icons are arranged. + * + * @ingroup components + */ +enum class StatusBarStyle { + Centered, ///< Clock centered, minimal iconry. + Split ///< Clock and icons distributed to the side regions. +}; + /** * @brief Abstract status bar panel. * - * Concrete implementations provide a status bar that attaches - * to a screen edge and participates in the panel layout. + * Concrete implementations provide a status bar that attaches to a screen + * edge and participates in the panel layout. The visibility accessors let + * callers toggle the clock and the system-icon region independently. * * @ingroup components */ class IStatusBar : public IPanel { public: + /** + * @brief Shows or hides the clock region. + * + * @param[in] visible true to show the clock, false to hide it. + * + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup components + */ + virtual void setTimeVisible(bool visible) = 0; + + /** + * @brief Reports whether the clock region is shown. + * + * @return true when the clock is visible, false otherwise. + * + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup components + */ + virtual bool isTimeVisible() const = 0; + + /** + * @brief Shows or hides the system-icon region. + * + * @param[in] visible true to show system icons, false to hide them. + * + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup components + */ + virtual void setIconsVisible(bool visible) = 0; + + /** + * @brief Reports whether the system-icon region is shown. + * + * @return true when system icons are visible, false otherwise. + * + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup components + */ + virtual bool isIconsVisible() const = 0; + + /** + * @brief Selects the clock/icon layout variant. + * + * @param[in] style The layout style to apply. + * + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup components + */ + virtual void setStyle(StatusBarStyle style) = 0; + + /** + * @brief Returns the active layout variant. + * + * @return The current status bar style. + * + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup components + */ + virtual StatusBarStyle style() const = 0; }; } // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/statusbar/icons/LICENSE.md b/desktop/ui/components/statusbar/icons/LICENSE.md new file mode 100644 index 000000000..c6af3523f --- /dev/null +++ b/desktop/ui/components/statusbar/icons/LICENSE.md @@ -0,0 +1,15 @@ +# Status Bar Icons — Attribution + +The status bar icons (`signal.png`, `battery.png`, `wifi.png`, `volume.png`) are +sourced from **Icons8** (https://icons8.com). + +- Source set: Icons8 line-style glyphs (50px, monochrome black on transparent). +- License: Icons8 Free License — https://icons8.com/license +- Requirement: Free usage requires attribution / a link back to icons8.com. + +These PNGs are used as monochrome alpha masks: at runtime `StatusBar` recolors +them to the active Material theme's `onSurfaceVariant` token, so they follow +Light/Dark theme switches with a single asset per icon. + +> NOTE: If this project is distributed without an Icons8 attribution elsewhere +> (e.g. README, About dialog), keep this file in place to satisfy the license. diff --git a/desktop/ui/components/statusbar/icons/battery.png b/desktop/ui/components/statusbar/icons/battery.png new file mode 100644 index 000000000..9dae88956 Binary files /dev/null and b/desktop/ui/components/statusbar/icons/battery.png differ diff --git a/desktop/ui/components/statusbar/icons/signal.png b/desktop/ui/components/statusbar/icons/signal.png new file mode 100644 index 000000000..6345c679a Binary files /dev/null and b/desktop/ui/components/statusbar/icons/signal.png differ diff --git a/desktop/ui/components/statusbar/icons/volume.png b/desktop/ui/components/statusbar/icons/volume.png new file mode 100644 index 000000000..8dc68d867 Binary files /dev/null and b/desktop/ui/components/statusbar/icons/volume.png differ diff --git a/desktop/ui/components/statusbar/icons/wifi.png b/desktop/ui/components/statusbar/icons/wifi.png new file mode 100644 index 000000000..ce0b09581 Binary files /dev/null and b/desktop/ui/components/statusbar/icons/wifi.png differ diff --git a/desktop/ui/components/statusbar/status_bar.cpp b/desktop/ui/components/statusbar/status_bar.cpp new file mode 100644 index 000000000..cc07abbd3 --- /dev/null +++ b/desktop/ui/components/statusbar/status_bar.cpp @@ -0,0 +1,342 @@ +/** + * @file status_bar.cpp + * @brief Concrete status bar panel implementation. + * + * Renders the clock and system-icon glyphs with Material Design 3 polish: a + * tonal elevation surface, an in-band soft shadow at the bottom seam, refined + * vector icons, and a boot fade-in. All rendering is QPainter-native so it + * builds for the embedded target without extra Qt modules. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-15 + * @version 0.2 + * @since 0.19 + * @ingroup components + */ + +#include "status_bar.h" + +#include "base/color.h" +#include "base/device_pixel.h" +#include "cflog.h" +#include "core/theme_manager.h" +#include "core/token/material_scheme/cfmaterial_token_literals.h" +#include "core/token/typography/cfmaterial_typography_token_literals.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +// Q_INIT_RESOURCE must run at global scope: the rcc-generated registration +// function lives in the global namespace, but the macro's extern declaration +// is emitted in the surrounding scope, so calling it from inside +// cf::desktop::desktop_component would look up a namespaced symbol that +// doesn't exist and fail to link. +static void registerStatusbarIconsResource() { + Q_INIT_RESOURCE(statusbar_icons); +} + +namespace cf::desktop::desktop_component { + +using cf::desktop::PanelPosition; +using cf::ui::base::CFColor; +using cf::ui::base::device::CanvasUnitHelper; +using namespace cf::ui::core::token::literals; + +namespace { +// Fallback palette used when no theme is available (mirrors MD3 light). +constexpr int kBarHeight = 48; ///< Status bar thickness in device pixels. +constexpr int kSideMarginDp = 16; ///< Horizontal padding for clock/icons (dp). +constexpr int kIconGapDp = 12; ///< Spacing between adjacent icons (dp). +constexpr int kIconSizeDp = 16; ///< Icon cell edge length (dp). +constexpr int kTimeGapDp = 10; ///< Gap between the time and the icon cluster (dp). +constexpr qreal kShadowBandDp = 5.0; ///< In-band elevation shadow height (dp). + +// Icon kinds and their compiled-resource mask paths. Indices align with +// StatusBar::icon_masks_[]. A missing mask leaves a visible gap in the bar +// (no silent fallback) so a broken resource stays obvious. +enum class StatusIcon { Signal = 0, Battery = 1, Wifi = 2, Volume = 3 }; +constexpr int kStatusIconCount = 4; +constexpr const char* const kIconMaskPaths[kStatusIconCount] = { + ":/cfdesktop/statusbar/signal.png", // Signal + ":/cfdesktop/statusbar/battery.png", // Battery + ":/cfdesktop/statusbar/wifi.png", // Wifi + ":/cfdesktop/statusbar/volume.png", // Volume +}; +} // namespace + +StatusBar::StatusBar(QWidget* parent) + : QWidget(parent), timer_(new QTimer(this)), cached_time_(currentTimeText()), + cached_date_(currentDateText()) { + setAttribute(Qt::WA_OpaquePaintEvent); + setAutoFillBackground(false); + setFixedHeight(kBarHeight); + setupUi(); + applyTheme(); + loadIconMasks(); + startFadeIn(); +} + +StatusBar::~StatusBar() = default; + +void StatusBar::setupUi() { + connect(timer_, &QTimer::timeout, this, &StatusBar::onTimeout); + timer_->start(1000); + + // React to theme switches (ThemeManager is the canonical source). + connect(&cf::ui::core::ThemeManager::instance(), &cf::ui::core::ThemeManager::themeChanged, + this, [this](const cf::ui::core::ICFTheme&) { applyTheme(); }); +} + +void StatusBar::applyTheme() { + try { + auto& tm = cf::ui::core::ThemeManager::instance(); + const auto& theme = tm.theme(tm.currentThemeName()); + auto& cs = theme.color_scheme(); + background_color_ = cs.queryColor(SURFACE); + foreground_color_ = cs.queryColor(ON_SURFACE); + icon_color_ = cs.queryColor(ON_SURFACE_VARIANT); + divider_color_ = cs.queryColor(OUTLINE_VARIANT); + clock_font_ = theme.font_type().queryTargetFont(TYPOGRAPHY_TITLE_MEDIUM); + // MD3 elevation tonal lift: lighten the surface tone a few steps for the + // top of the gradient, so the bar reads as raised above the shell. + const CFColor base(background_color_); + surface_top_color_ = + CFColor(base.hue(), base.chroma(), std::clamp(base.tone() + 3.0f, 0.0f, 100.0f)) + .native_color(); + } catch (...) { + // Fallback palette when no theme is registered yet. + background_color_ = QColor(0xF7, 0xF5, 0xF3); + surface_top_color_ = QColor(0xFF, 0xFF, 0xFF); + foreground_color_ = QColor(0x1C, 0x1B, 0x1F); + icon_color_ = QColor(0x49, 0x45, 0x4E); + divider_color_ = QColor(0xCA, 0xC4, 0xD0); + clock_font_ = font(); + clock_font_.setPixelSize(15); + } + update(); +} + +void StatusBar::startFadeIn() { + auto* fade = new QVariantAnimation(this); + fade->setDuration(250); + fade->setStartValue(qreal(0)); + fade->setEndValue(qreal(1)); + fade->setEasingCurve(QEasingCurve::OutCubic); + connect(fade, &QVariantAnimation::valueChanged, this, [this](const QVariant& v) { + fade_opacity_ = v.toReal(); + update(); + }); + fade->start(QAbstractAnimation::DeleteWhenStopped); +} + +void StatusBar::loadIconMasks() { + // Qt resources compiled into a STATIC library can be stripped by the linker + // because nothing references the registration symbol. Forcing registration + // keeps the object, so the ":/cfdesktop/statusbar/*.png" lookups succeed. + registerStatusbarIconsResource(); + + int loaded = 0; + std::string missing; + for (int i = 0; i < kStatusIconCount; ++i) { + icon_masks_[i] = QPixmap(QString::fromLatin1(kIconMaskPaths[i])); + if (icon_masks_[i].isNull()) { + missing += missing.empty() ? "" : ", "; + missing += kIconMaskPaths[i]; + } else { + ++loaded; + } + } + if (missing.empty()) { + cf::log::infoftag("StatusBar", "loaded {}/{} icon masks", loaded, kStatusIconCount); + } else { + // Fail loud: a missing mask now leaves a visible gap in the bar rather + // than a silent substitute, and this warning names exactly what is gone. + cf::log::warningftag("StatusBar", "loaded {}/{} icon masks; missing: {}", loaded, + kStatusIconCount, missing); + } +} + +QPixmap StatusBar::tintedPixmap(const QPixmap& mask, const QColor& color) { + if (mask.isNull()) { + return {}; + } + QPixmap out(mask.size()); + out.setDevicePixelRatio(mask.devicePixelRatio()); + out.fill(Qt::transparent); + QPainter tp(&out); + tp.drawPixmap(0, 0, mask); + // Replace every non-transparent pixel's color with `color`, keeping the + // mask's alpha shape. Works for any single-color source (black or white). + tp.setCompositionMode(QPainter::CompositionMode_SourceIn); + tp.fillRect(out.rect(), color); + tp.end(); + return out; +} + +QString StatusBar::currentTimeText() const { + return QDateTime::currentDateTime().toString(QStringLiteral("HH:mm")); +} + +QString StatusBar::currentDateText() const { + return QDateTime::currentDateTime().toString(QStringLiteral("yyyy/MM/dd")); +} + +void StatusBar::onTimeout() { + const QString nextTime = currentTimeText(); + const QString nextDate = currentDateText(); + if (nextTime != cached_time_ || nextDate != cached_date_) { + cached_time_ = nextTime; + cached_date_ = nextDate; + update(); + } +} + +// -- IPanel ---------------------------------------------------------------- +PanelPosition StatusBar::position() const { + return PanelPosition::Top; +} + +int StatusBar::priority() const { + return 100; +} + +int StatusBar::preferredSize() const { + return kBarHeight; +} + +QWidget* StatusBar::widget() const { + return const_cast(this); +} + +// -- IStatusBar ------------------------------------------------------------ +void StatusBar::setTimeVisible(bool visible) { + if (time_visible_ != visible) { + time_visible_ = visible; + update(); + } +} + +bool StatusBar::isTimeVisible() const { + return time_visible_; +} + +void StatusBar::setIconsVisible(bool visible) { + if (icons_visible_ != visible) { + icons_visible_ = visible; + update(); + } +} + +bool StatusBar::isIconsVisible() const { + return icons_visible_; +} + +void StatusBar::setStyle(StatusBarStyle s) { + if (style_ != s) { + style_ = s; + update(); + } +} + +StatusBarStyle StatusBar::style() const { + return style_; +} + +// -- Painting -------------------------------------------------------------- +void StatusBar::paintEvent(QPaintEvent* /*event*/) { + const CanvasUnitHelper h(devicePixelRatioF()); + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing, true); + p.setRenderHint(QPainter::SmoothPixmapTransform, true); + p.setOpacity(fade_opacity_); + + // Tonal elevation surface: gentle vertical gradient (lifted top -> surface). + QLinearGradient surface(0, 0, 0, height()); + surface.setColorAt(0.0, surface_top_color_); + surface.setColorAt(1.0, background_color_); + p.fillRect(rect(), surface); + + // Clock region: date on the left, time on the right just before the icons + // (Split) or centered (Centered). The icon-cluster left edge anchors the time. + const qreal sideMargin = h.dpToPx(kSideMarginDp); + const qreal iconSize = h.dpToPx(kIconSizeDp); + const qreal iconGap = h.dpToPx(kIconGapDp); + const qreal clusterWidth = icons_visible_ ? (4 * iconSize + 3 * iconGap) : 0; + const qreal iconClusterLeft = width() - sideMargin - clusterWidth; + + if (time_visible_) { + p.setPen(foreground_color_); + p.setFont(clock_font_); + const int sm = static_cast(sideMargin); + + // Date: far left. + p.drawText(QRect(sm, 0, width(), height()), Qt::AlignVCenter | Qt::AlignLeft, cached_date_); + + // Time: right-aligned to the icon cluster (Split) or centered (Centered). + if (style_ == StatusBarStyle::Centered) { + p.drawText(rect(), Qt::AlignCenter, cached_time_); + } else { + const int timeRight = static_cast(iconClusterLeft - h.dpToPx(kTimeGapDp)); + p.drawText(QRect(sm, 0, timeRight - sm, height()), Qt::AlignVCenter | Qt::AlignRight, + cached_time_); + } + } + + // System-icon cluster (right-aligned): signal, battery, wifi, volume. + if (icons_visible_) { + const int y = static_cast((height() - iconSize) / 2.0); + qreal x = width() - sideMargin; + auto cell = [&](qreal rightEdge) { + return QRectF(rightEdge - iconSize, y, iconSize, iconSize); + }; + + // PNG mask tinted to the theme. A missing mask leaves a visible gap — + // no silent fallback, so a broken resource stays obvious. + auto drawStatusIcon = [&](StatusIcon kind, qreal rightEdge) { + const QPixmap& mask = icon_masks_[static_cast(kind)]; + if (mask.isNull()) { + return; + } + p.drawPixmap(cell(rightEdge).toRect(), tintedPixmap(mask, icon_color_)); + }; + + drawStatusIcon(StatusIcon::Volume, x); + x -= (iconSize + iconGap); + drawStatusIcon(StatusIcon::Wifi, x); + x -= (iconSize + iconGap); + drawStatusIcon(StatusIcon::Battery, x); + x -= (iconSize + iconGap); + drawStatusIcon(StatusIcon::Signal, x); + } + + // In-band soft elevation shadow at the bottom seam (PanelManager locks the + // widget height to preferredSize, so the cast shadow is drawn in-band). + const qreal shadowH = h.dpToPx(kShadowBandDp); + QLinearGradient shadow(0, height() - shadowH, 0, height()); + shadow.setColorAt(0.0, QColor(0, 0, 0, 0)); + shadow.setColorAt(1.0, QColor(0, 0, 0, 26)); // ~10% alpha + p.fillRect(QRectF(0, height() - shadowH, width(), shadowH), shadow); + + // Horizontally-faded hairline (less "ruled" than a hard full-width line). + QColor lineMid = divider_color_; + lineMid.setAlphaF(0.45); + QColor lineEdge = divider_color_; + lineEdge.setAlphaF(0.0); + QLinearGradient hairline(0, 0, width(), 0); + hairline.setColorAt(0.0, lineEdge); + hairline.setColorAt(0.08, lineMid); + hairline.setColorAt(0.92, lineMid); + hairline.setColorAt(1.0, lineEdge); + p.setPen(QPen(hairline, h.dpToPx(1))); + p.drawLine(QPointF(0, height() - h.dpToPx(0.5)), QPointF(width(), height() - h.dpToPx(0.5))); +} + +} // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/statusbar/status_bar.h b/desktop/ui/components/statusbar/status_bar.h new file mode 100644 index 000000000..bcab51ce5 --- /dev/null +++ b/desktop/ui/components/statusbar/status_bar.h @@ -0,0 +1,278 @@ +/** + * @file status_bar.h + * @brief Concrete status bar panel. + * + * StatusBar is the top-edge panel implementation behind IStatusBar. It + * renders the clock and a row of system-icon glyphs, follows the active + * Material theme, and updates the clock once per second. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-15 + * @version 0.1 + * @since 0.19 + * @ingroup components + */ + +#pragma once + +#include "IStatusBar.h" +#include "base/weak_ptr/weak_ptr.h" +#include "base/weak_ptr/weak_ptr_factory.h" + +#include +#include +#include +#include + +class QTimer; + +namespace cf::desktop::desktop_component { + +/** + * @brief QWidget-based status bar. + * + * Renders the clock and system-icon glyphs directly in paintEvent so the + * active Material theme fully controls colors and typography. A one-second + * timer keeps the clock current. + * + * @ingroup components + */ +class StatusBar final : public QWidget, public IStatusBar { + Q_OBJECT + public: + /** + * @brief Constructs the status bar. + * + * @param[in] parent Owning widget (typically the desktop surface). + * + * @throws None + * @note Starts the clock timer and applies the current theme. + * @warning None + * @since 0.19 + * @ingroup components + */ + explicit StatusBar(QWidget* parent = nullptr); + + /** + * @brief Destructs the status bar. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + ~StatusBar() override; + + // -- IPanel --------------------------------------------------------------- + /** + * @brief Returns the anchoring edge. + * + * @return Always PanelPosition::Top. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + PanelPosition position() const override; + + /** + * @brief Returns the layout priority. + * + * @return Priority value (outermost on the edge). + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + int priority() const override; + + /** + * @brief Returns the bar thickness in device pixels. + * + * @return Preferred height in device pixels. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + int preferredSize() const override; + + /** + * @brief Returns the widget positioned by the layout engine. + * + * @return This status bar widget. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + QWidget* widget() const override; + + // -- IStatusBar ----------------------------------------------------------- + /** + * @brief Shows or hides the clock. + * + * @param[in] visible true to show the clock, false to hide it. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + void setTimeVisible(bool visible) override; + + /** + * @brief Reports clock visibility. + * + * @return true when the clock is visible. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + bool isTimeVisible() const override; + + /** + * @brief Shows or hides the system-icon cluster. + * + * @param[in] visible true to show icons, false to hide them. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + void setIconsVisible(bool visible) override; + + /** + * @brief Reports icon-cluster visibility. + * + * @return true when system icons are visible. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + bool isIconsVisible() const override; + + /** + * @brief Selects the clock/icon layout variant. + * + * @param[in] style The layout style to apply. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + void setStyle(StatusBarStyle style) override; + + /** + * @brief Returns the active layout variant. + * + * @return The current status bar style. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + StatusBarStyle style() const override; + + /** + * @brief Returns a weak reference to this status bar. + * + * @return WeakPtr valid for this instance's lifetime. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + WeakPtr GetWeak() const { return weak_factory_.GetWeakPtr(); } + + protected: + /** + * @brief Paints the background, clock, and icon glyphs. + * + * @param[in] event The paint event descriptor. + * + * @throws None + * @note Theme colors and fonts are resolved in applyTheme(). + * @warning None + * @since 0.19 + * @ingroup components + */ + void paintEvent(QPaintEvent* event) override; + + private slots: + /// @brief Refreshes the clock text each second. + void onTimeout(); + + private: + /// @brief Wires signals and starts the clock timer. + void setupUi(); + /// @brief Resolves theme colors and typography, then repaints. + void applyTheme(); + /// @brief Returns the formatted current time string (HH:mm). + QString currentTimeText() const; + /// @brief Returns the formatted current date string (yyyy/MM/dd). + QString currentDateText() const; + /// @brief Plays the boot fade-in (opacity 0 -> 1). + void startFadeIn(); + /// @brief Loads monochrome icon masks from compiled resources (qrc). + /// @note Masks stay null when the resources are absent; paintEvent then + /// falls back to the hand-drawn vector glyphs. + void loadIconMasks(); + /// @brief Recolors a monochrome mask to a target color, preserving alpha. + /// @param[in] mask Single-color icon on a transparent background. + /// @param[in] color Target fill color. + /// @return Tinted pixmap matching the mask's alpha shape. + static QPixmap tintedPixmap(const QPixmap& mask, const QColor& color); + + /// One-second clock timer. Ownership: this widget (Qt parent). + QTimer* timer_{nullptr}; + bool time_visible_{true}; + bool icons_visible_{true}; + StatusBarStyle style_{StatusBarStyle::Split}; + QString cached_time_; + QString cached_date_; + + // Resolved theme values (refreshed by applyTheme()). + QColor background_color_; + QColor surface_top_color_; ///< HCT tone-lifted surface, for the top of the gradient. + QColor foreground_color_; + QColor icon_color_; + QColor divider_color_; + QFont clock_font_; + + /// Boot fade-in opacity in [0, 1]; applied as painter opacity. + qreal fade_opacity_{0.0}; + + /// Cached monochrome icon masks (index = StatusIcon). Null entries fall + /// back to vector drawing in paintEvent. + QPixmap icon_masks_[4]; + + /// Weak pointer factory (must be the last member). + mutable cf::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/statusbar/statusbar_icons.qrc b/desktop/ui/components/statusbar/statusbar_icons.qrc new file mode 100644 index 000000000..535b3ffae --- /dev/null +++ b/desktop/ui/components/statusbar/statusbar_icons.qrc @@ -0,0 +1,8 @@ + + + icons/signal.png + icons/battery.png + icons/wifi.png + icons/volume.png + + diff --git a/document/development/02_quick_start.md b/document/development/02_quick_start.md index 234902354..3d49c7ee2 100644 --- a/document/development/02_quick_start.md +++ b/document/development/02_quick_start.md @@ -310,7 +310,7 @@ cat scripts/docker/logger/ci_build_*.log | tail -50 如果遇到本文未涵盖的问题: -1. 查看[设计文档](../../design_stage/)获取详细信息 +1. 查看[设计文档](../design_stage/)获取详细信息 2. 参考[构建系统文档](03_build_system.md) 3. 在 [GitHub](https://github.com/Awesome-Embedded-Learning-Studio/CFDesktop/issues) 上提交 issue diff --git a/document/notes/07-Logger-Flush-Lost-Wakeup-Hang.md b/document/notes/07-Logger-Flush-Lost-Wakeup-Hang.md new file mode 100644 index 000000000..e622e63f8 --- /dev/null +++ b/document/notes/07-Logger-Flush-Lost-Wakeup-Hang.md @@ -0,0 +1,105 @@ +--- +title: Logger Flush 丢失唤醒死锁 +description: 异步日志 logger_concurrency_test 在 clang/CI 下极端偶然卡死的根因定位与修复——flush_completed_cv_ 丢失唤醒(主因)与 MpscQueue readPos_ 数据竞争(次因) +--- + +# Logger Flush 丢失唤醒死锁 -- 排查与修复记录 + +## 背景 + +`logger_concurrency_test` 在 clang 构建(尤其 CI 高负载)下**极端偶然**地卡死:CTest 输出停在 `Start 11: logger_concurrency_test`,进程永不返回。直接怀疑死锁,但常规手段碰壁—— + +- 隔离运行该二进制 **23 次全部通过**(fast 配置:2 线程 × 50 条,队列 65536 永不满); +- 单核 `taskset`、填满队列的 stress 配置也无法在几次内复现; +- 静态读代码看不出任何 AB-BA 锁环。 + +一句话定性:**不是确定性死锁,是时序敏感的罕见竞态**。这类 bug 不能靠"多跑几次碰运气"定位,必须换工具。 + +## 排查方法论(可复用) + +定位罕见并发 bug 的三件套,按顺序用: + +1. **ThreadSanitizer(TSan)** —— 不依赖时序,基于 happens-before **确定性**报数据竞争与锁环。先确认"确有并发缺陷",再缩小范围。本例 TSan 当场抓到一处 `readPos_` 数据竞争,并(因 TSan 改变线程调度)顺带暴露了卡死。 + +2. **脱离重依赖的最小探针** —— logger 源码不依赖 Qt(`LogRecord` 是纯 `std`),于是用 `clang++` 直接编译 3 个 `.cpp` + 一个压测驱动,绕过整个 CMake/Qt 构建链。压测驱动刻意制造最坏组合:多生产者 + 一个**紧循环调 `flush_sync()`** 的 flusher 线程。复现率从"23 次不卡"提升到"约 1/3 卡死"。 + +3. **SIGABRT 看门狗 + gdb 作父进程** —— 罕见卡死无法 `gdb -p` 附加(ptrace_scope=1、无 sudo)。解法:探针内置看门狗线程,8 秒无进展则 `raise(SIGABRT)`;在 gdb 下 `run` 因信号自动中断,随后的 `-ex "thread apply all bt"` 便能在**卡死现场**抓全部线程栈。 + +最后用**逐事件插桩**(flush_sync 入口/出口、worker 完成事件、worker 心跳)打印到 stderr,在卡死时读末尾几行,精确还原控制流。 + +## 根因:`flush_completed_cv_` 丢失唤醒 + +`AsyncPostQueue::flush_sync()` 用 token 机制等待 flush 完成: + +```cpp +// flush_sync() —— 唯一无超时的阻塞点 +uint64_t my_token = flush_token_.fetch_add(1, acq_rel) + 1; +flush_requested_.store(true, release); +cv_.notify_one(); +std::unique_lock lock(flush_completed_mu_); +flush_completed_cv_.wait(lock, [my_token] { // 无 timeout + return flush_completed_.load(acquire) >= my_token || !running_; +}); +``` + +修复前,worker 完成 flush 时**未持锁**就 notify: + +```cpp +// worker_loop(修复前) +flush_completed_.store(current_token, release); +flush_completed_cv_.notify_all(); // ← 未持有 flush_completed_mu_ +``` + +致命窗口:flusher 已判定 predicate 为 false(`completed < my_token`)、尚未进入 futex 期间,worker 的 `notify_all` 正好飞过 —— 没人在 futex 里,通知**丢失**。flusher 随即进入 futex 永久阻塞,而该 `wait` **没有超时兜底**。 + +逐事件追踪抓到的现场(token 627): + +``` +[F-in ] tk=627 (token=627 comp=626) ← flusher 进入 wait(626>=627 false) +[W-done] comp=627 (flag was 1) ← worker 已置 completed=627 并 notify_all(未持锁) +[W-beat] qsize=0 fr=0 …8 秒… ← 再无 [F-out] tk=627,flusher 永久卡死 +``` + +**指纹**:`flush_token_ == flush_completed_`(token 已完成)却仍卡在 `wait`。这看似自相矛盾——predicate 明明为真——但 `wait(lock, pred)` 只在**被唤醒或伪唤醒时**才重判 predicate;notify 已丢,永远不重判。worker 之所以之后 `fr=0`,是因为 flusher 卡死、发不出 #628。 + +> 为何 clang/CI 更易触发:纯粹是该窗口的时序敏感,CI 高负载让 worker 与 flusher 的调度更容易撞进这个窗口,非编译器 bug。 + +## 修复:持锁 notify 关闭窗口 + +`pthread_cond_wait` 的语义保证"**原子地释放用户锁 + 登记为等待者**"。因此,只要 notify 方**持有同一把锁**,waiter 要么还没进 `wait`(之后取锁、重判 predicate 为真即返回),要么已在 futex(被这次 notify 唤醒、重判为真返回)。两个分支都不会丢。 + +关键决策: + +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| worker/stop 持 `flush_completed_mu_` 做 `store + notify_all` | 关闭丢失唤醒窗口,根治 | 给 `flush_completed_cv_.wait` 加超时:只是用轮询掩盖竞态(违背项目"no silent fallbacks"),且仍可能漏判 | +| notify 在锁内(而非解锁后) | `pthread_cond_wait` 原子解锁+登记语义要求 notify 持锁才能保证不丢;flush 是低频路径,唤醒后短暂等锁的开销可忽略 | 解锁后 notify:仍残留更小的丢唤醒窗口 | +| 不改 worker 唤醒路径(`cv_`/`wakeMu_`) | worker 用 `wait_for(10ms)` 有超时自愈,丢唤醒最多 10ms 延迟,非死锁 | 给 `submit` 热路径加 `wakeMu_` 锁竞争:得不偿失 | + +代码见 [async_queue.cpp](../../desktop/base/logger/src/async_queue/async_queue.cpp) 的 `worker_loop()` 与 `stop()`。 + +## 次因:`MpscQueue::readPos_` 数据竞争 + +TSan 顺带抓到的独立缺陷:`readPos_` 原为**非原子** `size_type`(Vyukov 队列的单消费者私有位),却被生产者经 `size()` 读取(`submit()` 的 drop 策略用它)。worker 写、生产者读,无同步 → 数据竞争(UB)。x86-64 上对齐 8 字节不会撕裂,故不致死锁,只影响 drop 判定的准确性,但是真实的并发 UB。 + +修复:`readPos_` 改为 `std::atomic`,消费者 `relaxed` 读 / `release` 推进,生产者 `size()`/`empty()` 用 `acquire` 读。代码见 [mpsc_queue.hpp](../../base/include/base/lockfree/mpsc_queue.hpp)。 + +## 验证 + +| 验证项 | 修复前 | 修复后 | +|--------|--------|--------| +| 独立探针(clang `-O2`)猛跑 | ~1/3 卡死 | 80 次零卡死 | +| TSan 探针 | 数据竞争 + 卡死 | **完全干净**,三次全到 `ALL DONE` | +| 项目 4 个 logger 测试 ×6 | — | 全通过 | +| `mpsc_queue_test` ×8 | — | 全通过 | +| clang `-Werror` 语法检查 | — | 通过 | + +两处改动共 +45/−21 行,均在并发核心,附详细注释说明"为何必须持锁 notify"。 + +## 可复用的经验 + +- **罕见竞态别靠多跑**:先用 TSan 拿确定性的 happens-before 证据,再缩小范围。 +- **绕过重依赖做最小探针**:能脱离 Qt/CMake 直接 `clang++` 编译的子系统,复现迭代快一个数量级。 +- **看门狗 + gdb 父进程**是无 sudo/受限 ptrace 环境下抓"卡死现场全栈"的通用套路。 +- **`condition_variable` notify 不持锁**是经典坑:带 predicate 的 `wait` 能挡住"notify 先于 wait 进入"的常见情况,但挡不住"notify 落在 pred 判定与 futex 进入之间"的窗口——只要 `wait` 无超时,这个窗口就是永久死锁。持锁 notify 是根治。 +- **`token == completed` 却卡在 wait** 是丢失唤醒的典型指纹:值已更新,但唤醒已丢,predicate 不再被重判。 diff --git a/document/notes/index.md b/document/notes/index.md index 4a1e84f4f..b16e0d5d9 100644 --- a/document/notes/index.md +++ b/document/notes/index.md @@ -89,6 +89,19 @@ description: 本文档系列详细介绍了桌面应用程序中窗口行为建 --- +### [07 - Logger Flush 丢失唤醒死锁](07-Logger-Flush-Lost-Wakeup-Hang.md) + +记录 `logger_concurrency_test` 在 clang/CI 下极端偶然卡死的完整排查与修复,包括: + +- 罕见竞态的定位方法论(TSan 确定性取证 → 脱离 Qt 的最小探针 → SIGABRT 看门狗 + gdb 父进程抓栈 → 逐事件插桩) +- 根因:`flush_completed_cv_` 丢失唤醒(worker notify 未持锁 + wait 无超时) +- 次因:`MpscQueue::readPos_` 数据竞争 +- 修复(持锁 notify)与验证矩阵 + +**适用场景**:排查 `condition_variable` 卡死、无超时 wait 的丢失唤醒、或需要复现罕见并发 bug 时参考。 + +--- + ## 架构概览 ```text diff --git a/document/status/current.md b/document/status/current.md new file mode 100644 index 000000000..bcbacf11b --- /dev/null +++ b/document/status/current.md @@ -0,0 +1,66 @@ + +--- +title: CFDesktop 当前项目状态 +description: CFDesktop 项目进度的唯一事实来源与全局导航。 +--- + +# CFDesktop 当前项目状态 + +> **校准日期**:2026-06-15 | **版本**:0.19.0 +> **本文件是项目进度的唯一事实来源(single source of truth)。** 其他位置一律指向此处,勿另行手抄。 + +## 项目导航(各信息去哪看) + +| 想了解 | 去看 | +|:---|:---| +| 项目定位 / 对外介绍 | [`README.md`](../../README.md) | +| 架构规则 / 构建命令 / 编码规范 | [`AGENT.md`](../../AGENT.md) | +| 组件 / API 用法(细节真相源) | [`document/HandBook/`](../HandBook/) | +| 脚本工具说明(细节真相源) | [`document/scripts/`](../scripts/) | +| 各 Phase 设计细节 | [`document/design_stage/`](../design_stage/) | +| 各模块待办清单 | [`document/todo/`](../todo/) | +| 已完成阶段历史归档 | [`document/todo/done/SUMMARY.md`](../todo/done/SUMMARY.md) | + +## 当前阶段 + +**一句话**:跑通「看到桌面 → 点图标 → 开应用」最小闭环。 + +## 进度状态(定性) + +### 三层架构 + +- **base**(`cfbase`):✅ 完成。硬件探针(CPU/Memory/GPU/Network) + HWTier 分级 + console + 工具库就绪。 +- **ui**(`cfui`):🚧 进行中。MD3 五层 pipeline + P0/P1 控件(19 个)完成;布局 / 手势 / P2-P3 控件待做。 +- **desktop**(`CFDesktop_shared`):🚧 进行中。DAG 初始化 / 4 层 ConfigStore / 异步 Logger / 窗口骨架就绪;Windows / WSL X11 后端完成;Wayland / 嵌入式待做。 + +### Phase + +- Phase 0 / 1(骨架 / 硬件探针含 HWTier):✅ 完成 +- Phase 2(基础库):🚧 进行中,接近完成 +- Phase 3(输入抽象)/ Phase 4(模拟器):⬜ 待开始 +- Phase 6(UI 框架 + 控件):P0/P1 ✅ · P2/P3 ⬜ +- Phase 8(测试):🚧 进行中 + +## 下一步路线(最小闭环先行) + +1. **MS2 状态栏**(顶部时间 + 系统图标)— ✅ 功能落地(`StatusBar` 实现 + 注册 PanelManager + 主题跟随 + MD3 美化;offscreen 启动通过,待真机视觉确认) +2. **MS3 任务栏**(底部居中图标条 + hover 动画)— ⬜ 待开始 +3. **MS4 应用启动器**(应用网格 + QProcess 启动)— ⬜ 待开始 +4. **MS5 窗口管理**(窗口装饰 + 任务栏联动)— ⬜ 待开始 + +闭环达成后按需推进:HWTier 策略引擎、CrashHandler、IPC、EGLFS 嵌入式后端、输入抽象层、P2/P3 控件。 + +## 最近里程碑(git 可证) + +- **2026-06**:`refactor: refactor the ui subsystem`;`hwtier system enabled`;文档清理 +- **已达成**:Milestone 1「桌面骨架可见」;Phase 0 / 1 / 2 / A(CI) / 6 / G(Widget) / H(显示后端)(详见 [SUMMARY.md](../todo/done/SUMMARY.md)) + +## 新人入门 + +1. [`README.md`](../../README.md) — 项目定位 +2. [`AGENT.md`](../../AGENT.md) — 构建命令(configure / fast build / run tests) +3. 本文件「下一步路线」— 取首个任务 MS2 上手 diff --git a/document/todo/base/02_input_layer.md b/document/todo/base/02_input_layer.md index aad1d07b4..934bd8702 100644 --- a/document/todo/base/02_input_layer.md +++ b/document/todo/base/02_input_layer.md @@ -209,8 +209,8 @@ description: "预计周期: 1~2 周,依赖阶段: Phase 0, Phase 1, Phase 2" ## 六、相关文档 - 设计文档: [../../design_stage/03_phase3_input_layer.md](../../design_stage/03_phase3_input_layer.md) -- 依赖: [工程骨架状态](../done/00_project_skeleton_status.md), [硬件探针状态](../done/01_hardware_probe_status.md) -- Base库已完成: [done/02_base_library_status.md](../done/02_base_library_status.md) +- 依赖: [工程骨架状态](../done/SUMMARY.md), [硬件探针状态](../done/SUMMARY.md) +- Base库已完成: [done/SUMMARY.md) --- diff --git a/document/todo/base/03_simulator.md b/document/todo/base/03_simulator.md index 77fa73ac5..6b855446c 100644 --- a/document/todo/base/03_simulator.md +++ b/document/todo/base/03_simulator.md @@ -264,7 +264,7 @@ description: "预计周期: 2~3 周,依赖阶段: Phase 0, Phase 2, Phase 3" ## 七、相关文档 - 设计文档: [../../design_stage/04_phase6_simulator.md](../../design_stage/04_phase6_simulator.md) -- 依赖: [工程骨架状态](../done/00_project_skeleton_status.md), [02_input_layer.md](02_input_layer.md) +- 依赖: [工程骨架状态](../done/SUMMARY.md), [02_input_layer.md](02_input_layer.md) --- diff --git a/document/todo/base/04_testing.md b/document/todo/base/04_testing.md index 4f4fcd6ea..f8277067b 100644 --- a/document/todo/base/04_testing.md +++ b/document/todo/base/04_testing.md @@ -9,7 +9,7 @@ description: "预计周期: 贯穿全程,依赖阶段: 所有阶段" > **预计周期**: 贯穿全程 > **依赖阶段**: 所有阶段 > **目标交付物**: 单元测试框架、集成测试、UI 自动化、CI/CD 配置 -> **完成归档**: [document/todo/done/05_testing_status.md](../done/05_testing_status.md) +> **完成归档**: [document/todo/done/SUMMARY.md) --- @@ -237,7 +237,7 @@ description: "预计周期: 贯穿全程,依赖阶段: 所有阶段" - 设计文档: [../../design_stage/05_phase8_testing.md](../../design_stage/05_phase8_testing.md) - 依赖: 所有其他阶段文档 -- 测试状态: [../done/05_testing_status.md](../done/05_testing_status.md) +- 测试状态: [../done/SUMMARY.md) --- diff --git a/document/todo/base/99_ui_material_framework.md b/document/todo/base/99_ui_material_framework.md index 4cb50321c..b3feb272e 100644 --- a/document/todo/base/99_ui_material_framework.md +++ b/document/todo/base/99_ui_material_framework.md @@ -189,7 +189,7 @@ Layer 1: Core Math & Utility Layer (math_helper, color, geometry, ...) ## 五、相关文档 -- 依赖: [工程骨架状态](../done/00_project_skeleton_status.md), [02_input_layer.md](02_input_layer.md) +- 依赖: [工程骨架状态](../done/SUMMARY.md), [02_input_layer.md](02_input_layer.md) --- diff --git a/document/todo/desktop/06_infrastructure.md b/document/todo/desktop/06_infrastructure.md index 43267cd39..a825778bf 100644 --- a/document/todo/desktop/06_infrastructure.md +++ b/document/todo/desktop/06_infrastructure.md @@ -8,14 +8,14 @@ description: "状态: 🚧 部分完成 (~50%),预计周期: 4~5 周" > **状态**: 🚧 部分完成 (~50%) > **预计周期**: 4~5 周 > **依赖阶段**: Phase 1, Phase 2, Phase 3 -> **已完成归档**: [done/06_infrastructure_status.md](../done/06_infrastructure_status.md) +> **已完成归档**: [done/SUMMARY.md) --- ## 已完成模块 > GPU 检测器、Network 检测器、ConfigStore、Logger 已完成。 -> 详细状态请参考: [done/06_infrastructure_status.md](../done/06_infrastructure_status.md) +> 详细状态请参考: [done/SUMMARY.md) --- @@ -277,7 +277,7 @@ description: "状态: 🚧 部分完成 (~50%),预计周期: 4~5 周" ## 五、相关文档 - 设计文档: [../desktop/summary.md](summary.md) Phase A 节 -- 依赖: [硬件探针状态](../done/01_hardware_probe_status.md), [Base库状态](../done/02_base_library_status.md) +- 依赖: [硬件探针状态](../done/SUMMARY.md), [Base库状态](../done/SUMMARY.md) - CMake 配置: `../../cmake/` (参考现有模块集成方式) --- diff --git a/document/todo/desktop/07_render_backend.md b/document/todo/desktop/07_render_backend.md index 49067ebd9..548c516b6 100644 --- a/document/todo/desktop/07_render_backend.md +++ b/document/todo/desktop/07_render_backend.md @@ -8,13 +8,13 @@ description: "状态: 🚧 部分完成 (接口已实现,具体后端待开发 > **状态**: 🚧 部分完成 (接口已实现,具体后端待开发) > **预计周期**: 2 周 > **依赖阶段**: Phase 6 -> **已完成归档**: [done/14_display_backend_status.md](../done/14_display_backend_status.md) +> **已完成归档**: [done/SUMMARY.md) ## 已完成模块 > RenderBackend 抽象接口、BackendCapabilities、RenderBackendFactory 已实现。 > 显示后端架构 (IDisplayServerBackend)、Windows 后端、WSL X11 后端已完成。 -> 详细状态请参考: [done/14_display_backend_status.md](../done/14_display_backend_status.md) +> 详细状态请参考: [done/SUMMARY.md) --- diff --git a/document/todo/desktop/08_p1_controls.md b/document/todo/desktop/08_p1_controls.md index 5114276c8..1f810e631 100644 --- a/document/todo/desktop/08_p1_controls.md +++ b/document/todo/desktop/08_p1_controls.md @@ -7,7 +7,7 @@ description: "完成日期: 2026-03-18,详细完成状态: done/13widgetappsst > **状态**: ✅ 已完成 > **完成日期**: 2026-03-18 -> **详细完成状态**: [done/13_widget_apps_status.md](../done/13_widget_apps_status.md) +> **详细完成状态**: [done/SUMMARY.md) --- @@ -27,7 +27,7 @@ description: "完成日期: 2026-03-18,详细完成状态: done/13widgetappsst ## 相关文档 -- 完成归档: [done/13_widget_apps_status.md](../done/13_widget_apps_status.md) +- 完成归档: [done/SUMMARY.md) - 设计文档: [summary.md](summary.md) Phase C 节 - Material Design 3 规范: https://m3.material.io/ diff --git a/document/todo/desktop/09_window_manager.md b/document/todo/desktop/09_window_manager.md index ab029fe13..51003d9d7 100644 --- a/document/todo/desktop/09_window_manager.md +++ b/document/todo/desktop/09_window_manager.md @@ -8,13 +8,13 @@ description: "状态: 🚧 部分完成,依赖阶段: Phase 6, Phase 7" > **状态**: 🚧 部分完成 > **预计周期**: 3 周 > **依赖阶段**: Phase 6, Phase 7 -> **已完成归档**: [done/14_display_backend_status.md](../done/14_display_backend_status.md) +> **已完成归档**: [done/SUMMARY.md) ## 已完成模块 > 基础 WindowManager (弱引用模式)、基础 IWindow 接口、IWindowBackend 接口已完成。 > Windows 和 WSL X11 平台后端已实现。 -> 详细状态: [done/14_display_backend_status.md](../done/14_display_backend_status.md) +> 详细状态: [done/SUMMARY.md) ### 已实现 - [x] `WindowManager` 窗口注册/查询/关闭/置顶 (弱引用模式) diff --git a/document/todo/desktop/index.md b/document/todo/desktop/index.md index e904ac574..87ae5a614 100644 --- a/document/todo/desktop/index.md +++ b/document/todo/desktop/index.md @@ -33,7 +33,7 @@ description: 桌面本体 (Desktop Shell) 开发规划 | 文件 | 里程碑 | 核心交付 | 状态 | |------|--------|----------|------| | [milestone_00_overview.md](milestone_00_overview.md) | 总览 | 依赖关系 + 可复用资产 + 可跳过模块 | 📋 参考 | -| [milestone_01_desktop_skeleton.md](milestone_01_desktop_skeleton.md) | MS1: 桌面骨架 | 壁纸/背景 + 面板布局 | ⬜ 待开始 | +| [../done/SUMMARY.md](../done/SUMMARY.md) | MS1: 桌面骨架 | 壁纸/背景 + 面板布局 | ⬜ 待开始 | | [milestone_02_status_bar.md](milestone_02_status_bar.md) | MS2: 状态栏 | 顶部时间+系统图标 | ⬜ 待开始 | | [milestone_03_taskbar.md](milestone_03_taskbar.md) | MS3: 任务栏 | 底部居中图标+hover动画 | ⬜ 待开始 | | [milestone_04_app_launcher.md](milestone_04_app_launcher.md) | MS4: 应用启动器 | 弹出应用网格+进程启动 | ⬜ 待开始 | @@ -45,9 +45,9 @@ description: 桌面本体 (Desktop Shell) 开发规划 ## 已完成归档 详见 [../done/](../done/) 目录,特别是: -- [../done/14_display_backend_status.md](../done/14_display_backend_status.md) — 显示后端完成状态 -- [../done/13_widget_apps_status.md](../done/13_widget_apps_status.md) — Widget + 控件完成状态 -- [../done/06_infrastructure_status.md](../done/06_infrastructure_status.md) — 基础设施完成状态 +- [../done/SUMMARY.md) — 显示后端完成状态 +- [../done/SUMMARY.md) — Widget + 控件完成状态 +- [../done/SUMMARY.md) — 基础设施完成状态 --- diff --git a/document/todo/desktop/milestone_00_overview.md b/document/todo/desktop/milestone_00_overview.md index 9ac74fa0d..e08ec7e51 100644 --- a/document/todo/desktop/milestone_00_overview.md +++ b/document/todo/desktop/milestone_00_overview.md @@ -14,7 +14,7 @@ description: "创建日期: 2026-03-31,目标: 将当前空白桌面推进到 | # | 里程碑 | 状态 | 核心交付 | 预计工作量 | |---|--------|------|----------|-----------| -| MS1 | [桌面骨架可见](done/milestone_01_desktop_skeleton.md) | ✅ | 壁纸/背景 + 正确的面板布局计算 | 1-2 天 | +| MS1 | [桌面骨架可见](../done/SUMMARY.md) | ✅ | 壁纸/背景 + 正确的面板布局计算 | 1-2 天 | | MS2 | [状态栏](milestone_02_status_bar.md) | ⬜ | 顶部时间+系统图标面板 | 3-5 天 | | MS3 | [任务栏/导航栏](milestone_03_taskbar.md) | ⬜ | 底部任务栏 (居中图标) | 5-7 天 | | MS4 | [应用启动器](milestone_04_app_launcher.md) | ⬜ | 应用网格弹窗 + 外部进程启动 | 5-7 天 | diff --git a/document/todo/desktop/milestone_02_status_bar.md b/document/todo/desktop/milestone_02_status_bar.md index 6c34bdb1c..a6aa11672 100644 --- a/document/todo/desktop/milestone_02_status_bar.md +++ b/document/todo/desktop/milestone_02_status_bar.md @@ -5,11 +5,38 @@ description: "预计周期: 3-5 天,前置依赖: Milestone 1: 桌面骨架可 # Milestone 2: 状态栏 -> **状态**: ⬜ 待开始 +> **状态**: ✅ 功能落地(v1 基础 + v2 MD3 美化) > **预计周期**: 3-5 天 -> **前置依赖**: [Milestone 1: 桌面骨架可见](milestone_01_desktop_skeleton.md) +> **前置依赖**: [Milestone 1: 桌面骨架可见](../done/SUMMARY.md) > **目标**: 屏幕顶部出现一条状态栏,显示时间、基础系统图标 +## 实现结果与偏差(2026-06-15) + +**已交付**:`StatusBar` 顶层面板,注册到 PanelManager 作为 Top edge panel, +顶部一条 48dp 状态栏,显示时间 (HH:MM)、系统图标簇(信号/电池/WiFi/音量), +背景与图标跟随 ThemeManager 主题;offscreen 启动通过、Doxygen lint 通过。 + +**与原计划的偏差**(实现时发现真实代码与本文档措辞不符,已按代码库实际 API 落地): + +1. **`PanelManager::registerPanel()` 原有 bug**:只校验、不把 panel 存入 `panels` + 向量,导致面板注册后永远不会被布局。已修复(`panels.push_back(panel)`)。 +2. **主题 API 名称不同**:实际接口是 `ICFTheme::color_scheme()` / `font_type()` + (非本文档写的 `colorScheme()` / `typography()`);且**无 `surfaceContainer` token**, + 实际用 `SURFACE` / `ON_SURFACE` / `ON_SURFACE_VARIANT` / `OUTLINE_VARIANT`。 +3. **主题管线原本未接通**:`main.cpp` 用裸 `QApplication`,而 Material 主题只在 + 构造 `MaterialApplication` 时才注册到 ThemeManager。已将 `main.cpp` 切到 + `MaterialApplication`,主题在运行时真正可用(顺带让全部 MD3 控件拿到真实主题色)。 +4. **v2 MD3 美化**(嵌入式安全,纯 QPainter,不引入 `Qt6::Svg` 依赖): + HCT 色调抬升的垂直渐变表面 + 底部接缝处 in-band 软阴影 + 横向渐隐发丝线 + + 开机 250ms 淡入。图标用 **icons8 单色 PNG 蒙版 + 运行时 `SourceIn` 染色** + 跟随主题(嵌入式只需 Qt GUI,无需 Svg 模块;资源经 qrc 编译进二进制, + 无文件系统依赖)。**无静默兜底**:mask 加载失败则该图标留可见空白并打 + WARNING 日志(不替换掩盖)。资源用 `Q_INIT_RESOURCE`(全局作用域 helper + 包裹,因该宏不能在命名空间内调用)防止静态库 qrc 被链接器剥离。 + 图标来源/署名见 `desktop/ui/components/statusbar/icons/LICENSE.md`。 + +**待人工确认**:真机/WSLg 显示下的视觉效果(本地无显示设备,仅 offscreen 验证不崩)。 + --- ## 一、阶段目标 diff --git a/document/todo/index.md b/document/todo/index.md index 8903b9e18..671f7d3ef 100644 --- a/document/todo/index.md +++ b/document/todo/index.md @@ -5,20 +5,18 @@ description: 本目录包含 CFDesktop 项目各模块的详细 TODO 清单, # CFDesktop 项目 TODO 看板 -本目录包含 CFDesktop 项目各模块的详细 TODO 清单,基于 `design_stage` 设计文档和 `MaterialRules.md` 架构规范整理而成。 +本目录包含 CFDesktop 项目各模块的详细 TODO 清单,基于 [`design_stage`](../design_stage/) 设计文档与 [`AGENT.md`](../../AGENT.md) 架构规范整理而成。 ## 模块索引 -| TODO 文件 | 模块 | 预计周期 | 依赖 | 状态 | -|----------|------|---------|------|------| -| [done/00_project_skeleton_status.md](done/00_project_skeleton_status.md) | 工程骨架搭建 | 1~2 周 | - | ✅ 100% | -| [done/01_hardware_probe_status.md](done/01_hardware_probe_status.md) | 硬件探针与能力分级 | 2~3 周 | Phase 0 | 🚧 90% | -| ~~[done/02_base_library_status.md](done/02_base_library_status.md)~~ | ~~Base 库核心~~ | ~~3~4 周~~ | Phase 0, 1 | ✅ 100% | -| [02_input_layer.md](base/02_input_layer.md) | 输入抽象层 | 1~2 周 | Phase 0, 1 | ⬜ 0% | -| [03_simulator.md](base/03_simulator.md) | 多平台模拟器 | 2~3 周 | Phase 0, 2 | ⬜ 0% | -| [04_testing.md](base/04_testing.md) | 测试体系 | 贯穿全程 | 所有阶段 | 🚧 55% | -| [99_ui_material_framework.md](base/99_ui_material_framework.md) | UI Material Framework | 持续迭代 | Phase 0-3 | 🚧 95% | -| desktop/ | Desktop 模块 (显示后端+窗口管理) | 持续迭代 | Phase 0-6 | 🚧 90% | +| 模块 | 待办清单 | 预计周期 | 依赖 | 状态 | +|------|---------|---------|------|------| +| 工程骨架 / 硬件探针 / Base 库 | [`done/SUMMARY.md`](done/SUMMARY.md)(归档) | — | — | ✅ 完成 | +| 输入抽象层 | [`base/02_input_layer.md`](base/02_input_layer.md) | 1~2 周 | Phase 0, 1 | ⬜ 待开始 | +| 多平台模拟器 | [`base/03_simulator.md`](base/03_simulator.md) | 2~3 周 | Phase 0, 2 | ⬜ 待开始 | +| 测试体系 | [`base/04_testing.md`](base/04_testing.md) | 贯穿全程 | 所有阶段 | 🚧 进行中 | +| UI Material Framework | [`base/99_ui_material_framework.md`](base/99_ui_material_framework.md) | 持续迭代 | Phase 0-3 | 🚧 进行中 | +| Desktop 模块(显示后端 + 窗口管理) | [`desktop/`](desktop/) | 持续迭代 | Phase 0-6 | 🚧 进行中 | ## 状态图例 @@ -30,10 +28,10 @@ description: 本目录包含 CFDesktop 项目各模块的详细 TODO 清单, ## 项目状态报告 -- **综合报告**: [PROJECT_STATUS_REPORT.md](done/PROJECT_STATUS_REPORT.md) -- **整体完成度**: 约 75% -- **显示后端详情**: [done/14_display_backend_status.md](done/14_display_backend_status.md) — Windows + WSL X11 后端已完成 +> 项目进度以 [`../status/current.md`](../status/current.md) 为唯一事实来源。本看板仅维护各模块的待办清单,不再重复声明整体完成度。 + +已完成阶段的归档见 [`done/SUMMARY.md`](done/SUMMARY.md)。 --- -*最后更新: 2026-03-30* +*看板最后更新: 2026-06-15* diff --git a/scripts/release/hooks/install_hooks.ps1 b/scripts/release/hooks/install_hooks.ps1 index a6c2a27c4..950dc127a 100644 --- a/scripts/release/hooks/install_hooks.ps1 +++ b/scripts/release/hooks/install_hooks.ps1 @@ -4,6 +4,8 @@ # # 用法: .\scripts\release\hooks\install_hooks.ps1 # +# 推荐:直接 cmake configure 即自动设置 core.hooksPath,无需本脚本。 +# 本脚本为 fallback(复制钩子到 .git/hooks/),仅用于不跑 cmake 的场景。 # 此脚本将钩子安装到 .git/hooks/ 目录 # ============================================================================= @@ -155,10 +157,10 @@ Log-Info "安装 Git Hooks..." Write-Host "" # 安装 pre-commit -Install-Hook "pre-commit.sample" "pre-commit" +Install-Hook "pre-commit" "pre-commit" # 安装 pre-push -Install-Hook "pre-push.sample" "pre-push" +Install-Hook "pre-push" "pre-push" # ============================================================================= # 完成 diff --git a/scripts/release/hooks/install_hooks.sh b/scripts/release/hooks/install_hooks.sh index b79cda9d7..d3ef8aac7 100755 --- a/scripts/release/hooks/install_hooks.sh +++ b/scripts/release/hooks/install_hooks.sh @@ -5,6 +5,8 @@ # # 用法: bash scripts/release/hooks/install_hooks.sh # +# 推荐:直接 `cmake configure` 即自动设置 core.hooksPath,无需本脚本。 +# 本脚本为 fallback(复制钩子到 .git/hooks/),仅用于不跑 cmake 的场景。 # 此脚本将钩子安装到 .git/hooks/ 目录 # ============================================================================= @@ -109,9 +111,9 @@ backup_existing_hooks() { # 安装单个钩子 # ============================================================================= install_hook() { - local sample_name="$1" + local source_name="$1" local hook_name="$2" - local source_hook="$HOOKS_SOURCE_DIR/$sample_name" + local source_hook="$HOOKS_SOURCE_DIR/$source_name" local target_hook="$HOOKS_TARGET_DIR/$hook_name" # 备份现有钩子 @@ -131,17 +133,17 @@ log_info "安装 Git Hooks..." echo "" # 安装 pre-commit -if [[ -f "$HOOKS_SOURCE_DIR/pre-commit.sample" ]]; then - install_hook "pre-commit.sample" "pre-commit" +if [[ -f "$HOOKS_SOURCE_DIR/pre-commit" ]]; then + install_hook "pre-commit" "pre-commit" else - log_warning "pre-commit.sample 不存在,跳过" + log_warning "pre-commit 不存在,跳过" fi # 安装 pre-push -if [[ -f "$HOOKS_SOURCE_DIR/pre-push.sample" ]]; then - install_hook "pre-push.sample" "pre-push" +if [[ -f "$HOOKS_SOURCE_DIR/pre-push" ]]; then + install_hook "pre-push" "pre-push" else - log_warning "pre-push.sample 不存在,跳过" + log_warning "pre-push 不存在,跳过" fi # ============================================================================= diff --git a/scripts/release/hooks/pre-commit.sample b/scripts/release/hooks/pre-commit old mode 100644 new mode 100755 similarity index 74% rename from scripts/release/hooks/pre-commit.sample rename to scripts/release/hooks/pre-commit index 804e17f5d..0684bf52b --- a/scripts/release/hooks/pre-commit.sample +++ b/scripts/release/hooks/pre-commit @@ -1,136 +1,172 @@ -#!/bin/bash -# ============================================================================= -# Git Pre-Commit Hook - 本地代码质量检查 -# ============================================================================= -# -# 安装方法: bash scripts/release/hooks/install_hooks.sh -# -# 检查内容: -# 1. C++代码格式(需要clang-format) -# 2. Doxygen注释检查(需要Python) -# -# 绕过方法: git commit --no-verify -m "message" -# ============================================================================= - -# ============================================================================= -# 颜色定义 -# ============================================================================= -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -# ============================================================================= -# 日志函数 -# ============================================================================= -log_info() { - echo -e "${BLUE}>>>${NC} $1" -} - -log_success() { - echo -e "${GREEN}✓${NC} $1" -} - -log_warning() { - echo -e "${YELLOW}⚠${NC} $1" -} - -log_error() { - echo -e "${RED}✗${NC} $1" -} - -# ============================================================================= -# 获取项目根目录 -# ============================================================================= -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" - -# ============================================================================= -# 1. C++ 格式化(如果 clang-format 可用) -# ============================================================================= -if command -v clang-format >/dev/null 2>&1; then - cd "$PROJECT_ROOT" || exit 1 - - # 获取暂存的 C/C++ 文件 - STAGED_CPP_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(cpp|cc|cxx|h|hpp|hxx)$' || true) - - if [ -n "$STAGED_CPP_FILES" ]; then - log_info "格式化暂存的 C/C++ 文件..." - - # 对每个暂存的 C/C++ 文件运行 clang-format - echo "$STAGED_CPP_FILES" | while IFS= read -r file; do - [ -z "$file" ] && continue - [ ! -f "$file" ] && continue - - # 格式化文件 - clang-format -i "$file" 2>/dev/null || true - done - - # 重新添加格式化后的文件到暂存区 - echo "$STAGED_CPP_FILES" | while IFS= read -r file; do - [ -z "$file" ] && continue - git add "$file" 2>/dev/null || true - done - - log_success "C++ 文件已格式化" - fi -fi - -# ============================================================================= -# 2. Doxygen 注释检查(如果 Python 可用) -# ============================================================================= -PYTHON_CMD="" -if command -v python3 >/dev/null 2>&1; then - PYTHON_CMD="python3" -elif command -v python >/dev/null 2>&1; then - PYTHON_CMD="python" -fi - -if [ -n "$PYTHON_CMD" ]; then - cd "$PROJECT_ROOT" || exit 1 - - # 检查是否有暂存的头文件 - STAGED_HEADER_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(h|hpp|hxx)$' || true) - - if [ -n "$STAGED_HEADER_FILES" ]; then - log_info "检查 Doxygen 注释..." - - # 运行 doxygen lint - # 捕获输出和退出码 - LINT_OUTPUT=$("$PYTHON_CMD" "$PROJECT_ROOT/scripts/doxygen/lint.py" 2>&1) - LINT_EXIT_CODE=$? - - if [ $LINT_EXIT_CODE -ne 0 ]; then - # 检查输出中是否包含 "FAILED" 字样来确认真正的失败 - if echo "$LINT_OUTPUT" | grep -q "FAILED"; then - log_error "Doxygen 注释检查失败" - echo "" - echo "发现的违规已写入: FAILED_DOXYGEN.md" - echo "" - echo "修复方法:" - echo " 1. 查看 FAILED_DOXYGEN.md 了解详细问题" - echo " 2. 参考 document/DOXYGEN_REQUEST.md 添加/修复 Doxygen 注释" - echo "" - echo -e "${YELLOW}提示: 使用 --no-verify 可跳过此检查(不推荐)${NC}" - exit 1 - else - # 输出中有 "All Doxygen checks passed" 但退出码非零 - # 这可能是 Windows + Git bash 的兼容性问题,忽略 - log_success "Doxygen 注释检查通过" - fi - else - log_success "Doxygen 注释检查通过" - fi - fi -else - log_warning "未找到 Python,跳过 Doxygen 注释检查" -fi - -# ============================================================================= -# 完成 -# ============================================================================= -echo "" -log_success "Pre-Commit 检查全部通过" -echo "" -exit 0 +#!/bin/bash +# ============================================================================= +# Git Pre-Commit Hook - 本地代码质量检查 +# ============================================================================= +# +# 安装方法: bash scripts/release/hooks/install_hooks.sh +# +# 检查内容: +# 1. C++代码格式(需要clang-format) +# 2. Doxygen注释检查(需要Python) +# +# 绕过方法: git commit --no-verify -m "message" +# ============================================================================= + +# ============================================================================= +# 颜色定义 +# ============================================================================= +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# ============================================================================= +# 日志函数 +# ============================================================================= +log_info() { + echo -e "${BLUE}>>>${NC} $1" +} + +log_success() { + echo -e "${GREEN}✓${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}⚠${NC} $1" +} + +log_error() { + echo -e "${RED}✗${NC} $1" +} + +# ============================================================================= +# 获取项目根目录 +# ============================================================================= +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Position-independent: works under core.hooksPath (scripts/release/hooks/) or .git/hooks/ +PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" +[ -z "$PROJECT_ROOT" ] && PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# ============================================================================= +# 1. C++ 格式化(如果 clang-format 可用) +# ============================================================================= +if command -v clang-format >/dev/null 2>&1; then + cd "$PROJECT_ROOT" || exit 1 + + # 获取暂存的 C/C++ 文件 + STAGED_CPP_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(cpp|cc|cxx|h|hpp|hxx)$' || true) + + if [ -n "$STAGED_CPP_FILES" ]; then + log_info "格式化暂存的 C/C++ 文件..." + + # 对每个暂存的 C/C++ 文件运行 clang-format + echo "$STAGED_CPP_FILES" | while IFS= read -r file; do + [ -z "$file" ] && continue + [ ! -f "$file" ] && continue + + # 格式化文件 + clang-format -i "$file" 2>/dev/null || true + done + + # 重新添加格式化后的文件到暂存区 + echo "$STAGED_CPP_FILES" | while IFS= read -r file; do + [ -z "$file" ] && continue + git add "$file" 2>/dev/null || true + done + + log_success "C++ 文件已格式化" + fi +fi + +# ============================================================================= +# 2. Doxygen 注释检查(如果 Python 可用) +# ============================================================================= +PYTHON_CMD="" +if command -v python3 >/dev/null 2>&1; then + PYTHON_CMD="python3" +elif command -v python >/dev/null 2>&1; then + PYTHON_CMD="python" +fi + +if [ -n "$PYTHON_CMD" ]; then + cd "$PROJECT_ROOT" || exit 1 + + # 检查是否有暂存的头文件 + STAGED_HEADER_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(h|hpp|hxx)$' || true) + + if [ -n "$STAGED_HEADER_FILES" ]; then + log_info "检查 Doxygen 注释..." + + # 运行 doxygen lint + # 捕获输出和退出码 + LINT_OUTPUT=$("$PYTHON_CMD" "$PROJECT_ROOT/scripts/doxygen/lint.py" 2>&1) + LINT_EXIT_CODE=$? + + if [ $LINT_EXIT_CODE -ne 0 ]; then + # 检查输出中是否包含 "FAILED" 字样来确认真正的失败 + if echo "$LINT_OUTPUT" | grep -q "FAILED"; then + log_error "Doxygen 注释检查失败" + echo "" + echo "发现的违规已写入: FAILED_DOXYGEN.md" + echo "" + echo "修复方法:" + echo " 1. 查看 FAILED_DOXYGEN.md 了解详细问题" + echo " 2. 参考 document/DOXYGEN_REQUEST.md 添加/修复 Doxygen 注释" + echo "" + echo -e "${YELLOW}提示: 使用 --no-verify 可跳过此检查(不推荐)${NC}" + exit 1 + else + # 输出中有 "All Doxygen checks passed" 但退出码非零 + # 这可能是 Windows + Git bash 的兼容性问题,忽略 + log_success "Doxygen 注释检查通过" + fi + else + log_success "Doxygen 注释检查通过" + fi + fi +else + log_warning "未找到 Python,跳过 Doxygen 注释检查" +fi + +# ============================================================================= +# 3. 三层依赖检查(base 不可依赖 ui/desktop;ui 不可依赖 desktop) +# ============================================================================= +cd "$PROJECT_ROOT" || exit 1 +log_info "检查三层依赖..." +LAYER_ERR=0 +while IFS= read -r file; do + [ -z "$file" ] && continue + [ ! -f "$file" ] && continue + case "$file" in + base/*) + if grep -nE '#include[[:space:]]+[<"]?(ui/|desktop/)' "$file" >/dev/null 2>&1; then + log_error "base 层违规: $file 引用了 ui/ 或 desktop/" + grep -nE '#include[[:space:]]+[<"]?(ui/|desktop/)' "$file" + LAYER_ERR=1 + fi + ;; + ui/*) + if grep -nE '#include[[:space:]]+[<"]?desktop/' "$file" >/dev/null 2>&1; then + log_error "ui 层违规: $file 引用了 desktop/" + grep -nE '#include[[:space:]]+[<"]?desktop/' "$file" + LAYER_ERR=1 + fi + ;; + esac +done <<< "$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(cpp|cc|cxx|h|hpp|hxx)$' || true)" +if [ "$LAYER_ERR" -ne 0 ]; then + echo "" + echo "三层依赖规则: base ↛ ui/desktop, ui ↛ desktop(见 AGENT.md Architecture)" + echo "提示: 使用 --no-verify 可跳过(不推荐)" + exit 1 +fi +log_success "三层依赖检查通过" + +# ============================================================================= +# 完成 +# ============================================================================= +echo "" +log_success "Pre-Commit 检查全部通过" +echo "" +exit 0 diff --git a/scripts/release/hooks/pre-push.sample b/scripts/release/hooks/pre-push old mode 100644 new mode 100755 similarity index 95% rename from scripts/release/hooks/pre-push.sample rename to scripts/release/hooks/pre-push index 1cfe14dfc..6ec88b891 --- a/scripts/release/hooks/pre-push.sample +++ b/scripts/release/hooks/pre-push @@ -44,7 +44,9 @@ log_error() { # Project root # ============================================================================= SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +# Position-independent: works under core.hooksPath (scripts/release/hooks/) or .git/hooks/ +PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" +[ -z "$PROJECT_ROOT" ] && PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" cd "$PROJECT_ROOT" # =============================================================================