Tinycast Notes 功能深度解析:纯本地 Markdown 笔记编辑器与格式化原理
桌面应用【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址https://gitcode.com/GitHub_Trending/ti/tinycast点击查看免费下载Notes 是 Tinycast一款原生 macOS 启动器、热键与剪贴板历史工具内置的无限量本地纯 Markdown 笔记模块它以持久浮动窗口承载一个常驻编辑器并在编辑器中就地渲染 Markdown。本文以 docs/features/notes.md 为骨架结合Tinycast/Features/Notes/下的模型、服务与 UI 源码及Tests/下的测试用例系统讲解 Notes 的存储模型、命名规则、Markdown 解析与就地渲染机制、Markdown 感知编辑、快捷键体系、自动保存策略以及验证方式。读完本文你将理解一个以文件系统为唯一数据源的轻量 Markdown 编辑器的完整实现思路并掌握如何在 Tinycast 中启用、使用与调优 Notes。设计不变量Notes 的十条核心规则Notes 的功能定义可以用一组不变量invariants来精确描述它们是整个模块的行为契约一个普通、非隐藏的.md文件就是一条笔记。文件名去掉扩展名即为标题源文件中没有 frontmatter、内嵌 ID 或标题字段也没有数据库或 sidecar 文件。未命名的笔记用其首行代替标题展示。只有create声称占用的名字Untitled、Untitled 2…才会触发该规则它纯粹是展示层行为一旦用户重命名就被真实名称取代。存储即数据源。NSTextView中的字符串、NotesStore、搜索和磁盘文件四者内容一致渲染只是在那个字符串上叠加属性与绘制绝不存在第二份字符串或偏移映射表。光标所在行始终显示原始 Markdown。选区覆盖的每一行都显示源码编辑器失焦时不揭示任何内容。样式化永不进入撤销栈。属性写入绕过shouldChangeText任何 Markdown 手势产生的编辑都经由NoteTextView.performEdit。关闭渲染 Markdown后就是字面编辑器原生 Return、Tab 与快捷键没有任何解析。格式栏只是另一种按快捷键的方式。每个按钮通过NoteTextView.format发送其对应快捷键的NoteEditAction因此拥有该快捷键同样的门槛gate、撤销步与自动保存按钮的亮起状态与其 toggle 是否会移除该格式完全一致。只有活动笔记可以是脏的。切换、创建、重命名、删除之前都会先 flush因此集合导航不可能遗弃内存中的草稿。Tinycast 是唯一写入者。没有文件监视器、没有版本检查保存就是用编辑器内容整体覆盖文件。每次显示窗口都会重新列举文件夹因此在外部新增的笔记会出现但活动草稿永远不会被从磁盘重新读取。搜索按需进行、不建索引。空查询只读元数据加每条未命名笔记的文件头非空查询在后台线程顺序读取正文且不保留与集合规模相当的源码缓存。关闭即无入口、无任何 Notes 工作。该功能默认关闭关闭时快捷键 no-op、命令不存在且仅启用本身不会创建或枚举 Notes 目录。集合可以为空。允许删除最后一条笔记且不会创建替代品窗口显示空状态Create Note 依然可用。窗口大小由用户掌控。AppKit 负责缩放并自动保存 frame控制器只将其钳制在标题栏部件不会互相碰撞的最小尺寸之上。编辑器是 Snippets 唯一展开目标。NoteTextView遵循InjectableTextView因此键入的关键词——以及 Snippets 浏览器的 ↵——直接写入文本存储而不是作为事件投递到当前最前端的任意应用。Tinycast 中没有其他组件遵循该协议详见 snippets.md。存储与身份一个目录、一条笔记、一个 IDNotes 的每通道目录固定为~/Library/Application Support/bundle-id/Notes/NoteID就是相对文件名因此重命名会产生新的身份没有针对单条笔记的启动器条目、热键、收藏或可见性设置会持有旧 ID。目录列举的规则由 NotesRepository.swift 实现只取直接的、非隐藏的、非符号链接的常规.md文件pathExtension.caseInsensitiveCompare(md)并请求.isRegularFileKey、.isHiddenKey、.isSymbolicLinkKey等资源键排序先按修改时间倒序再按本地化标题升序summaryPrecedes某个条目不可读会被跳过——一个坏文件不能拖垮整个列表。唯一命名unique-name claimingcreate使用Untitled.md随后Untitled 2.md、Untitled 3.md…重命名按同样规则认领空闲名字。importNotes备份恢复入口也走同一套逻辑因此从备份恢复的笔记会落到与它同名笔记的旁边而不是覆盖其上见 NotesRepository.swift。与其他笔记的冲突判定对大小写和变音符号不敏感folding(options: [.caseInsensitive, .diacriticInsensitive])所以plán与Plan并存时后者会成为plán 2.md。笔记永远不会与自身冲突只有精确文件名匹配才视为无操作——这正是仅改大小写或重音的重命名得以成功的原因。活动文件名是保存在UserDefaults中的本地 UI 状态不随设置备份迁移。所有文件 URL 都经过validatedFileURL校验标准化路径、解析符号链接、确认父目录为注入的 Notes 目录、扩展名为.md越界即抛invalidLocation错误标题则经validatedTitle清洗去空白、去尾部.md、禁止空串/./../.前缀///\0。派生标题未命名笔记如何展示仍携带create认领名称Untitled、Untitled 2…的笔记展示其源码中第一条含有可见文本的行。规则由 NoteTitle.swift 拥有空行、分隔线rule和围栏行fence跳过无论渲染开关与否块级与行内 Markdown 标记一律剥除借助NoteMarkdownParser的行内标记范围逐段拼接可见文本行内容截断到 120 字符displayLimit保证任何行或标题栏都不必承载一段长文本扫描范围限制在源码前 1024 字符scanLimit。NoteSummary.title仍为文件名displayTitle才是所有界面渲染的标题——切换器行及其 VoiceOver 标签、移入废纸篓的确认框、窗口标题、NoteSearch的标题带——因此模糊搜索可以命中一条从未被命名的笔记。list()对每条未命名笔记最多读取 4 KB 文件头来派生标题firstLine(of:)用FileHandle.read(upToCount:)并对截断在多字节字符中间的读取做最多 4 字节的回退见 NotesRepository.swift已命名笔记除了已付出的枚举成本外零开销。活动笔记的标题从实时草稿派生而非上次列表NotesStore.swift所以窗口标题会随着输入实时跟随首行而切换器行则在下次自动保存时赶上。重命名编辑的是文件名因此重命名字段从title起步——派生行只是名称的替身永远不是名称。NotesRepository统一拥有 list、create、load、save、rename、trash、search 读取放在Service/是因为它执行文件系统副作用NotesStore通过 detached task 驱动其阻塞工作见 NotesStore.swift。所有权与启用谁拥有什么AppCore拥有NotesStore并懒构造NotesCoordinatorNotesView只通过Environment接收 coordinator从不接触AppCore或直接修改 store见 NotesCoordinator.swift。Settings Notes 面板拥有AppSettings.notesEnabled缺省为 false。面板通过CommandCatalog列出Show Notes、Create Note、Search Notes三条命令因此即使AppIndex在禁用时省略它们也能正常渲染。这是它们唯一的归属面板SettingsTab.ownedCommands点名这三条命令使其从 Settings Commands 及Enable Commands的作用域中移出——Notes 自身的开关才是决定它们存亡的唯一开关设置面板实现见 NotesSettingsView.swift。AppCore观察启用状态并调用NotesCoordinator.applyEnabled()NotesCoordinator.swift。禁用时会隐藏面板、使待处理的展示工作失效、取消搜索、flush 草稿、移除命令若 flush 失败则保留草稿以便重试。命令、切换器与窗口三条命令的行为Show Notes切换面板——选中最后活动的笔记并显示若已可见则隐藏Create Note创建并选中一条唯一的 Untitled 笔记包括在空集合中Search Notes显示同一面板同时打开切换器并聚焦搜索框。窗口级快捷键NotesWindowController安装⌘N创建⌘P打开/重新聚焦切换器⌘O打开 Notes 文件夹Escape先关切换器再隐藏⌘W与红色交通灯按钮直接隐藏。隐藏会恢复此前的外部应用或 Tinycast 窗口并 flush但不延迟 order-out——且仅在原应用仍是前端时才恢复焦点因此用户已经离开的窗口关闭后他们停留在所移到的任何应用中。⌘Q 在整个应用范围不绑定任何功能因此任何 Notes 组合键都无法退出 Tinycast。两个窗口笔记窗口与切换器共用同一个NotesPanel——一个非激活浮动面板拥有 Escape 规则并读取 ⌘⌫见 NotesPanel.swift。两者仅差样式掩码style mask与控制器安装的commandChords笔记窗口认领 ⌘N、⌘P、⌘O、⌘W切换器把 ⌘N、⌘W 和 ⌘P 视为关闭。AppKit 绘制笔记窗口的边框。其 52 点高的标题栏容纳交通灯、居中的活动标题以及一个毛玻璃胶囊Create / Browse / Open Folder。标题是绘制的而非原生标题因此相对窗口居中它不可命中测试因此拖拽标题即移动窗口。切换器是无边框子窗口居中挂在其宿主窗口标题栏之下而非窗口内屏——笔记窗口可能只有 180pt 高而列表不该如此。它使用与PopoverMenu相同的玻璃表面240 点高度是上限而非固定尺寸列表报告自身高度窗口收缩到它且顶边钉住。切换器随宿主移动、失去 key 时像 popover 一样关闭、最后一条笔记消失时彻底关闭。空查询按最近修改列出元数据非空查询在120 毫秒防抖后搜索标题与字面正文结果上限 200 条并借助代数generation检查防止过期的搜索或选中操作发布结果。方向键与 Return 不拦截行内重命名。⌘⌫仅在切换器未处于重命名状态时把选中行移入废纸篓在编辑器与标题字段中它仍是原生文本命令。确认后Trash 从当前可见顺序中选择后继笔记。每行都向 VoiceOver 暴露激活、重命名、移入废纸篓动作。编辑器一个 TextKit 2 文本视图NoteEditorView是NSScrollView内的一个 TextKit 2NSTextView。它把NoteEditorInput.source安装为NSTextView.string从不换成显示版本——渲染就是覆盖在源码上的属性与绘制。解析GFM 子集NoteMarkdownParser.swift 以与NSString.lineRange(for:)相同的边界切分源码因此一行即一个 TextKit 段落lineBounds同时识别\n、\r/\r\n、U0085、U2028、U2029。NoteMarkdown为每行给出种类kind、内容的 UTF-16 范围、块标记与任务复选框范围、以及由缩进栈推导的列表层级。以终止符结尾的源码与空源码都会追加一条零长度行因此空末行的光标落在一条真实行上。行内 span 不存储NoteMarkdown.inlines(of:)只扫描被查询的那一行——样式器、编辑规则与NoteTitle需要的全部信息仅此而已。支持的语法标题 1–64–6 视觉上等同 3、粗体、斜体、粗斜体、删除线、行内代码、链接、裸http/httpsURL、带嵌套的圆点/数字/任务列表、引用、围栏代码块、水平分隔线。行内 span 绝不跨行。以下内容保持纯文本图片、下划线、HTML、setext 标题、缩进代码、脚注、引用链接、引用内嵌套块。GFM 表格一行管道行、一行等列数的分隔行、其后跟随管道行也保持纯文本但会被识别从而不施加行内样式它显示为代码字体换行行悬挂在首行之下在已有表格下方键入分隔行会重样式整张表。每次编辑都会重解析整篇笔记。AI 聊天中的MarkdownBlock是独立的只读解析器不共享。渲染属性 布局NoteMarkdownRenderer.swift 持有解析结果与揭示行集合并作为文本存储的 delegate。每次变更包括不投递textDidChange的 undo、redo、marked-text都通过编辑范围与长度增量报告给它多次变更可折叠为一个待处理编辑textDidChange、textViewDidChangeSelection及任何解析读取都会消费并重解析。被编辑行及其各一侧的相邻行被重样式围栏块移动时扩大到整篇笔记列表行深度变化时扩大到所有相关列表行。NoteMarkdownStyler把一行变成属性排版NoteMarkdownTypography正文上调一个系统文本样式title3标题用 largeTitle、title1、title2Interface Size 不缩放 Notes。隐藏标记用 0.01 点系统字体 透明色因此留在字符串中几乎不占宽度围栏与分隔线行以正常字体清除样式从而保留行高。列表项间距每条列表项圆点、数字或任务后追加 8 点空间渲染或揭示状态下都如此因此条目读起来像独立行且移动光标不会使其位移换行条目保持正常行距。样式写入在beginEditing/endEditing内直写NSTextStorage随后使这些行失效并重排。它从不调用shouldChangeText——这正是样式不进入撤销栈的原因。NoteRevealPolicy决定哪些行显示原始 Markdown选区下的每一行加上光标所在代码块的两条围栏。除非编辑器是 key window 中的第一响应者否则不揭示任何内容揭示的标记使用textTertiary颜色。拖拽选择期间揭示推迟到 mouse-up揭示会移动指针下的文本被揭示的列表/引用行把标记悬挂在内容缩进左侧使文本保持在渲染行的位置。由于光标行永远是原始文本光标绝不会落在隐藏文本内方向键也无需特殊处理。块绘制无私有 APINoteLayoutFragmentProvider作为文本布局管理器的 delegate其首字符携带NoteBlockDecoration的段落由NoteBlockLayoutFragment布局绘制代码带及其语言标签、引用条、分隔线、圆点、源码自带的列表序号与复选框——所有列表标记均为中性灰。纵向间距来自段落样式覆写 fragment frame 会让光标悬在字形之上。没有文本附件、叠加控件、NSTextList、NSTextTable或私有 API。编辑手势 → 编辑计划NoteMarkdownEditing.swift 把手势转换为NoteEditPlan一次替换 替换后的选区。返回 nil 表示不归我管由 AppKit 原生处理该按键。NoteTextView.performEdit见 NoteTextView.swift通过shouldChangeText→replaceCharacters→didChangeText应用计划因此每个手势恰好一个撤销步并到达自动保存。Return延续列表或引用并在空条目上退出Tab / Shift-Tab以四个空格嵌套列表项Backspace在条目内容起点先减缩进、再移除标记。这些编辑在同一个撤销步内对触及的有序列表重新编号。在段落开头键入[]或[ ]自动变为- [ ]。⌥⌘C用围栏包裹触及的行或移除光标所在块的围栏空行上则打开一个空块并把光标放入其中。⇧⌘B为每行添加当所有行都是引用时则每行移除一个选区内的空行、代码、表格与分隔线保持不动。在单行选中文本上粘贴单个http/httpsURL 会生成textpasteLink只在粘贴板内容为单个 URL 时触发否则普通粘贴。点击复选框切换[ ]/[x]且不移动光标命中测试使用NoteCheckboxGeometryfragment 绘制的矩形向外扩大 3 点并在拖拽期间显示箭头光标。点击渲染链接打开它除非点击落在标签首尾字形外侧 30% 区域——那会放置光标。只有http、https、mailto会打开被揭示的行没有链接属性因此其 URL 按文本编辑。快捷键总表快捷键作用⌘B, ⌘I, ⌘E粗体、斜体、行内代码⇧⌘X删除线⌥⌘C代码块⇧⌘B引用⌘K链接⇧⌘7, ⇧⌘8, ⇧⌘9数字列表、圆点列表、任务列表⌥⌘1, ⌥⌘2, ⌥⌘3标题 1、2、3⌥⌘0普通段落数字键按键码匹配DigitKey见 NoteTextView.swift因为不同键盘布局下带 Shift/Option 的数字字符不同。文本视图先于NotesPanel看到这些组合键且互不冲突在笔记中⌘E 取代 AppKit 的 Use Selection for Find。AppKit 仍拥有键入、选择、剪切/复制/粘贴、全选、查找、marked text、emoji、组合字符与撤销分组。复制得到原始 MarkdownVoiceOver 朗读源码。改变笔记身份或编辑器 epoch 会重装并重样式字符串、清空上一份文档的撤销历史。Snippets 通过insertText展开并按键入文本样式化。格式栏另一种按快捷键的方式当Render Markdown与Show Formatting Bar同时开启时编辑器下方的带区左端显示字符计数、右端显示格式栏。NoteFormattingBar是标题栏配方的毛玻璃胶囊初始折叠为单个圆形paintbrush按钮⌥⌘T或点击展开按钮从该按钮后方滑出——先是标题菜单、Bold、Italic、Strikethrough、Inline Code、Link再是 Code Block、Quote最后是 Numbered、Bullet、Task List。悬停显示名称与快捷键。每个按钮为 28 点见方整行可塞进最小窗口计数在行没有空间时隐藏带区从不加宽笔记。展开/折叠是窗口状态而非偏好AppCore以notesFormattingBarExpanded键读写UserDefaults并交给NotesCoordinator方式与交给 store 活动文件名相同它刻意不是AppSettings键因此设置备份不携带它。关闭 Show Formatting Bar 后连圆形按钮都不存在计数回到自己的页脚。NoteEditorView在每次安装、编辑、选区变化时上报NoteMarkdownEditing.formatting(source:selection:markdown:)NotesCoordinator只在变化时发布。该函数与 toggle 使用同一套 span 与行规则因此点亮的按钮必然可撤销。点击路径为NotesCoordinator.format→NotesWindowController.format→NoteTextView.format。按钮永不取焦点因此光标及其揭示行保持原位。标题按钮打开NoteHeadingMenuViewHeading 1–3 与 Text当前项打勾——一个永不成为 key 的无边框子窗口因此可以越过矮笔记窗口延伸同时编辑器保持光标与组合键。它在一个选择、Escape、笔记窗口内任意鼠标按下、一次编辑、笔记窗口失去 key、或隐藏时关闭。它复制 popover 菜单的行外观因为PopoverMenu依赖 palette 状态。设置项Render MarkdownAppSettings.notesRendersMarkdown缺省开启随设置备份携带。关闭即字面编辑器单一字体与颜色、原生 Return/Tab/快捷键、零解析。切换它会重样式打开的笔记但不触碰其撤销历史也不标记为脏缺省值逻辑见 AppSettings.swift。Show Formatting BarAppSettings.notesShowsFormattingBar缺省开启随设置备份携带。仅在 Render Markdown 开启时生效否则该行禁用NotesSettingsView.swift。空笔记显示对齐 16 点文本容器内边距的Start writing…占位符。字符计数直接取自NSTextStorage.length位于编辑器下的页脚或格式栏带区左端。两者都属于编辑器表面因此没有活动笔记时都不出现。自动保存300ms 防抖与保存即覆盖编辑变更立即更新主 actor 上的草稿并将保存防抖300 毫秒只保留活动源码。成功保存会刷新元数据排序切换笔记前等待同一 flush 再加载另一源码。终止时等待该 flush 后才退出应用但绝不否决退出prepareForTermination→store.flush()见 NotesCoordinator.swift。NotesStore.flush()是唯一启动写入的地方NotesStore.swift因此防抖保存与显式 flush 永远不会在同一文件上重叠它跑两轮一轮等待在途写入、一轮处理写入期间落地的编辑。保存会用编辑器内容覆盖磁盘上的任何东西。没有监视器、没有版本对比、没有冲突状态当 Tinycast 打开活动笔记时你在另一应用中编辑它下次防抖触发时该编辑即丢失。Open Notes Folder⌘O正是邀请这种行为——这是单一本地编辑器定位所接受的取舍。所有其他外部变更都会被拾取因为显示窗口会先重新列举文件夹再呈现任何内容。写入本身通过NSFileCoordinator协调save用.forReplacing、rename用.forMoving、trash用.forDeleting新建文件则走临时文件 原子写入 move的writeNewFileAtomically流程见 NotesRepository.swift。验证三层测试覆盖Tests/notes-test.swift编译随发布的 Notes 模型与服务源码使用真实模糊匹配器覆盖仓库安全、唯一命名认领、派生标题、搜索、选择、自动保存、空集合、切换器交互、取消以及 Markdown 解析器、每个编辑计划、各选区报告的格式与揭示策略。Tests/notes-editor-test.swift使用真实 TextKit 2 与 AppKit undo 对象在渲染开/关两种状态下运行原生剪切/复制/粘贴、Unicode 与 marked-text 用例覆盖撤销隔离、样式化后源码精确一致、隐藏与揭示标记、编辑与撤销后的重样式、块装饰与布局 fragment、列表键、组合键、任务规则、复选框切换、链接 scheme、URL 粘贴以及格式栏使用的格式报告与format(_:)。Tests/notes-editor-performance.swift在 10 万字符笔记上计时安装、键入与光标移动预算见 docs/testing.md。窗口 chrome 未自动化Notes 手工清单命令、快捷键、切换器、焦点恢复、Finder、废纸篓恢复、无障碍同样记录在 docs/testing.md。小结Notes 的核心设计可以用一句话概括以文件系统为唯一真相源、以 TextKit 2 为单一文本表面、以源码字符串 属性覆盖完成就地渲染。它没有数据库、没有 sidecar、没有私有 API也没有第二份字符串——所有复杂度解析、样式、编辑计划、揭示策略、块绘制都围绕让光标永远坐在真实源码上这一目标展开。这份文档与源码共同说明了在 macOS 上构建轻量 Markdown 编辑器的一条可复现路径小而完整测试充分行为契约明确。赞分享桌面应用【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址https://gitcode.com/GitHub_Trending/ti/tinycast点击查看免费下载相关推荐Tinycast Notes 使用指南纯 Markdown 文件驱动的浮动笔记编辑器Tinycast Notes 使用指南纯 Markdown 文件驱动的浮动笔记编辑器 Tinycast 的 Notes 功能是一个以“文件即笔记”为设计核心的桌面应用AI编程放在自己电脑上PI-Desktop为什么适合注重隐私的开发者AI编程放在自己电脑上PI Desktop为什么适合注重隐私的开发者 PI Desktop 是一款 本地优先的 AI 编程桌面应用 项目文件、对话历史和设置人工智能AI Agent代码智能体桌面应用插件系统Tolaria 表格笔记Sheet Notes架构与实战Markdown CSV 纯文本存储与 IronCalc 公式引擎Tolaria 表格笔记Sheet Notes架构与实战Markdown CSV 纯文本存储与 IronCalc 公式引擎 Tolaria 的表格笔记桌面应用知识管理AI 应用MCP 服务创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考