Ink — 用 React 开发交互式命令行应用,使用 React组件构建丰富的终端 UI。
墨水技能指南是一项面向实际任务的技能,主要用于Overview.;Ink 使用瑜伽布局(Flexbox for CLI)使组件直接到终端.;
使用时应结合输入条件选择合适的执行方式,核对必要参数、依赖环境与输出内容,并按原始要求处理异常情况。从功能定位来看,该技能强调把分散的操作要求整理成清晰、可复用的处理流程,使用户能够围绕既定目标快速准备输入、选择执行方式并获得结构化结果。实际使用前应先确认任务范围、数据来源、运行环境、必要权限和关键参数,再依据技能说明逐步执行;若输入条件不完整,应先补齐信息或采用保守配置,避免因错误假设导致结果偏离需求。
执行过程中需要关注工具调用是否成功、接口或依赖是否可用、输出格式是否符合预期,并对异常提示、缺失字段和边界情况进行处理;涉及批量任务时,还应保存进度,避免中断后重复操作。该技能适合用于一次性任务,也可以接入自动化工作流,与其他技能或上层代理配合完成更完整的业务链路;在组合使用时,应明确每一步的输入输出关系,并避免不同步骤之间出现参数冲突。
Ink 使用 Yoga 布局引擎(CLI 版 Flexbox)将 React 组件直接渲染至终端。它负责差异计算(diffing)、重新渲染以及终端 I/O 操作。组件可使用完整的 React 状态、副作用和 Hook —— 其心智模型与 Web 端 React 完全一致。要求 Node.js 22+ 和 React 19.2+ 作为对等依赖(peer dependencies)。
当用户希望使用 JSX/React 语法构建 CLI/TUI 应用、终端仪表盘、交互式提示、带颜色的终端输出、加载指示器(spinners)、进度条或表格时,Ink 是理想选择。
mkdir my-cli && cd my-cli pnpm init pnpm add ink react @types/react pnpm add -D tsx typescript @types/node
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"outDir": "build"
}
}
// index.tsx
import { render, Box, Text } from 'ink';
const App = () => (
Hello, CLI!
);
render( );
运行命令:node --import=tsx index.tsx
Ink 使用 react-reconciler 作为其渲染核心,并以 yoga-layout 为布局引擎。它将组件挂载到一个虚拟终端中,与当前输出进行 diff,并将变更写入 stdout。这意味着:
— 布局容器import { Box, Text } from 'ink';
// 行布局(默认 flexDirection)
Left
Right
// 列布局并添加边框
Title
subtitle
关键 Box 属性:width、height、padding/paddingX/paddingY、borderStyle(single/double/round/classic/bold)、borderDimColor、gap、flexGrow、flexShrink、flexDirection(row/column)、justifyContent、alignItems。
— 样式化文本// 直接设置样式Success // 嵌套 Text 元素Regular // 按宽度自动换行或截断bold andinverse red Truncated long text...
颜色支持:black/red/green/yellow/blue/magenta/cyan/white/gray 及其 Bright 变体(如 redBright 等)、十六进制(#ff0000)、rgb(255,0,0)。
背景色:在颜色名前加 bg 前缀(如 bgGreen、bgCyanBright)。
文本样式:bold、dim、italic、underline、strikethrough、inverse。
、// 插入 2 行空行 // 弹性占位符(flex spacer) // 固定高度占位符
useInput — 键盘输入处理import { useInput } from 'ink';
useInput((input, key) => {
if (key.escape || input === 'q') exit(); // 按 Escape 或 'q' 退出
if (key.upArrow) navigateUp();
if (key.downArrow) navigateDown();
if (key.return) selectItem();
});
key 对象包含以下属性:upArrow、downArrow、leftArrow、rightArrow、return、escape、tab、backspace、delete、ctrl、shift、meta、space、pageUp、pageDown、home、end、f1–f12。
useApp — 应用级控制import { useApp } from 'ink';
const { exit } = useApp();
exit(); // 或 exit(error)
useFocus — 焦点管理import { useFocus } from 'ink';
const { isFocused } = useFocus({ autoFocus: true });
// 聚焦时应用不同样式
{label}
useStdin / useStdout — 流访问const { stdin, isRawModeSupported } = useStdin();
const { stdout } = useStdout();
render() 配置项render(, { exitOnCtrlC: true, // 默认为 true debug: false, // 显示 Yoga 布局调试信息 patchConsole: true, // 屏蔽 console 输出 });
const Menu = ({ items, onSelect }) => {
const [idx, setIdx] = useState(0);
useInput((_, key) => {
if (key.upArrow) setIdx(i => Math.max(0, i - 1));
if (key.downArrow) setIdx(i => Math.min(items.length - 1, i + 1));
if (key.return) onSelect(items[idx]);
});
return (
{items.map((item, i) => (
{i === idx ? '❯ ' : ' '}{item}
))}
);
};
const Spinner = () => {
const [frame, setFrame] = useState(0);
const chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
useEffect(() => {
const id = setInterval(() => setFrame(f => (f + 1) % chars.length), 80);
return () => clearInterval(id);
}, []);
return {chars[frame]} Loading... ;
};
const ProgressBar = ({ percent }) => {
const filled = '█'.repeat(Math.round(percent / 5));
const empty = '░'.repeat(20 - filled.length);
return {filled}{empty} {percent}% ;
};
const Input = ({ onSubmit }) => {
const [value, setValue] = useState('');
useInput((input, key) => {
if (key.return) { onSubmit(value); setValue(''); }
else if (key.backspace) setValue(v => v.slice(0, -1));
else if (input.length === 1 && !key.ctrl) setValue(v => v + input);
});
return ❯ {value}█ ;
};
本技能包含以下文档: