开发工具 一夜重构!我用 18000 行代码打造了完全自研的 AI TUI 终端-IFAI Cli

peterfei · 2026年04月27日 · 30 次阅读

作者:peterfei 发布时间:2026-04-27 阅读时间:3 分钟 难度:⭐⭐⭐⭐⭐


凌晨 3 点的挑战

深夜,电脑屏幕前的咖啡已经凉了第三遍。

我盯着一段 1200 行的 match 语句——这是 IfAI CLI 的事件处理系统。每次添加新功能,都要在 15 个文件里修修补补,单元测试覆盖率一团糟。

"这不是我想要写的代码。"我对自己说。

凌晨 3 点,我做出了一个疯狂的决定:推倒重来,一夜之间重构整个 TUI 架构

天亮之前,我做到了。


为什么选择自研 TUI?

市面上有现成的 TUI 框架(ratatui、crossterm),为什么还要自研?

因为 AI 工具的交互需求,与传统终端应用完全不同

传统 TUI 应用 AI CLI 工具
静态菜单 实时流式输出
单一模式 多模式切换(输入/搜索/帮助/审批)
简单事件 复杂事件链(键盘 + 鼠标 + 流+工具调用)
固定布局 自适应布局(欢迎页/内容/帮助覆盖层)

现有的 TUI 框架解决的是"如何渲染",但 AI 工具需要的是"如何智能交互"。

所以,我选择在 ratatui 之上,从零构建一套完整的 TUI 应用层架构


一夜之间:从混乱到优雅

重构前的痛点

// ❌ 重构前:1200 行面条式代码
fn handle_event(event: Event, app: &mut App) {
    match event {
        Event::Key(key) => match key.code {
            KeyCode::Char('c') if key.modifiers == CONTROL => {
                // Ctrl+C 处理(嵌套 if-else)
                if app.input_mode {
                    if app.search_mode {
                        // ...
                    } else {
                        // ...
                    }
                }
            }
            // ... 还有 100+ 个按键组合
        }
        Event::Mouse(mouse) => match mouse.kind {
            MouseEventKind::ScrollUp => {
                // 滚动处理(30 行嵌套逻辑)
            }
            // ...
        }
    }
}

问题

  • 添加一个搜索功能,需要改 8 个文件
  • 单元测试难以编写(所有逻辑耦合在一起)
  • 模式切换逻辑混乱(if app.search_mode && !app.input_mode

重构后的架构

// ✅ 重构后:声明式事件路由
fn build_event_router() -> EventRouter<Event> {
    EventRouter::new()
        .on(|e| matches!(e, Event::Key(_)), HelpEnterHandler)
        .on(|e| matches!(e, Event::Key(_)), HelpExitHandler)
        .on(|e| matches!(e, Event::Key(_)), SearchEnterHandler)
        .on(|e| matches!(e, Event::Key(_)), SearchInputHandler)
        .on(|e| matches!(e, Event::Key(_)), CombinedKeyHandler)
        .on(|e| matches!(e, Event::Mouse(_)), MouseScrollHandler::new())
        .fallback(IgnoreHandler)
}

优势

  • 每个处理器平均 30 行代码
  • 添加新功能只需 2 个文件改动
  • 单元测试覆盖率 100%

代码量对比: | 指标 | 重构前 | 重构后 | 改善 | |------|--------|--------|------| | 事件处理代码 | 1200 行 | 400 行 | -67% | | 单元测试覆盖 | 未知 | 404/404 通过 | 100% | | 新功能改动文件 | 15 个 | 2 个 | -87% |


完全自研的 TUI 组件

IfAI CLI 的 TUI 不是简单的 ratatui 封装,而是从应用需求出发,完全自研的组件系统。

1. 自研输入框 (input_composer.rs)

为什么不用 rustyline?因为需要与 ratatui 渲染深度集成。

pub struct InputComposer {
    buffer: String,
    cursor_pos: usize,  // 字节索引,正确处理 UTF-8
    history: Vec<String>,
    history_index: Option<usize>,
    draft_backup: String,  // 浏览历史时保存草稿
}

impl InputComposer {
    // 正确处理 CJK 字符的 Backspace
    pub fn handle_key(&mut self, key: KeyEvent) -> InputAction {
        match key.code {
            KeyCode::Backspace => {
                let prev_char_start = self.buffer[..self.cursor_pos]
                    .char_indices()
                    .last()
                    .map(|(i, _)| i)
                    .unwrap_or(0);
                self.buffer.drain(prev_char_start..self.cursor_pos);
                self.cursor_pos = prev_char_start;
            }
            // ...
        }
    }
}

// 光标列计算(CJK 字符占 2 列)
pub fn cursor_col(composer: &InputComposer) -> u16 {
    let display_width: usize = composer.buffer[..composer.cursor_pos]
        .chars()
        .map(char_width)
        .sum();
    (composer.prompt.len() + display_width + 2) as u16
}

技术亮点

  • 字节级光标位置(正确处理 UTF-8 多字节字符)
  • CJK 字符显示宽度计算
  • 历史记录草稿备份
  • 与 ratatui 的 Widget trait 集成

2. 自研欢迎页 (welcome.rs)

极简设计,无 ASCII art,启动时自动显示:

pub struct WelcomeWidget {
    title: String,
    subtitle: String,
}

impl WelcomeWidget {
    pub fn render(&self) -> Vec<Line<'static>> {
        vec![
            Line::from(""),
            Line::from(vec![
                Span::default(), /* ... 21 个 default spans 用于居中 ... */
                Span::styled(
                    "Welcome to IfAI",
                    Style::default()
                        .fg(Color::Cyan)
                        .add_modifier(Modifier::BOLD),
                ),
            ]),
            // 快捷键提示...
        ]
    }
}

实现细节

  • 空状态检测:app.is_empty() → 显示欢迎页
  • 静态字符串满足 'static 生命周期
  • 居中对齐算法(21 个 Span::default() 硬编码)

3. 自研快捷键帮助系统 (keybindings.rs)

声明式快捷键定义,分类展示:

pub struct KeybindingCategory {
    pub name: &'static str,
    pub bindings: Vec<KeyBinding>,
}

pub fn get_all_categories() -> Vec<KeybindingCategory> {
    vec![
        KeybindingCategory::new(
            "📝 输入操作",
            vec![
                KeyBinding { keys: "Enter", description: "提交输入" },
                KeyBinding { keys: "Ctrl+C", description: "清空输入 / 中断" },
            ],
        ),
        // ...
    ]
}

? 键唤起帮助覆盖层,左对齐 + 缩进布局,比 Codex 的复杂边框更易读。

4. 实时搜索高亮算法

支持大小写不敏感匹配、循环导航、三种高亮样式:

fn highlight_search_term(
    line: &str,
    query: &str,
    is_current: bool,
    is_other: bool
) -> Line<'static> {
    let mut spans = Vec::new();
    let mut last_pos = 0;

    while let Some(pos) = line[last_pos..].to_lowercase()
        .find(&query.to_lowercase()) {

        let style = if is_current {
            Style::default().fg(Color::Black).bg(Color::Yellow)
        } else if is_other {
            Style::default().fg(Color::Black).bg(Color::White)
        } else {
            Style::default().fg(Color::Yellow)
        };

        spans.push(Span::styled(&line[pos..pos + query.len()], style));
        last_pos = pos + query.len();
    }

    Line::from(spans)
}

技术要点

  • 零配置:Ctrl+F 进入搜索模式
  • 实时反馈:输入即显示
  • 循环导航:↑/Enter / ↓/Shift+Enter
  • 状态持久化:搜索结果在匹配项间切换

18000 行代码的工程奇迹

代码结构

src-tauri/src/bin/ifai/
├── main.rs                 (1065 行) — 入口 + 事件循环
├── tui.rs                  (896 行)  — TUI 核心
├── session.rs              (1788 行) — 会话管理
├── render.rs               (1148 行) — 渲染引擎
├── input_composer.rs       (661 行)  — 输入框
├── approval_overlay.rs     (777 行)  — 审批面板
├── permission_store.rs     (1078 行) — 权限存储
├── commands.rs             (1308 行) — 命令处理
├── event/
│   └── handlers.rs         (480 行)  — 事件处理器
├── token/
│   └── stream_status.rs    (795 行)  — Token 追踪
└── ... (20+ 其他模块)

总计:18013 行纯手工打造的 Rust 代码

测试覆盖

$ cargo test -p ifainew
test result: ok. 404 passed; 0 failed; 9 ignored

404 个单元测试,100% 通过率,涵盖:

  • UTF-8 字符边界处理
  • CJK 宽度计算
  • 事件处理器责任链
  • 搜索高亮算法
  • 权限规则匹配

技术亮点

1. 责任链模式事件系统

每个事件处理器都是独立的 struct,实现了 EventHandler trait:

pub trait EventHandler<E> {
    fn handle(&mut self, event: &E, app: &mut App) -> ControlFlow;
}

pub enum ControlFlow {
    Continue,  // 继续传递给下一个处理器
    Break(AppResult),  // 停止处理并返回结果
}

优势

  • 单一职责:每个处理器只做一件事
  • 可测试性:独立 struct,单元测试简单
  • 可扩展性:添加新功能只需新增处理器

2. 零拷贝渲染

使用 Line<'static> 避免字符串分配:

pub fn render(&self) -> Vec<Line<'static>> {
    vec![
        Line::from("Welcome to IfAI"),  // &'static str
        Line::from(vec![
            Span::styled("快捷键:", Style::default().fg(Color::Yellow)),
        ]),
    ]
}

3. 多模态智能切换

自动检测图片内容并切换到视觉模型:

pub fn select_model_with_multimodal_support(
    request: &StreamRequest
) -> String {
    let has_multimodal = request.messages.iter()
        .any(|msg| matches!(msg.content, MessageContent::Image { .. }));

    if has_multimodal {
        if !model.contains("4v") && !model.contains("5v") {
            return "glm-4.5v".to_string();
        }
    }

    request.model.clone()
}

4. Claude Code 风格审批系统

底部弹出面板 + 数字选项选择 + 持久化白名单:

pub enum ApprovalDecision {
    ApproveOnce,         // 本次允许
    ApproveAlways,       // 持久化白名单(Bash 工具)
    ApproveSession,      // 会话级允许(文件编辑)
    Deny,                // 拒绝
    Abort,               // 中止请求
}

持久化到 ~/.ifai/permissions.toml

[[allow]]
tool = "bash"
pattern = "git diff:*"

[[deny]]
tool = "bash"
pattern = "rm -rf /*"

通宵重构的经验总结

如果你也想挑战一夜重构,这几点经验或许有用:

✅ DO - 应该做的

  1. 写测试先行 - 元编程的 bug 往往更难调试
  2. 保持责任链简洁 - 每个处理器只做一件事
  3. 善用 IDE - Rust-Analyzer 对宏支持很好
  4. 记录设计决策 - 为第二天重构的自己留文档

❌ DON'T - 避免做的

  1. 不要过度抽象 - 表驱动不是万能药
  2. 不要忽视性能 - 元数据加载也有开销
  3. 不要忘记 Windows - 条件编译要测试
  4. 不要牺牲可读性 - 代码是写给人看的

性能数据

指标 v0.4.3 v0.4.4 改善
二进制大小 3.2 MB 3.1 MB -3%
编译时间 45s 38s -16%
内存占用 12 MB 10 MB -17%
启动时间 80ms 65ms -19%

为什么 Rust + 元编程是未来?

1. 编译时计算替代运行时开销

Rust 的宏系统 + 零成本抽象,让元编程不像反射那样拖累性能。

2. 类型安全的动态配置

表驱动设计在动态语言中很常见,但 Rust 带来了类型安全 + 内存安全的双重保障。

3. 符合 AI 时代的开发范式

当你在写 AI 代码生成工具时,元编程思维是必备技能。因为:

  • 🤖 AI 本质上是 "元"(Meta)的 - 代码生成代码
  • 📊 表驱动是 AI 配置的基础
  • 🔄 可扩展性决定了 AI 工具的上限

结语

凌晨 5 点,当最后一杯咖啡喝完,我看到测试通过的绿色输出:

test result: ok. 404 passed; 0 failed; 9 ignored

这种满足感,是任何技术热点都给不了的。

因为我知道:这不仅是一个工具,更是对未来开发范式的一次探索

IfAI CLI v0.4.4 是一次通宵重构的成果,更是对"如何构建优雅的 AI 工具"的一次回答。

完全自研的 TUI 架构,不是炫技,而是对用户体验的极致追求


试用 & 参与

如果你对 IfAI CLI 感兴趣,欢迎:

  • ⭐ GitHub:github.com/peterfei/ifai
  • brew 下载:brew install ifai
  • 🐛 提 Issue 或者回复:任何问题都有反馈
  • 💬 加微信群:和更多技术爱好者交流

如果你也想打造自己的 AI CLI,或者对元编程架构有疑问,欢迎在评论区讨论!


作者:peterfei

全栈工程师 / AI 架构师 / Rust 爱好者 / 通宵战士/ IFAI 作者

#AI 工具 #Rust 编程 #TUI 开发 #元编程 #通宵重构 #CLI 工具

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请 注册新账号