基于 GPUI 的本地优先 Markdown 笔记桌面应用,目标是提供 Obsidian 的基础能力: 目录管理、Markdown 编辑、实时预览,以及 JSON / YAML / TOML 数据文件的编辑与格式化预览。
核心原则:笔记就是磁盘上普通的 .md 文件——不需要账号,不上传云端,不转换格式,
不引入数据库。索引、缓存、配置都是可随时重建的派生数据。
- 需求规格:
docs/REQUIREMENTS.md - 技术设计:
docs/DESIGN.md
| 功能 | 说明 |
|---|---|
| 打开文件夹作为库 | Cmd+O 选择任意文件夹;库根就是普通目录,可被 Obsidian 直接打开 |
| 多库管理 | 独立「库管理」窗口:新增、切换、改名、重新定位(路径失效时重选目录且 id 不变)、删除(仅移除记录,绝不删磁盘);每个库带状态徽标(当前库 / 失效 / 重复 / 嵌套) |
| 目录树浏览 | 递归扫描,目录在前、同层按名称排序,库根默认展开 |
| 文件类型图标 | 目录 / Markdown / 图片 / 代码 / 数据文件 / 压缩包 / 表格 / 其他 |
| 默认可见范围 | 可编辑文本(.md、.json、.yaml、.yml、.toml)+ 图片;开启"显示全部"后其他类型也进树 |
| 始终忽略 | .git、node_modules、target、.DS_Store、*.md.tmp 以及(未开"显示全部"时的)点开头隐藏项 |
| 新建 | 新建笔记(自动补 .md、重名报错)、新建目录 |
| 重命名 / 删除 / 移动 | 工具栏按钮、右键菜单、F2;禁止删除库根与库外路径 |
| 树内拖拽移动 | 拖拽带文件名浮层、悬停高亮落点;禁止移入自身或子目录、同名自动改名不覆盖;跨设备走"复制 + 大小校验 + 删源",大小不一致保留源文件并报错 |
| 外部导入 | 从访达拖入文件/目录即复制进库;也支持复制模式/移动模式的批量导入 |
| 粘贴 | 访达复制的文件导入,或截图直接写成 pasted-<时间戳>.<ext> |
| 右键菜单 | 重命名、删除、在此新建笔记/文件夹、移动到…、复制相对路径、复制为 Markdown 链接、插入到当前笔记、粘贴到此目录 |
| 自动刷新 | notify 递归监听库目录;仅"新建/删除"才重扫整棵树,"内容修改"不触发重扫 |
| 功能 | 说明 |
|---|---|
| 源码模式编辑 | 基于代码编辑控件(行号可开关),Rope 存储,不逐字符拷贝全文 |
| 格式工具栏 | 13 个按钮:加粗、斜体、删除线、标题、无序列表、有序列表、任务列表、引用、代码块、链接、图片、表格、分割线。有选区则包裹,无选区则插入占位并把光标放中间 |
| 自动保存 | 停止输入 300ms(可在 200–60000ms 间配置)后防抖写入;每次输入覆盖上一个任务,旧任务自动取消 |
| 立即保存 | Cmd+S 绕过防抖立刻写盘 |
| 原子写盘 | 同目录写 .tmp 再 rename,崩溃绝不产生半截文件;失败清理临时文件 |
| 脏标记 | 未保存时标签标题显示 ● |
| 外部修改处理 | 磁盘文件被外部改动:无未保存修改则自动重载;有未保存修改则保留你的内容并提示,绝不静默覆盖 |
| 多标签 | 同一文件只会有一个标签(重复打开只激活已有标签);标签按打开顺序排列,每个标签独立关闭按钮 |
| 最后一个标签 | 关闭最后一个标签不清空编辑区,而是置为空白占位页,保证下次打开有落点 |
| 会话恢复 | 重启后恢复上次的标签顺序与活动标签(按当前库过滤,失效路径自动剔除) |
| 缩进配置 | Tab / 空格、缩进宽度 1–16,改设置立即生效 |
| 功能 | 说明 |
|---|---|
| Markdown 渲染 | 标题、粗斜体、删除线、列表、任务列表、引用、表格、代码块、分隔线、链接、图片 |
| 图片预览模式 | 点击笔记里的图片或目录树中的图片文件,在同一个预览面板显示(不新开窗口);切换/编辑笔记时自动退出 |
| 本地图片内联 | 相对路径 / 绝对路径 / file:// 本地图自动转 base64 data URL;带缓存(按路径 + mtime + 大小),单图上限 5MB |
| 图片显示方式 | 适应宽度(20–100% 可调)或隐藏(退化为纯文本链接) |
| 代码块高亮 | syntect 默认语法全集,按语言名或扩展名查找,找不到回退纯文本;结果带缓存(256 条上限) |
| 高亮主题 | 可从 syntect 全部主题中选择;默认「跟随主题」,随明暗自动切换深浅 |
| 滚动同步 | 编辑区与预览双向同步,逐帧轮询,带回显吸收防死循环;可在设置里关闭;图片模式不参与 |
| 版面密度 | 紧凑 / 适中 / 宽松三档(影响留白、段间距、标题缩放、正文最大宽度),也可单独设最大宽度 |
| 链接点击 | 应用内打开(尝试作为笔记打开)或交给系统浏览器,可配置 |
| 防御性预处理 | 连续分隔线之间自动补空行,规避底层 Markdown 解析器 panic 导致应用退出 |
| 功能 | 说明 |
|---|---|
| 可打开的类型 | .json、.yaml、.yml、.toml |
| 编辑区语法高亮 | 按语言高亮(json / yaml / toml) |
| 预览显示格式化结果 | 预览区展示 pretty-print 之后的内容,并带代码块高亮 |
| 格式校验 | 实时校验;非法内容给出行 / 列 + 原因,编辑区打波浪线标记,预览顶部也有红字提示 |
| 错误不阻断保存 | 校验错误不会阻止你保存(xink 是编辑器,不是 IDE) |
| 缩进宽度 | JSON 缩进 1–8 空格可配置 |
| 大文件保护 | 超过体积上限(默认 1MB,可配置,0 表示不限制)就跳过格式化、直接显示原文 |
| 格式化只进预览 | 程序绝不改写你的文件。YAML 往返会丢失注释,所以格式化结果只用于预览,写盘永远是你输入的原文 |
| 功能 | 说明 |
|---|---|
| 三栏布局 | 左文件树 / 中编辑 / 右预览,基于可拖拽、可停靠的 DockArea;默认左 260px、右 420px |
| 布局持久化 | 布局写入 layout.json(带版本号),结构变更时自动重建默认布局 |
| 主题 | 跟随系统 / 浅色 / 深色;跟随系统时随系统外观变化实时切换 |
| 字体与图标 | 目录树、编辑区、预览区各自可设字体族 / 字号 / 行高 / 字重;目录树图标大小 12–48px |
独立设置窗口(单例),三个分类八个分组,改动即时生效并落盘:
| 分类 | 分组 | 主要设置项 |
|---|---|---|
| 基本 | 外观 | 主题、界面语言 |
| 基本 | 目录树 | 图标大小、字体、是否显示全部文件 |
| 编辑器 | 编辑行为 | 自动保存开关与延迟、缩进、行号、拼写检查 |
| 编辑器 | 字体与排版 | 字体族 / 字号 / 行高 / 字重、配色 |
| 编辑器 | 数据文件 | JSON 缩进宽度、格式化体积上限 |
| 预览 | 渲染 | 代码高亮主题、图片显示方式与宽度、链接打开方式 |
| 预览 | 排版 | 字体、版面密度、正文最大宽度 |
| 预览 | 同步 | 编辑区与预览滚动同步 |
附加能力:设置项搜索(输入即过滤、回车跳首个命中)、单项重置为默认、配置导入 / 导出 / 恢复全部默认; 对已配置但尚未生效的项会标注「⚠ 暂未生效」。
已知尚未生效的配置项:界面语言(文案未本地化)、拼写检查(引擎未接入)、 编辑区配色(跟随主题,未独立适配)。
应用状态统一放在应用数据目录(由 dirs::data_dir() 决定):
| 平台 | 路径 |
|---|---|
| macOS | ~/Library/Application Support/xink/ |
| Linux | ~/.local/share/xink/ |
| Windows | %APPDATA%\xink\ |
├── config.json # 用户配置(基本 / 编辑器 / 预览)
├── layout.json # Dock 布局(面板位置与大小)
├── open_notes.json # 已打开的标签(顺序 + 当前活动标签)
├── vaults.json # 多库注册表与当前选择
├── vault.json # 旧版单库记录(仅作迁移来源)
└── xink.pid # 单实例锁
- 笔记正文不在这里,始终是库目录里普通的
.md文件;这里都是可重建的派生状态。 - 删除整个目录只会丢失偏好与界面状态,不会丢失任何笔记。
- 文件缺失或 JSON 损坏一律回落默认值继续启动,不会崩在启动阶段。
- 旧版本配置文件缺字段自动补齐;已移除的旧枚举值有兜底,不会导致整个配置解析失败。
- 单实例保护:PID 文件 + 进程探测,重复启动直接退出(避免多个渲染窗口与监听线程)。
- 外部变更监听:
notify递归监听 + 无界通道,区分"创建 / 删除 / 修改"三类事件。
| 快捷键 | 作用 |
|---|---|
Cmd+O |
打开文件夹作为库 |
Cmd+N |
新建笔记 |
Cmd+S |
立即保存当前笔记 |
Ctrl+W |
关闭当前笔记标签 |
F2 |
重命名(焦点在文件树时生效) |
Cmd+, |
打开设置 |
「新建文件夹」「删除」目前只能通过文件树工具栏按钮或右键菜单触发,未绑定键位。
以下功能目前没有实现,避免误以为可用:
| 功能 | 状态 |
|---|---|
双链 [[笔记名]] |
未实现 |
| 反向链接面板 | 未实现 |
标签 #tag |
未实现 |
| 全文搜索(跨笔记) | 未实现(设置窗口内的"搜索设置项"不算) |
| 大纲 / TOC 面板 | 未实现 |
命令面板(Cmd+P) |
未实现 |
| 视频播放 | 已尝试并放弃:Spike 阶段解码 / 渲染 / 音频 / 音画同步全部验证通过,但接进应用后画面始终出不来,代码与依赖已清理。完整复盘在 docs/video-playback-design.md(该文件位于 spike/video-playback 分支,未合入 main) |
macOS(GPUI 使用 Metal):安装 Xcode 命令行工具即可。
xcode-select --install已验证:仅装 Command Line Tools 也能编译并正常运行(Metal 渲染正常)。 若要做打包签名,还需要
iconutil/sips/hdiutil(由命令行工具提供)以及pkgbuild。
cargo run # 运行
cargo test # 运行全部测试(含 core 与 ui 的单元测试)
cargo clippy --all-targets
cargo fmt首次编译会拉取并构建 blade-graphics / tree-sitter / resvg 等重依赖,耗时较长,属正常现象。
打包由 scripts/build_macos.sh 完成:编译 → 生成 .icns 图标 → 组装 .app → 签名 → 打包 .dmg / .pkg。
./scripts/build_macos.sh # 编译 release,产出 .app + .dmg(最常用)
./scripts/build_macos.sh --pkg # 额外产出 .pkg 安装器
./scripts/build_macos.sh --no-dmg # 只产出 .app
./scripts/build_macos.sh --skip-build # 跳过编译,用已有 target 产物重新打包
./scripts/build_macos.sh -h # 查看用法产物统一输出到 build/(版本号取自 Cargo.toml,无需另外维护):
| 产物 | 说明 |
|---|---|
build/xink.app |
应用 Bundle |
build/xink-<version>.dmg |
拖拽安装盘(默认产出) |
build/xink-<version>.pkg |
安装器(加 --pkg) |
可用环境变量覆盖默认值:
PROFILE=debug ./scripts/build_macos.sh --skip-build # 用 debug 产物试跑打包流程
ICON_SRC=resources/other.png ./scripts/build_macos.sh # 指定其它图标源
VERSION=0.2.0 ./scripts/build_macos.sh # 覆盖版本号| 变量 | 默认值 |
|---|---|
APP_NAME |
xink |
BUNDLE_ID |
com.helight.xink |
VERSION |
取自 Cargo.toml |
ICON_SRC |
resources/AppIcon.png |
PROFILE |
release |
MIN_MACOS |
11.0 |
BUILD_DIR |
./build |
注意事项:
- 图标由
sips生成多尺寸iconset再转.icns。 - 脚本会校验图标资源确实内嵌进二进制(在产物里检索 SVG),未内嵌则直接失败—— 否则打出的包在本机正常、拷到别的机器就图标全丢。
- 签名默认是 ad-hoc(
codesign --sign -),仅保证本地可运行; 分发前需用 Developer ID 重新签名(脚本结束时会打印对应命令)。
src/
├── main.rs 启动入口:单实例检查、快捷键绑定、面板重建工厂
├── app.rs 全局状态 AppState、DockArea 布局、主题、配置更新入口
├── actions.rs 快捷键动作定义
├── core/ 领域层(纯业务,不含 UI,可独立测试)
│ ├── vault.rs 库扫描、增删改移、多库注册表
│ ├── note.rs 笔记加载/保存、原子写盘、外部修改检测
│ ├── markdown.rs 预览预处理(图片内联、防御性 sanitize)
│ ├── datafile.rs JSON / YAML / TOML 解析、格式化、校验
│ ├── media.rs 文件类型判定(唯一判据来源)
│ ├── highlight.rs 代码块语法高亮(syntect)
│ ├── config.rs 配置项定义、区间校正、持久化
│ ├── watcher.rs 外部变更监听
│ ├── session.rs 标签去重与恢复规则
│ └── fs.rs 文件复制/移动/删除/导入
├── ui/ 视图层
│ ├── file_tree.rs 文件树面板
│ ├── editor.rs 编辑面板
│ ├── preview.rs 预览面板
│ ├── settings.rs 设置窗口
│ ├── tabs.rs 标签注册与"打开笔记"唯一入口
│ └── vault_manager.rs 库管理窗口
└── state/ 持久化
├── workspace.rs 配置 / 布局 / 标签 / 库列表的读写
└── single_instance.rs 单实例锁
测试代码放在仓库根的 tests/,目录结构镜像 src/(如 src/core/datafile.rs 的测试在
tests/core/datafile.rs,由源文件末尾的 #[path] 挂载)。详见 docs/TESTS.md。
分层铁律:ui → core 单向依赖,core/ 不得出现任何 UI 组件类型或布局样式。
| 文档 | 内容 |
|---|---|
docs/REQUIREMENTS.md |
需求规格(R1–R13) |
docs/DESIGN.md |
技术设计 |
docs/TESTS.md |
测试代码的组织方式与命令 |
docs/json-yaml-editing-requirements.md |
数据文件编辑需求(R13) |
docs/json-yaml-editing-design.md |
数据文件编辑设计 |
docs/multi-vault-design.md |
多库机制设计 |
docs/preview-layout-design.md |
预览版面设计 |
.codebuddy/rules/ |
开发铁律(开工前必读,含会导致编译失败的硬约束) |
项目级 AI 开发规则与技能位于 .codebuddy/:
.codebuddy/rules/ 开发铁律(技术栈、分层架构、编码规范、领域规则、先设计后开发)
.codebuddy/skills/ 开发技能(GPUI 桌面开发、本应用开发 SOP)
开工前请先阅读 .codebuddy/rules/,其中有会导致编译失败或架构腐化的硬约束。
MIT(见 Cargo.toml 的 license 字段)。
