# Peri Code 输入框重构：用一个包装器替代重复的输入代码

> **[Peri Code](https://github.com/konghayao/peri)** — 用 Rust 写的开源 Coding Agent，兼容 Claude Code 生态。<https://github.com/KonghaYao/peri>

Peri 的 TUI（终端里的文本界面）里有十几个表单输入框，分散在登录、配置、初始化向导、插件搜索各个面板。主对话区的输入框用的是成熟的编辑器组件，支持多行、粘贴、选区、中文混排。其余表单字段各存一段文本和一个光标位置数字，共用一个按键处理函数。

这个函数只实现了逐字符的插入、删除和左右移动，粘贴、选区、按词删除都没有。同一个缺陷在每个输入框各犯一遍——空格键在某些场景被截获，中文输入时光标偏移，粘贴内容静默消失。这些不是某个输入框特有的问题，是所有走这条路径的输入框共有的结构缺陷。

这次重构把十几个手写输入框统一到一个基于成熟编辑器组件的包装器上，删掉了那段共用的按键处理函数。改了三十多个文件，旧的共用逻辑删除干净。

## 三种输入框方案的能力差异

| 方案 | 用在哪 | 支持的操作 |
|------|--------|-----------|
| 成熟编辑器组件 | 主对话输入框（1 处） | 多行、粘贴、选区、中文混排、按词删除 |
| 文本字符串加光标位置数字 | 配置、登录、初始化向导等十几个字段 | 逐字符插入、删除、左右移动 |
| 带密码遮挡的输入组件 | 插件搜索、会话浏览等 3 处 | 逐字符插入、删除、密码遮挡、粘贴 |

成熟编辑器组件只用在主对话输入框，能力最全。用量最大的是第二种，十几个表单字段各自维护一段文本和一个光标位置数字，共用一个按键处理函数驱动。第三种介于两者之间，有密码遮挡和粘贴，但没有选区和按词删除。问题集中在第二种方案上，能力最弱，用量最大。

## 手写方案缺失哪些能力

那段共用的按键处理逻辑只认识三种操作，插入一个字符、删除一个字符、光标左右移动一格。以下这些操作全部缺席。

- 粘贴。终端的粘贴走独立事件，不走按键链，这段逻辑没有对应的处理分支，粘贴内容直接消失。
- 选区。没有选区状态，选中的文本范围无从记录，复制和剪切自然也没有。
- 按词删除。没有词边界判断，Ctrl 加退格键只能删一个字符，不能删一个词。
- 中文字符的宽度处理。用字节位置管理光标，中文是多字节字符，索引落在字符中间字节上会导致光标偏移甚至崩溃。

每个手写输入框都走这条路径，所以每个输入框都有同样的缺陷。这不是某个字段少写了一个分支，是整个方案的能力上限就在这里。要加粘贴支持得改这个共用函数，要加选区得先加选区状态再改这个函数，改了之后十几个调用方还要逐个验证。维护成本和缺陷数量都和输入框数量成正比。

## 替代方案的选择依据

三个候选方案里，第一个是继续用带密码遮挡的第三种方案。排除——它的能力和手写方案同级，只有单字符级操作，没有选区和按词删除，换过去只是换了一种残缺。

第二个是每个字段直接裸用成熟编辑器组件。排除——编辑器组件自带边框、光标行高亮、内边距，表单字段需要的是无边框、行内渲染。每个调用方都得重复配置一遍样式和行数限制，等于把样板代码复制十几遍。

第三个是做一个配置化包装器。编辑能力全部委托给成熟编辑器组件，包装器只负责单行或多行的模式配置、样式设定、行数限制。这个方案既复用了成熟组件的编辑能力，又避免了每个字段重复配置。包装器不重新实现任何编辑逻辑，插入、删除、光标移动、选区、按词删除全部是成熟组件已经做好的。

## 包装器的接口设计

包装器对外暴露两种构造方式和一组统一接口。单行模式用于 API Key、搜索框这类只需要一行的场景，多行模式用于自定义输入这类需要换行的场景，接受一个最大行数参数。

统一接口包括处理按键输入并返回是否被消费、获取文本内容、设置文本内容、清空、判空、计算渲染高度、渲染到指定区域。所有编辑操作直接转交给内部的成熟编辑器组件，包装器不做任何拦截或重新实现。

它额外做的只有三件事——配置无边框样式（表单字段不需要编辑器自带的边框），限制最大行数（单行模式固定为 1），计算渲染高度（多行模式根据实际内容行数返回，但不超过设定的上限）。

设置文本内容时，包装器不是逐字符插入，而是按行拆分后重新构造一个编辑器实例。编辑器组件没有提供批量设置文本的接口，逐字符插入既慢又会在中间状态触发不必要的渲染，按行拆分后重新构造避免了这些副作用。

## 表单渲染方式的调整

这是迁移中工作量最大的部分。旧渲染方式是手动拼接文本片段组成一行，在文本末尾加一个方块字符当光标，整个面板是一组文本行统一交给滚动区域渲染，输入框只是其中一行文本。

新渲染方式下编辑器组件是一个独立控件，不是文本片段。它需要一块独立的矩形区域才能渲染，有自己的光标、选区高亮、滚动逻辑。两种模型不兼容——文本行列表里塞不进一个独立控件。

解决方案是覆盖渲染。先用旧的文本拼接方式排好面板布局，记录当前正在编辑的字段在哪一行，渲染完文本行之后，在那个字段的位置上覆盖一层编辑器控件。拿到字段在面板中的行号，减去滚动偏移，得到它在屏幕上的纵向坐标，加上横向偏移和宽度，拼出一块矩形区域渲染上去。字段因为滚动不在可见区域内则跳过。

这个方案的面板布局逻辑改动最小，但它引入了一个新的坐标问题。文本行在面板里的逻辑行号不等于它在屏幕上的视觉行号——前面的内容发生自动换行时，行号会有累积偏移。迁移过程中弹窗里的自定义输入框位置偏了一行。修复方式是用渲染引擎的行数计算函数精确算出前置内容的实际视觉行数，替换掉简单的行号索引。

## 迁移的执行顺序

先迁移最复杂的场景，自定义输入弹窗的多行输入。这是唯一的多行场景，嵌在弹窗里，弹窗高度要随输入行数动态变化。这个场景验证通过后，单行表单的渲染适配就不用再担心。

然后迁移配置面板的三个单行字段，验证覆盖渲染在固定行号布局的表单里能正常工作。模式跑通后，登录面板、OAuth 弹窗、初始化向导按相同模式批量替换。这些全是单行场景，每个字段的改动模式相同——结构体里的文本字符串和光标数字替换成一个包装器实例，按键处理从调用共用函数改为调用包装器的输入接口，渲染从手动拼接文本改为覆盖渲染。最后替换插件搜索和会话浏览器的搜索框，删除旧的共用按键处理函数和显示拼接函数。

十几个输入框不再各自维护光标位置数字，不再共用那段只认得字符插入的按键处理逻辑。粘贴、选区、按词删除、中文混排，所有输入框现在和主输入框能力一致。新增的包装器不到 150 行。

项目地址：[github.com/konghayao/peri](https://github.com/konghayao/peri)
