在日常开发中,我们经常需要在终端里快速记录待办事项,但切换到一个图形化的 Todo 应用又太麻烦。本实践课将带大家用 OpenCode + Rust,从零开始构建一个轻量级的命令行 Todo 管理器,数据以 JSON 格式保存在本地,支持增删改查等基本操作。
开始之前,确保你的环境满足以下条件:
安装 Rust:如果还没安装,运行以下命令:
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
安装 OpenCode:参考 opencode.ai 官方文档完成安装
打开终端,进入你常用的项目目录,执行 opencode 启动对话
启动 OpenCode 后,我们就可以开始了。
启动 OpenCode 后,我直接对它说:
> 帮我用 Rust 创建一个命令行 Todo 管理器项目,名字叫 "todo"。需要用到 clap 库做命令行参数解析、serde_json 做 JSON 序列化、dirs 获取用户主目录。先帮我初始化项目结构和依赖。
OpenCode 随即执行了 cargo init todo,然后打开 Cargo.toml 添加了依赖:
[package]
name = "todo"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
dirs = "5"
要点:OpenCode 理解"命令行 Todo 管理器"这个描述,自动选择了合适的依赖——clap 的 derive feature 可以用宏简化 CLI 定义,dirs 跨平台获取用户主目录。如果自己手配,光是确定 crate 和 reading 文档就要花几分钟。
接着我说:
> 定义一个 Todo 结构体,包含 id、title、done 三个字段。数据用 JSON 格式保存到 ~/.rust_todo.json 文件。写 load_todos 和 save_todos 两个函数来处理读写,读取失败时返回空列表。
OpenCode 打开 src/main.rs,写下了数据结构和持久化代码。我看了下 load_todos 的实现,它用 .ok().and_then(…).unwrap_or_default() 链式处理了文件不存在、JSON 解析失败等边界情况:
fn load_todos() -> Vec<Todo> {
let path = get_store_path();
fs::read_to_string(&path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
要点:我只描述了"做什么"和"出错怎么办",OpenCode 自动选用了 Rust 中惯用的错误处理链。这种 ok() + and_then() + unwrap_or_default() 组合是标准库中 Option 的经典模式。
然后我给出了一段较长的提示:
> 用 clap 的 derive 模式定义命令行接口,支持以下子命令:
> - add <title> —— 添加一个任务,自动分配递增 id
> - list —— 列出未完成的任务,-a 或 --all 显示全部
> - done <id> —— 标记指定 id 为完成
> - delete <id> —— 删除指定 id 的任务
> - clear —— 清除所有已完成任务
>
> 每个命令对应的处理逻辑也写出来,操作完自动保存到 JSON 文件。
OpenCode 在 main.rs 末尾补充了大约 60 行代码,包括 Cli 结构体、Command 枚举和 main 函数。其中一个值得注意的细节是 Add { title: Vec<String> } 的定义方式——支持多段字符串拼接,这样 "完成实践课文章" 这种带空格的任务描述就不用加引号。
我对 OpenCode 说:
> 编译并测试所有功能是否正常。
OpenCode 先执行 cargo build,上一步写代码时漏了一个 use std::io; 导入,编译器提示后 OpenCode 立刻修复。编译通过后,它自动跑了一组功能测试:
$ cargo run -- add "完成实践课文章" ✓ #1 已添加 $ cargo run -- add "给项目加单元测试" ✓ #2 已添加 $ cargo run -- list [ ] #1 完成实践课文章 [ ] #2 给项目加单元测试 $ cargo run -- done 1 ✓ #1 已完成 $ cargo run -- list -a [✓] #1 完成实践课文章 [ ] #2 给项目加单元测试 $ cargo run -- clear ✓ 已清除 1 个已完成任务
要点:整个过程我只在第三步描述需求时多打了几行字,后续的编译修复和测试验证都是 OpenCode 自主完成的。如果人工操作,编译报错 → 查文档 → 修代码 → 再编译这个循环至少要多花 5-10 分钟。
最终 src/main.rs(约 100 行):
use clap::{Parser, Subcommand};
use serde::{Deserialize, Serialize};
use std::fs;
use std::io;
use std::path::PathBuf;
const STORE_FILE: &str = ".rust_todo.json";
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Todo {
id: usize,
title: String,
done: bool,
}
#[derive(Parser)]
#[command(name = "todo", about = "命令行 Todo 管理器")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
#[command(about = "添加任务")]
Add { title: Vec<String> },
#[command(about = "列出任务")]
List {
#[arg(short = 'a', long)]
all: bool,
},
#[command(about = "标记完成")]
Done { id: usize },
#[command(about = "删除任务")]
Delete { id: usize },
#[command(about = "清除已完成任务")]
Clear,
}
fn get_store_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(STORE_FILE)
}
fn load_todos() -> Vec<Todo> {
let path = get_store_path();
fs::read_to_string(&path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
fn save_todos(todos: &[Todo]) -> io::Result<()> {
let path = get_store_path();
let json = serde_json::to_string_pretty(todos)?;
fs::write(path, json)
}
fn main() -> io::Result<()> {
let cli = Cli::parse();
let mut todos = load_todos();
match cli.command {
Command::Add { title } => {
let title = title.join(" ");
let id = todos.iter().map(|t| t.id).max().unwrap_or(0) + 1;
todos.push(Todo { id, title, done: false });
save_todos(&todos)?;
println!("✓ #{} 已添加", id);
}
Command::List { all } => {
let items: Vec<_> = if all {
todos.iter().collect()
} else {
todos.iter().filter(|t| !t.done).collect()
};
if items.is_empty() {
println!("暂无任务");
} else {
for t in &items {
let mark = if t.done { "✓" } else { " " };
println!("[{}] #{} {}", mark, t.id, t.title);
}
}
}
Command::Done { id } => {
if let Some(t) = todos.iter_mut().find(|t| t.id == id) {
t.done = true;
save_todos(&todos)?;
println!("✓ #{} 已完成", id);
} else {
println!("✗ 未找到 #{}", id);
}
}
Command::Delete { id } => {
let len_before = todos.len();
todos.retain(|t| t.id != id);
if todos.len() < len_before {
save_todos(&todos)?;
println!("✓ #{} 已删除", id);
} else {
println!("✗ 未找到 #{}", id);
}
}
Command::Clear => {
let before = todos.len();
todos.retain(|t| !t.done);
save_todos(&todos)?;
println!("✓ 已清除 {} 个已完成任务", before - todos.len());
}
}
Ok(())
}
Cargo.toml:
[package]
name = "todo"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
dirs = "5"
用 OpenCode 开发这个 Todo 管理器的几点体会:
描述式编程是关键:只说"做什么"、不说"怎么做"。比如定义数据结构和错误处理策略,OpenCode 能自动选出符合语言习惯的最佳实践。
迭代效率高:分 3-4 步逐步添加功能,每步都能立即看到可运行的代码,出错也能原地对话修复,不像传统开发那样要在"写代码 → 编译 → 调试"之间来回切换。
时间对比明显:从零到完整可运行的 CLI 工具,纯对话时间不到 5 分钟。如果完全手写,查 crate 文档 + 写代码 + 处理编译错误,保守估计需要 25-30 分钟。
跨语言无差别:无论 Go、Python、Rust,OpenCode 对主流语言的代码生成质量都相当稳定。这次选 Rust 也证明了它对类型系统和所有权模型的良好理解。
如果你也经常在开发中需要快速出工具原型,OpenCode 能让你把精力集中在需求描述上,把实现细节交给 AI。