1. 为什么会有 t3code:一个终端重度用户的折腾记录
1.1 痛点:代码片段的管理方式一直在“凑合”
我其实挺早就有这个需求:每天写脚本、调配置、查 API 用法的时候,总是在各种工具之间来回切换。今天要用一段解析 URL 参数的代码,明天要找一个从 JSON 里取值的小函数,后天又要翻出一个 Nginx 的 rewrite 规则。这些代码,有的埋在公司项目里,有的藏在自己电脑某个深不见底的文件夹,有的干脆记在飞书/笔记软件里,时间一长连自己都懒得找。真正想用的时候,最快的办法居然还是重新敲一遍,或者打开浏览器去搜,一搜就是一堆广告和无关内容。
我试过市面上很多方案。笔记软件虽然能记,但打开要时间,还要面对密密麻麻的富文本;在线粘贴片段的服务方便是方便,可一断网就什么都看不到,而且很多代码涉及内部配置,不适合往公网放。后来我甚至专门建了一个“常用代码.md”,往里复制粘贴。结果半个月之后这个文件涨到 3000 多行,找个函数就得靠 Ctrl+F,效率比最开始还低。
于是标准就变得非常明确了:工具必须离线运行,必须能在终端里秒开,必须支持快捷键直接唤起,因为我的日常开发环境几乎都在终端和编辑器之间,没那么多耐心等一个 GUI 慢慢启动。我给自己定的三个硬指标是:启动不超过 300 毫秒、全部操作不用离开键盘、数据文件我自己能看懂能备份。这就是 t3code 最初的起点。
1.2 t3code 名字来源与设计定位
t3code 这个名字,拆开看就是 T3 和 code 的组合。T3 在我这有两层意思:第一层是 Terminal 的第三个字母 T,强调它诞生于终端;第二层是 Three-in-one,因为我在设计时强行把要管理的代码片段分成了三类——短表达式(一二行的工具代码)、函数块(一段逻辑完整的小函数)、完整配置(一份可以直接复制的配置文件)。这刚好对应了我日常最常碰到的三种形态,也决定了后面整个存储结构都围绕这三类来做。
定位上,t3code 不是要成为一个庞大的知识管理系统,它就是一个“代码速记员”。你可以把它理解成终端里的一个小抽屉,把随时冒出来的、抄下来又怕忘的代码段扔进去,等要用的时候喊一声名字它就把内容递给你。因为存储格式是完全开放的,我不用担心被某个软件绑架,也不需要服务器,所有数据都放在本机的目录里,想怎么备份就怎么备份。
这个工具适合谁呢?我觉得最合适的是像我一样的本地优先开发者:习惯命令行,不愿意为了一个“记代码”的功能打开一个重量级应用,或者经常要在脚本里批量调用片段、希望用一套可编程的方式管理代码资产的人。如果你只是偶尔需要一个收藏夹,用它也不是不行,但优势可能没那么明显。
2. 整体架构与三个核心设计决策
2.1 存储格式:一切皆文本,目录即是数据库
设计 t3code 时我做的第一个决定,就是把“数据库”这件事彻底抛开。我见过太多项目,明明只是存几百条文本记录,非得上 SQLite、非得上服务端,结果部署、升级、备份全变成负担。我需要的规模撑死了几千个片段,用一个自由格式的目录结构完全够用,而且好处是肉眼可见、路径即查找、内容可 grep。
目录结构大概是这样的:
~/.t3code/ ├── snippets/ │ ├── js/ │ │ ├── url-params.md │ │ ├── debounce.md │ │ └── ts/ │ │ └── type-guard.md │ ├── nginx/ │ │ └── rewrite-https.md │ └── shell/ │ └── find-large-files.md ├── config.json └── templates/ └── default.md每个片段就是一个 Markdown 文件,第一行是标题,紧跟着是标签行,后面才是正文代码。比如:
# 提取 URL 参数为对象 tags: js, browser, utility function parseParams(url) { const params = new URL(url).searchParams; return Object.fromEntries(params.entries()); }文件路径里的目录天然就是一级标签分类,文件里的 tags 字段是二级补充标签。这样我既能靠目录做到“一眼望过去知道有什么分类”,也能靠标签做到跨目录检索。用 Markdown 而不是 txt,是因为我想保留标题层级和代码块标记,后续如果要生成 HTML 速查手册,直接转换就行,不需要额外定义格式。
这个设计还有一个隐藏优势:我可以直接打开 bash 对 snippets 目录做任何操作。批量重命名、批量清空、复制整个分类到新电脑,全部都可以用 shell 完成。对于一个个人工具来说,这种“退可守”的方案,远比一开始就引入重型框架稳妥。
2.2 检索策略:文件名与内容两级匹配
存进去容易,找出来难。t3code 的检索核心其实不复杂,我把它设计成了两级匹配:第一级看文件名和标题,第二级看标签和正文内容。匹配的时候会计算一个粗糙的分数,最后的列表按分数排序输出。这个逻辑摊开来说就三步。
首先是分词。把用户输入的查询词按空白拆开,单个英文单词就直接用,中文输入的话不强行分词,而是用整段去匹配,因为个人片段库的规模小,没必要引入分词器。然后是打分,规则如下:
- 标题或文件名中出现查询词,每个词加 5 分;
- tags 字段中出现查询词,每个词加 3 分;
- 正文代码中出现查询词,每个词加 1 分;
- 命中的位置越靠前,额外加 0.5 分。
比如我输入js url,结果里有 10 个文件都提到过 url,但只有 1 个文件的标题就是url-params.md,那它的分数会明显靠前。列表展示的时候,默认每行显示文件名、所属分类、命中类型和一句话摘要。摘要就是正文的第一行非空内容,切到 60 个字符左右,避免列表被长行塞满。
为什么不做全文索引、不引入搜索引擎那一套?核心原因还是规模。几千个 Markdown 文件,纯 Node 用递归读取,加上简单的正则扫描,最坏耗时也就几十毫秒,根本不需要索引。真到了几万个片段,那种量级大概率需要的是整体管理的重构,而不是继续往单机工具里堆功能。与其提前优化,不如先把数据结构和检索规则定清楚,保持透明。
2.3 同步方案:交给 Git 生态,不自造轮子
我曾经认真考虑过要不要给 t3code 写一个云端同步功能,但想了一个晚上就放弃了。理由很简单:同步是计算机领域最麻烦的问题之一,涉及到冲突处理、增量同步、多端协作,做不好就是数据丢失。而个人的代码片段库,其实已经有一种非常成熟的同步方案,那就是 Git。
所以最后 t3code 只暴露了一个t3 sync命令,内部实现就是帮你把这个目录做成 Git 仓库,自动提交并推送。大概步骤是:
- 检查 snippets 目录是否有
.git,没有就执行git init; - 检查是否有远程地址,没有就提示用户先配置;
git add -A,然后提交,提交信息默认是update: YYYY-MM-DD HH:mm:ss;git push,如果有失败则保留本地提交,输错信息。
这个方案的好处太明显了:我可以拿到任意一台新电脑上,git clone一下仓库,然后把~/.t3code指过去,几秒钟就完成全部迁移。每次修改都有历史记录,如果一个文件被误改了,我可以直接git checkout找回,连专门的回收站都不用做。
当然 Git 同步也有一个很现实的问题:如果一个片段在两台电脑上同时修改,push 的时候一定会冲突。我的解决办法比较“暴力”,算是个人工具的特权:同步前自动先git pull --rebase,如果有合并冲突,就把冲突方标记成一个新文件,然后保留所有版本靠人工处理。因为我这个库的使用场景是“一个人、多台机器”,同时改同一个片段的概率极低,这种策略够用了。
3. 从零实现 t3code 的关键模块与经验
3.1 项目骨架与依赖选型
实现语言我选了 Node.js,基于两个判断:第一,我日常的终端环境里 Node 基本是必备的,不需要额外装运行时;第二,Node 的跨平台处理对 Windows、macOS、Linux 都比较友好,文件路径和子进程这块不用我自己啃系统 API。至于框架,我整个项目只有一个第三方依赖,连命令行解析都没用,直接自己写process.argv的解析,这样 t3code 在任何机器上装了 Node 就能跑,不用先npm install一大片东西。
项目结构大约是这样:
t3code/ ├── bin/ │ └── t3.js ├── src/ │ ├── store.js // 片段存取、目录扫描 │ ├── search.js // 检索打分逻辑 │ ├── format.js // 终端输出格式化 │ ├── config.js // 配置读写 │ └── commands/ │ ├── add.js │ ├── list.js │ ├── get.js │ ├── rm.js │ └── sync.js ├── package.json └── README.md入口文件bin/t3.js的顶部加上#!/usr/bin/env node,然后通过npm link命令把t3链接到全局。剩下的代码量其实不大,核心逻辑加起来不到 500 行,但每个模块的边界我是刻意切清楚的:store 层只知道文件和目录,search 层只负责查和打分,format 层只管输出。这样后续要换存储、换检索算法,不会牵一发动全身。
3.2 核心命令的实现细节
一个命令行工具最常用的命令就是增删改查,t3code 的这组命令我设计成了:
t3 add [文件路径]或t3 add -从标准输入读取内容;t3 list列出指定分类或全部片段;t3 get <关键词>检索并输出片段内容;t3 rm <关键词或文件名>删除片段;t3 sync执行 Git 提交推送。
以add为例,整个流程是这样的:先接收标题参数、标签参数、正文来源,然后检查 snippets 目录下是否已经存在同名文件,如果存在就进入“追加内容”还是“整体覆盖”的交互选择。这个交互不能让用户输入太多内容,所以默认按两行处理:标题是-t参数,标签是-g参数,正文要么读文件、要么读 stdin、要么从终端粘贴后按 Ctrl+D 结束。
写到这里我不得不提一个特别容易踩的坑:用 Node 读标准输入时,如果你不监听end事件,脚本会一直在那里挂起。我当时第一版代码犯过这个错,加了一个process.stdin.resume()却忘记在数据结束之后退出,结果所有管道方式传入的片段全部卡死。后来我的做法是,用readline模块逐行读取,然后统一在close事件里调用写入逻辑,这个方案对“文件重定向”和“手动粘贴”两种场景都稳定。
get命令的设计稍微特殊一点。它默认会进入一个“预览模式”,先显示匹配列表,再提示输入序号选中某个片段,然后才把正文完整打印出来。如果你直接给了一个精确文件名,就不需要走选中流程,直接输出。我特别加了一个--copy参数,生效时会直接把片段内容放进系统剪贴板,省得我选中一长段代码再手动复制。
3.3 模糊检索与格式化输出的实现思考
检索这块,我不想用String.includes一下就完事,因为用户往往记不住精确的片段名。比如我有一个find-large-files.md,用户可能会输find files、large file,那匹配时就要有一定的容错。我实现的思路是:把查询词拆成若干 token,然后对每个 token 做两种匹配——包含匹配和大小写折叠的包含匹配,再用indexOf的位置来调整分数。
这里的核心代码大概长这样:
function scoreSnippet(file, tokens) { let score = 0; const name = file.name.toLowerCase(); const content = file.content.toLowerCase(); const tags = file.tags.join(' ').toLowerCase(); for (const token of tokens) { const t = token.toLowerCase(); if (name.includes(t)) score += 5; if (tags.includes(t)) score += 3; if (content.includes(t)) score += 1; // 标题命中位置越靠前,额外加分 const pos = name.indexOf(t); if (pos >= 0 && pos < 3) score += 0.5; } return score; }打分规则简单粗暴,但实测下来够用。因为场景就是几千个文件,比起准确率,我更在意“别把一个完全无关的东西排到最前面”。有一次我搜索config,结果 score 最高的确实是一个名为config-file.md的片段,而不是一堆内容里提到 config 的脚本,说明标题权重的设计是有效的。
格式化输出也同样讲究。直接用console.log固然简单,但一个 200 行的配置片段的打印结果会把整个终端刷得乱七八糟。我的做法是:预览模式下只打印文件名、分类、摘要,而且摘要行有最大宽度限制,超过宽度的部分用省略号截断,这个宽度不是写死的,而是用process.stdout.columns动态获取。正文输出时,如果检测到当前终端宽度小于 80 列,就提示可以加--wrap参数进行软换行,避免横向滚动条。这些交互细节看起来小,但就是这些地方决定了工具用起来顺不顺手。
4. 日常使用工作流与配置技巧
4.1 把 t3code 接到键盘上:终端唤起配置
作为一个追求“手不离键盘”的用户,如果每次用 t3code 还要切换到终端窗口,再敲命令,那我大概率很快就会放弃。所以我的使用方式是给 t3code 配一个全局热键,在任何应用里按一下,直接弹出一个纯终端面板,输入检索词就能用。
我用的是终端模拟器自带的快捷键绑定,不同平台的配置方式略有差异。macOS 下我会在终端模拟器的设置里新建一个 Profile,把执行命令设为t3 get,再给这个 Profile 绑定一个全局快捷键,这样在任何界面按快捷键,终端窗口会直接滑出来,这时我已经把片段列表检索出来了。Windows 下的做法类似,在 Windows Terminal 里可以给某个命令配置热键,实现“新标签页直接打开 t3code 检索界面”。
一个很关键的细节是,热键打开窗口后要能自动关闭。我的 t3code 加了一个环境变量T3_ONE_SHOT,如果检测到它存在,那么执行完get并输出片段内容后,进程会自动退出,同时终端模拟器配置里要把“退出后关闭窗口”打开。这样整个交互流程就是:按热键,输入关键词,看到内容,按 Esc 关掉窗口,全程不到 5 秒。
4.2 与编辑器和自动化脚本联动
t3code 不只是给我手动查东西用的,它还承担了一部分代码模板引擎的角色。我现在在写一个函数前,经常会想“之前是不是写过类似的”,于是直接在编辑器里把光标处的单词选中,执行一个外部命令,把当前选中文本作为查询词传给 t3code,命中的片段会以插入模式填到当前文件里。
在编辑器里配置一个自定义命令其实并不复杂,以我常用的 Vim 系编辑器为例,只需要写一个很短的映射,把当前视觉选区的文本拼成 shell 命令:
vnoremap <F6> y:exec system('t3 get ' . shellescape(@@) . ' --paste-mode')<CR>这里的关键是--paste-mode,它代表不显示多余的颜色格式,只输出纯文本内容,这样插入到代码文件里就不会带一堆 ANSI 颜色码。同类的联动还有不少:在 CI 脚本里,我可以把某个配置片段 grep 出来做模板变量替换;在 dotfiles 安装脚本里,我可以先检查 t3code 里有没有最新的安装配置,再决定要不要下载完整的配置文件。
自动化上要特别注意一点:命令的退出码一定要正确。比如t3 get im-not-exist,如果没有找到任何匹配,进程必须返回非 0,否则你在 shell 脚本里用&&判断就会出大问题。这个我是踩过坑的,早期的版本不管找没找到都返回 0,结果导致一个部署脚本在真正缺配置的时候继续往下跑,最后出了个低级事故。
4.3 片段库的组织规范,这样做才不会烂掉
任何工具用久了都会变成垃圾场,关键是提前定规矩。我现在给自己定了三个组织规范,简单但有效:
第一,命名必须短且语义完整。比如debounce.md可以,debounce-function-v2-final.md绝对不行,文件名本身就是要暴露给检索系统的,越简洁越好。第二,标签只保留跨分类的信息。目录已经代表一级分类了,标签我一般只加“语言、框架、场景”这类属性,不在标签里重复目录名,否则检索时会出现同一维度权重叠加,分类反而模糊了。第三,每一个新片段默认要带上“用途一行注释”。我会在正文第一行写清楚这段代码是干什么的、适用什么版本,因为代码文件本身不可能记录这些上下文。
清洗频率也很重要。我建议每季度做一次完整 review:统计文件数量,把所有超过 3 个月没被检索命中的片段拉出来看一遍,能合并的合并,能用模板替换的就改成模板参数,用不上的直接删除。这一套流程听起来有点“洁癖”,但恰恰是因为数据规模小,我们才更应该让每一个条目都有实际价值,而不是留一堆“万一以后用得上”的僵尸代码。
5. 常见问题与排查方法实录
5.1 内容丢失、重复导入,多半是路径和交互问题
我实际使用 t3code 的时候,早期遇到频率最高的两个问题,一个是“明明 add 成功,结果 get 的时候找不到”,另一个是“同一个片段被导入了两遍”。
先说我怎么排查第一个。add 成功但找不到,最常见的原因是写入路径和你检索时的片段目录不一致。我在代码里存了一个ROOT常量,所有命令都统一从 config 里读snippetsDir,但调试脚本时如果我在某个测试用例里不小心把它写死了,就会导致数据落到了别的目录。这个问题的教训就是:任何涉及文件路径的命令,最终都要通过同一个函数来解析,不要在子命令里各自写path.join。
第二个问题,重复导入,基本是交互设计不到位。早期add命令遇到同名文件时,直接覆盖,用户根本没机会后悔。后来我改成了“同名文件存在时,先读旧文件的前几行标题,显示给用户确认是否覆盖”,并且在文件名相同但标签不同的情况下,允许把新标签合并进旧文件头部。这样重复导入的概率就低多了。
5.2 中文和特殊字符处理,简单但不简单
t3code 的内容以 Markdown 为主,但用户粘贴进来的代码可不一定规规矩矩。我遇到过几个真实问题:一是 Windows 上文件内容包含中文时,在 Node 里默认读出来是乱码,因为系统默认编码不是 UTF-8。解决方式是在读取文件时显式指定fs.readFileSync(path, 'utf8'),而不是让它走系统默认编码。二是粘贴的内容里有制表符,搜索时特别容易出问题,因为同名内容里可能又是空格又是 tab。我的处理办法是在 add 阶段就统一把制表符展开成四个空格,保证后续检索的一致性。
还有一个细节是 shell 转义。用t3 add接收-g tags参数时,用户在 bash 里敲-g "js,string"是没问题的,但如果标签里有括号、空格之类,很容易被 shell 切碎。所以我在 add 命令里允许重复使用-g参数,每个-g只传一个标签,末尾再统一join(', '),这样既避免了空格问题,又让脚本调用时可以写循环。
5.3 性能瓶颈和终端兼容性,数据变大以后怎么办
说实话,个人的片段库很难到性能瓶颈。但如果你的使用场景比较变态,比如有几千个超长配置文件,那启动扫描确实会有压力。我的解决方案是给 store 层加了一个三级缓存:第一次扫描时把全部文件名和文件大小写入缓存,后续如果目录的mtime没变化,就直接用缓存,不重新扫盘。这个优化我做了之后,t3code 的启动时间从 350ms 降到了 80ms 左右,体感差别非常明显。
终端兼容性则是个更隐蔽的问题。不同终端对 ANSI 颜色、字符宽度的支持不一样。t3code 在输出高亮时用了\x1b[32m之类的颜色码,但如果你在管道重定向场景里输出这些,最终落盘的文件就会多出一堆鬼符号。所以我的做法是:显式检测 stdout 是否是 TTY,如果不是,就自动禁用所有颜色和交互提示,只输出纯文本。判断方式很简单,process.stdout.isTTY === true时才开启高亮,这个细节不加上,工具会显得很不专业。
6. 最后聊聊 t3code 的后续迭代和个人心得
t3code 我已经连续用了几个月,最大的体会是:一个工具受欢迎不是因为它功能多,而是因为它能在最关键的时刻不打断你。在我这里,它做到了“想用的时候马上能拿到”,这个体验阈值一旦跨过,再也不想回到过去那种到处翻代码的日子。
有一些后续方向我的想法已经比较明确了。第一是把模板渲染功能做得更完整,比如在片段里保留${variable}占位符,通过t3 render <name> --key value来完成变量替换,这样很多配置模板就能真正复用了。第二是建立一个插件机制,允许在add和get之后执行用户自定义的 hook 脚本,比如自动格式化、自动加日期、自动贴 Tag。第三是增加一个轻量的 Web 预览模式,把片段库导出成一份离线 HTML 页面,偶尔想用手机翻一翻也方便,但这并不代表要变成在线服务。
如果有人也想做一个类似的工具,我会建议从最小功能开始,先把“存、查、取”闭环跑通,再想优化和扩展。存储格式选纯文本、同步交给 Git、检索规则简单粗暴,这三条是 t3code 的根基,也是我踩了很多坑之后确定下来的原则。最容易被忽略的永远是细节:退出码、编码、 TTY 检测、终端宽度,这些做好之后,工具才不会在关键时刻掉链子。
最后分享一个小技巧:我的 t3code 会定期统计所有片段的检索命中次数,然后把命中最高的若干个片段打印成一个hot.md,放在首页。这相当于一个“高频代码清单”,我隔一段时间看一遍,发现某些代码频繁检索,就说明它应该被封装成一个本地函数或者公用的配置模板,而不是每次靠检索去调。工具用久了,它会反过来帮你审视自己的效率死角,这种感觉还挺有意思的。