ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

手写极简命令行任务管理工具:Caveman设计与实现解析

手写极简命令行任务管理工具:Caveman设计与实现解析 最近半年我几乎把所有效率类软件都卸干净了——不是矫情是真的被“工具肥胖症”折磨够了。Caveman 就是我为了解决这个问题亲手写的命令行任务管理小工具只有一个二进制、一堆 JSON 文件、八九条命令却撑起了我每天的任务记录、番茄钟专注和晚间复盘。名字起得很直白它就是要像原始人手里的石斧一样只有一块石头一根棍子但能干活、能打猎、能活下来。这篇文章我会把它的设计思路、核心实现、踩过的坑全部拆开讲如果你也想做一个属于自己的极简工具可以直接照着抄。1. 项目整体设计与思路拆解1.1 为什么做一个“原始人”级别的工具先交代一下背景。我先后用过 Notion、Todoist、滴答清单、Things来回搬了至少三次家。每次换工具都要重新设计标签体系、整理分组结构、处理多端同步冲突。等我把工具收拾利索写任务清单的那个冲动早过去了。更崩溃的是这些软件里有一半的功能我根本没用过却要为它们承担学习成本和订阅费用。后来我干脆退回文本文件记待办撑了两个月发现比任何 App 都顺手。Caveman 就是在那个状态下诞生的记录、执行、复盘三个动作一个命令。Caveman 这个名字有两层意思。第一层是“简单到像原始人”它不提供日历视图、不搞云同步、不做自然语言解析所有功能加起来只有 8 个动词第二层是“帮你在电脑前找回专注”就像原始人一次只追一头猎物你通过番茄钟一次只做一件事。它解决的核心问题非常具体减少“选择工具”本身带来的认知负担让你从打开终端到开始干活耗时不超过 10 秒。这个工具适合三类人每天泡在终端里的开发者想要极简生活却总被复杂工具劝退的人以及所有对“配置半小时、干活五分钟”深恶痛绝的人。如果你完全不用命令行这篇文章里的取舍思路同样值得看——做减法这件事和用不用终端无关。1.2 技术选型从语言到存储到依赖我做个人工具有个习惯动手前先问“这玩意儿我要维护几年”。Caveman 定位是“自己天天用、不想折腾”的小工具所以选型标准只有三条单文件可执行、零运行时依赖、数据文件人眼能读能改。语言选了 Go原因很直接go build 出来就是一个静态二进制扔到 /usr/local/bin 就能跑不需要装 Python 环境不怕系统升级把依赖弄坏。启动速度也是刚需命令行工具只要超过 50 毫秒体感就发粘。你如果更熟悉 Python用标准库写同样逻辑完全没问题后面所有设计都是语言无关的。存储方案是 JSON 单文件加 JSONL 追加日志没有碰 SQLite。任务数据量撑死几千条SQLite 的索引、事务、并发控制属于大炮打蚊子。JSON 文件的好处是随时能打开看出了 bug 一眼定位备份就是拷走一个文件。缺点是不能并发写但命令行工具本来就是单用户单进程注意原子写入就够用。这个选择背后有个很朴素的逻辑小工具的数据库应该是“你能用手摸着的数据”而不是一个需要命令行进出的黑盒。依赖方面我压到极致时间处理用标准库颜色输出自己拼 ANSI 转义序列系统通知做成可选模块检测到 notify-send 或 osascript 才启用没有就静默写日志。核心原则是通知这种锦上添花的功能绝不能成为主流程的绊脚石。1.3 功能边界的确定砍掉哪些“理所当然”做工具最难的不是加功能是砍功能。Caveman 第一版砍掉的清单比保留的长得多。没有日历视图、没有标签筛选器、没有团队共享、没有自然语言解析、没有云同步、没有手机端。每次砍功能前我都问自己一个问题如果今天没有这个功能我会不会活不下去答案全是“不会”。日历视图的本质是把信息换成视觉呈现但终端里一条时间线就能覆盖 80% 的回顾需求云同步解决的是多设备一致性问题而我的真实场景里“公司写完、回家还想看”一周发生不了一次真需要就用 git 仓库同步一个 JSON 文件十分钟搞定自然语言解析“12月5日下午3点前提交周报”看着很酷可它本质上只是比“add 提交周报 --due 周五”少敲几个字却要引入一整套解析器性价比太低。最终留下来的命令只有 8 个add、done、rm、ls、do、log、stat、edit。每个动词对应一个动作没有子命令嵌套没有 flags 风暴。这个减法过程本身就是项目的核心产出之一。你在设计任何工具时第一步永远是画一条“功能红线”线内的做到极致线外的一律不碰。2. 核心细节解析与实操要点2.1 命令设计每个动作就是一个动词Caveman 的交互哲学是“所见即所动”想记事输入 add想专注输入 do。命令统一采用“动词 参数”的扁平结构拒绝子命令嵌套。我见过太多工具把 add 藏在 create 里、把 list 藏在 view 里用户得先记忆一棵命令树才能开始干活这是典型的认知税。典型会话长这样# 记三件事 caveman add 写周报 --tag work --due 周五 caveman add 修数据库慢查询 --tag work,dev caveman add 买猫粮 --tag life # 看今天要做什么 caveman ls # 开始 25 分钟专注默认取第一个未完成任务 caveman do 1 # 完成一件事 caveman done 1 # 晚上复盘 caveman log设计时有几个细节很关键。第一add 成功后会直接打印新建任务的 ID让“刚建完就 do”的链路顺畅无阻第二所有 flag 都有合理默认值do 不带参数自动选最旧的未完成任务due 不填默认当天第三输出刻意不玩花活——颜色只用来区分状态和 ID不用来装饰标题保证输出重定向到文件或管道后依然干干净净。2.2 事件日志一切皆追加状态可重建Caveman 的数据模型分两层。任务表保存当前状态事件日志保存历史事实。任务表里的每条记录有 ID、标题、标签、创建时间、完成时间事件日志用 JSONL 格式追加每发生一个动作就写一行{ts:2025-03-16T09:00:0008:00,type:focus_start,task_id:1} {ts:2025-03-16T09:25:0008:00,type:focus_end,task_id:1,finished:false} {ts:2025-03-16T09:25:0008:00,type:break_start,len_min:5}这种“事件溯源”风格对个人工具有三点好处。第一日志天然不可变复盘和统计始终有原始数据可查第二就算任务表意外损坏从日志也能重建绝大部分状态第三将来想换数据结构、想导出给别的工具日志就是现成的数据真相。代价是统计要扫描日志但个人数据量下成本几乎可以忽略。命令和日志的对应关系是add 追加 createddone 追加 completeddo 追加 focus_start番茄钟结束追加 focus_end 或 break_start。每天结束时 log 按时间排序输出时间线stat 按天聚合算出“今天专注几次、总共多少分钟”。这套设计的精髓在于任务表只是“视图”日志才是“事实”。2.3 番茄钟状态机用时间戳不靠倒计时番茄钟是 Caveman 的核心模块也是最容易写歪的地方。我第一版用的是 sleep 倒计时结果发现两个致命问题终端窗口一关进程死了倒计时就丢了电脑休眠半小时定时器漂移醒来界面还停在“还剩 23 分钟”。所以后来我彻底改了思路只记录“开始时刻”不记录“还剩多少”。状态机维护一个 state.json里面存当前相位idle、focus、short_break、long_break和上次变更的时间戳。任何命令进来先算 elapsed now - last_ts再决定是否需要推进状态idle ──do──▶ focus ──25min──▶ short_break ──5min──▶ focus │ ▼ 第4轮后 long_break核心代码逻辑如下func cmdDo(ws *Workspace, taskID int) { st : ws.LoadState() now : time.Now() switch st.Phase { case focus: elapsed : now.Sub(st.Ts) if elapsed 25*time.Minute { ws.Log(focus_end, st.TaskID, false) st.Phase short_break // 第4轮后切 long_break st.Ts now } else { fmt.Printf(专注剩余 %d 分钟\n, 25-int(elapsed.Minutes())) } case idle: st.TaskID taskID st.Phase focus st.Ts now ws.Log(focus_start, taskID, true) } ws.SaveState(st) }这套“状态校正”逻辑只要命令被调用就会执行一次。它解释了为什么 Caveman 不需要后台驻留进程——它只是一个每次运行几毫秒、算一算“当前该处于什么状态”的小程序。这个方案也适合任何“定时提醒但允许中断”的场景比如喝水提醒、定时发邮件都可以套用。2.4 输出与交互克制是美德CLI 工具的输出本质是一种接口设计。Caveman 的输出规则只有三条不用表格库、不用动画、不画花哨边框。ls 的默认输出就是平铺列表ID STATUS TITLE TAGS DUE 1 pending 写周报 work 周五 2 pending 修数据库慢查询 work,dev - 3 running 买猫粮 life - (25:00)唯一称得上“花活”的是 running 状态后面的实时倒计时它会在每次 do 运行时刷新。颜色只在标准输出是 TTY 时开启并支持 NO_COLOR 环境变量直接关闭。错误提示我也下了功夫不报“操作失败”这种废话而是报“找不到 ID 为 3 的任务当前最大 ID 是 5”让用户一眼知道下一步该干什么。这种“错误信息即导航”的思路比任何错误码都管用。3. 实操过程与核心环节实现3.1 环境准备与项目初始化写这个项目只需要一个 Go 环境。我用的是 Go 1.23项目目录保持扁平方便一眼看全caveman/ ├── go.mod ├── main.go # 命令入口子命令路由 ├── cmd_add.go # add / done / rm ├── cmd_do.go # 番茄钟状态机 ├── cmd_log.go # 日志与统计 ├── store.go # JSON 读写、原子写入、备份 └── tasks.go # 任务模型与操作初始化只需两步go mod init caveman然后逐文件填充。main.go 只做一件事——把 os.Args[1] 路由到对应函数未知命令直接打印用法并返回退出码 1。我故意不引入 cobra 这类 CLI 框架因为 8 个命令不值得为了几个 flag 解析背上五六个依赖包。标准库 flag 加一层 switch 完全够用而且所有行为都在自己掌控里出了问题不用去翻框架源码。3.2 数据层原子写入与自动备份store.go 是 Caveman 的“数据库层”包含三块加载、保存、备份。加载用 json.Unmarshal 读 data.json解析失败时尝试恢复最近的 .bak 文件并打印警告保存用“先写临时文件、再 rename 覆盖”的原子方式每次启动时把现有 data.json 复制为 data.json.bak保留最近两个副本。func Save(w *Workspace) error { tmp : w.Dir /data.json.tmp f, err : os.Create(tmp) // 写入内容... return os.Rename(tmp, w.Dir/data.json) }提示临时文件必须和目标文件在同一个目录下rename 才能保证原子性。跨目录 rename 在某些文件系统上会退化成 copy delete失去“写到一半断电也不损坏”的保护意义。任务模型长这样type Task struct { ID int json:id Title string json:title Tags []string json:tags,omitempty Due string json:due,omitempty CreatedAt time.Time json:created_at DoneAt *time.Time json:done_at,omitempty }ID 分配采用 max1 自增删除任务后不复用。为什么坚决不复用因为日志里存的是 task_id如果删除后同一个 ID 又给了新任务日志统计就会串味。个人工具的数据一致性很多时候不是靠数据库约束而是靠这种“不给自己挖坑”的编码约定。3.3 番茄钟状态机的完整实现番茄钟涉及三个命令do开始或恢复、pause暂停、abort放弃。核心逻辑我前面已经贴了框架这里补一些实操细节。首先是长休息的切换逻辑统计 state.json 里本轮连续完成的专注次数每满 4 次focus_end 后的休息相位就切到 long_break否则是 short_break。这个计数也在命令每次被调用时重新计算进程杀掉也不影响。其次是结束提醒。focus_end 发生时代码先尝试系统通知func Notify(msg string) { if _, err : exec.LookPath(notify-send); err nil { exec.Command(notify-send, caveman, msg).Start() } else if _, err : exec.LookPath(osascript); err nil { exec.Command(osascript, -e, display notification msg with title caveman).Start() } // 都没有就静默反正日志里已经记了 }这里的原则是“尽力而为”通知发不出去没关系日志永远在。我还把 caveman status 的输出接进了 zsh 的 RPROMPT右侧提示符每次回车都能看到“专注中剩余 18 分钟”或“休息中”。这是我最推荐的集成方式——番茄钟的提醒感不靠弹窗靠无处不在的轻量可视化。3.4 日志与统计用标准工具也能查log 命令读 JSONL 日志按时间排序输出stat 命令做聚合。聚合逻辑不复杂遍历日志focus_start 遇到对应的 focus_end 才算一次完整专注时长等于两者时间戳之差被 abort 的不计入完成次数单独列为“中断”。输出按日聚合2025-03-16 专注 3 次共 75 分钟中断 1 次 2025-03-17 专注 5 次共 125 分钟中断 0 次这里有个彩蛋因为日志就是 JSONL你完全可以用标准 shell 工具直接分析不必等 stat 支持所有场景。比如查今天所有专注开始时间grep type:focus_start ~/.caveman/journal.jsonl | tail -5当初选择“人眼可读的纯文本日志”带来的红利就在这里你不需要为每个统计需求改代码文件就在那儿jq、awk、grep 随便折腾。工具的尽头是文本这不是一句玩笑。3.5 安装、别名与日常集成构建安装一条命令搞定go build -o caveman . sudo install caveman /usr/local/bin/日常使用我给它配了短别名。在 .zshrc 里加几行alias cmcaveman alias cmlcaveman log --today alias cmscaveman stat每天开工前我的流程固定是三步caveman add 把当天所有杂事丢进去caveman ls 挑出三个优先级最高的然后 caveman do 开始第一个番茄钟。这套流程跑了两三个月最直接的改变不是“专注时长”变长了而是我对“今天到底干了什么”有了清晰的交代——每天晚上 caveman log 打出来时间线就摆在那儿哪里浪费了自己心里有数。4. 常见问题与排查技巧实录工具用了几个月踩过的坑不少整理成速查表全是真实发生过的问题现象根本原因解决办法番茄钟结束没有系统通知系统没装 notify-send / osascript安装 libnotify-bin或改用 shell 提示符显示状态电脑休眠后倒计时“穿越”第一版用 sleep 累加计时改时间戳计算命令进入时做状态校正data.json 打不开JSON 报错写入时断电或进程被杀原子写入 启动前校验 .bak 自动回滚统计数字对不上删除任务后 ID 复用日志串味ID 永不复用删除只标记状态输出重定向后满是乱码颜色ANSI 在非 TTY 环境未关闭检测 isatty支持 NO_COLOR 环境变量中文标题在终端里对不齐中英混排宽度计算复杂果断放弃对齐用空格分隔即可读4.1 系统休眠导致的状态漂移这个问题发生在第一版用 sleep 计数器的时候午休合上笔记本下午打开发现番茄钟还显示“还剩 18 分钟”实际已经过去两小时。改成时间戳方案后彻底解决核心就一句话只记“开始时刻”不记“还剩多少”。这个经验我后来用到了很多地方——任何需要“定时但允许中断”的功能都应该记录起点而不是倒计时。4.2 数据损坏与自愈机制有一次我 cat data.json 发现少了个右括号才知道写文件时被另一个终端里的进程打断了。修复方案分三层Save 里严格走临时文件加 rename每次启动先做 json.Valid 校验非法就复制 .bak 覆盖并打印警告每周用 cron 把整个 ~/.caveman 目录打包到本地 NAS。一个 JSON 文件承载了我一个月的工作痕迹再怎么小心都不过分。4.3 跨平台与终端环境的坑macOS 上测试一切正常换到 Windows 的 cmd 里发现颜色全是乱码原因是老式终端不认 ANSI 转义序列。我的处理是isatty 检测只区分 TTY 和非 TTY不区分系统Windows 用户直接推荐用 WSL 跑体验和 Linux 一致。另一个经验是时间存储必须带时区偏移RFC3339否则用 UTC 存、本机是 08:00跨天统计会晚 8 个小时账目全错。这两个坑都很基础但值得写进任何 CLI 工具的开发笔记里。5. 实战心得与可以继续扩展的方向用了 Caveman 两个多月我最深的一个体会是工具的价值不在功能多在“你用它的频率”。以前装的那些效率软件打开频率越来越低最后沦为文件夹里的图标Caveman 因为足够轻反而成了我每天打开终端后第一个敲的命令。它不美化数据、不生成花哨图表就是把时间诚实地摆在那里让你自己判断哪里值得、哪里浪费了。我也在琢磨几个扩展方向但都遵循“先难受再动手”的原则。比如想做 shell 自动补全因为每次敲 --tag 都要翻帮助想做只读 web 报表因为每周总结时想用浏览器看数据想加提醒音效因为番茄钟结束全靠眼神瞥到 RPROMPT。这些需求都是真实用出来的不是拍脑袋想出来的。最后再分享一个对我很有用的习惯每天开工前只挑三件事进“今日专注额度”其余事情不是不重要而是不配占据今天的深度工作时间。Caveman 只是把这套流程压缩成了三行命令。你要是也想做个类似的小工具我的建议是先把最常用的三个动作写死用两周把难受的点全记下来第二版再改。工具这东西永远是“用出来的”不是“设计出来的”。
返回列表