OpenCode 实践课:5 分钟用 Rust 写出一个命令行 Todo 管理器

项目介绍

在日常开发中,我们经常需要在终端里快速记录待办事项,但切换到一个图形化的 Todo 应用又太麻烦。本实践课将带大家用 OpenCode + Rust,从零开始构建一个轻量级的命令行 Todo 管理器,数据以 JSON 格式保存在本地,支持增删改查等基本操作。

  • 技术栈:Rust + clap(CLI 解析)+ serde_json(JSON 序列化)+ dirs(跨平台路径)
  • 核心功能:添加任务 / 列出任务 / 标记完成 / 删除任务 / 清除已办
  • 代码量:主文件约 100 行,加 Cargo.toml 共约 110 行

准备工作

开始之前,确保你的环境满足以下条件:

安装 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 管理器"这个描述,自动选择了合适的依赖——clapderive 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 的经典模式。

第三步:添加 CLI 命令和业务逻辑

然后我给出了一段较长的提示:

> 用 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。