Java 版终端 UI 框架,灵感来自 ink(React for CLI)。
用声明式的方式构建终端界面:组件树 → Flexbox 布局 → ANSI 渲染 → 键盘交互。
![]() |
![]() |
![]() |
![]() |
| 功能 | 说明 |
|---|---|
| 组件模型 | Builder 模式构建 UI 组件树,类 React 有状态组件 |
| Flexbox 布局 | 纯 Java 实现,支持 direction/justify/align/gap/grow |
| 丰富样式 | 16 色 / 256 色 / RGB 真彩色,粗体/斜体/下划线/反色 |
| 9 种边框 | SINGLE, DOUBLE, ROUND, BOLD, CLASSIC, ARROW 等 |
| 键盘输入 | 基于 JLine 3 raw mode,方向键/功能键/Ctrl 组合键 |
| CJK 支持 | 中日韩字符正确占 2 列宽度 |
| 焦点管理 | Tab/Shift+Tab 导航,可编程聚焦 |
| 全屏模式 | 备用屏幕缓冲区,退出后终端恢复干净 |
| 帧率控制 | 可配置 maxFps,默认 30 |
- Java 8+
- Maven 3.6+
- 真实终端(Windows Terminal / iTerm2 / GNOME Terminal)
<dependency>
<groupId>io.mybatis.jink</groupId>
<artifactId>jink</artifactId>
<version>0.5.0</version>
</dependency>import io.mybatis.jink.Ink;
import io.mybatis.jink.component.*;
import io.mybatis.jink.style.*;
public class HelloWorld {
public static void main(String[] args) {
Ink.renderOnce(
Box.of(
Text.of("Hello, Jink!").color(Color.GREEN).bold()
).borderStyle(BorderStyle.ROUND)
.borderColor(Color.BRIGHT_MAGENTA)
.paddingX(1),
40, 5
);
}
}╭──────────────────────────────────────╮
│ Hello, Jink! │
╰──────────────────────────────────────╯
ink(React/TypeScript)的第一个示例是一个自动计数器:
// ink (TypeScript)
import React, {useState, useEffect} from 'react';
import {render, Text} from 'ink';
const Counter = () => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCounter(previousCounter => previousCounter + 1);
}, 100);
return () => clearInterval(timer);
}, []);
return <Text color="green">{counter} tests passed</Text>;
};
render(<Counter />);等效的 jink(Java)实现:
// jink (Java)
import io.mybatis.jink.Ink;
import io.mybatis.jink.component.Component;
import io.mybatis.jink.component.Renderable;
import io.mybatis.jink.component.Text;
import io.mybatis.jink.style.Color;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Counter extends Component<Counter.State> {
record State(int count) {}
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor();
public Counter() {
super(new State(0));
}
@Override
public void onMount() {
scheduler.scheduleAtFixedRate(() -> {
int next = getState().count() + 1;
setState(new State(next));
if (next >= 100) {
scheduler.shutdown();
}
}, 100, 100, TimeUnit.MILLISECONDS);
}
@Override
public void onUnmount() {
scheduler.shutdownNow();
}
@Override
public Renderable render() {
return Text.of(getState().count() + " tests passed")
.color(Color.GREEN);
}
public static void main(String[] args) {
Ink.render(new Counter()).waitUntilExit();
}
}Box.of(
Text.of("粗体").bold(),
Text.of("红色").color(Color.RED),
Text.of("RGB橙").color(Color.rgb(255, 165, 0)),
Text.of("反色").inverse(),
Text.of(
Text.of("嵌套: ").color(Color.CYAN),
Text.of("红色粗体").color(Color.RED).bold()
)
).flexDirection(FlexDirection.COLUMN);// 水平等分面板
Box.of(
Box.of(Text.of("左侧")).flexGrow(1).borderStyle(BorderStyle.SINGLE),
Box.of(Text.of("右侧")).flexGrow(1).borderStyle(BorderStyle.SINGLE)
).width(60).height(5);
// 垂直布局 + 弹性空白
Box.of(
Text.of("标题").bold(),
Spacer.create(), // 自动填充中间空间
Text.of("底部").dimmed()
).flexDirection(FlexDirection.COLUMN).height(10);public class Counter extends Component<Counter.State> {
record State(int count) {}
public Counter() { super(new State(0)); }
@Override
public Renderable render() {
return Box.of(
Text.of("计数: " + getState().count()).color(Color.GREEN).bold(),
Text.of("↑ 增加 ↓ 减少 q 退出").dimmed()
).flexDirection(FlexDirection.COLUMN)
.borderStyle(BorderStyle.ROUND).paddingX(1);
}
@Override
public void onInput(String input, Key key) {
if (key.upArrow()) setState(new State(getState().count() + 1));
else if (key.downArrow()) setState(new State(getState().count() - 1));
}
public static void main(String[] args) {
Ink.render(new Counter()).waitUntilExit();
}
}// CopilotDemo: 完整复刻 GitHub Copilot CLI 界面
// 包含:标题框 + 消息滚动 + 多行输入 + 输入历史 + 快捷键栏
Ink.render(new CopilotDemo()).waitUntilExit();| 文档 | 说明 |
|---|---|
| 快速入门 | 从零开始,7 个完整示例 |
| 架构与 API 参考 | 包结构、渲染管道、完整 API |
| 示例集锦 | 所有 Demo 的操作步骤(适合录制 GIF) |
| ink vs jink 对比 | 功能覆盖率、缺失项、不可移植项分析 |
自动列出所有 Demo 类,选择序号运行:
# PowerShell(Windows)
.\scripts\run.ps1
# 指定 JDK 路径(当系统 Java < 21 时)
.\scripts\run.ps1 C:\path\to\jdk21# Bash(Linux/macOS)
./scripts/run.sh
# 指定 JDK 路径
./scripts/run.sh /path/to/jdk21:: CMD(Windows)
scripts\run.cmd
:: 指定 JDK 路径
scripts\run.cmd C:\path\to\jdk21# PowerShell
.\scripts\run-demo.ps1 io.mybatis.jink.demo.Counter
.\scripts\run-demo.ps1 io.mybatis.jink.demo.FeatureShowcase
.\scripts\run-demo.ps1 io.mybatis.jink.demo.CopilotDemo
# 指定 JDK 路径(第二个参数)
.\scripts\run-demo.ps1 io.mybatis.jink.demo.Counter C:\path\to\jdk21:: CMD
scripts\run-demo.cmd io.mybatis.jink.demo.CounterJDK 优先级:命令行参数 >
JINK_JAVA_HOME环境变量 > 系统 Java(须 >= 21)
当需要排查 Windows Terminal + JLine 下的键盘/鼠标输入时,可以运行诊断工具:
.\scripts\run-demo.ps1 io.mybatis.jink.demo.InputDiagnostic它会直接启用 trackMouse(),并打印收到的方向键、滚轮和其他 ESC 序列,方便确认当前终端实际发送的输入。
# 编译
mvn clean compile
# 运行 146 个单元测试
mvn test
# 打包
mvn clean packageio.mybatis.jink
├── Ink # 框架入口(render / renderToString / renderOnce)
├── component/ # 组件系统
│ ├── Component<S> # 有状态组件基类
│ ├── Box # Flexbox 容器
│ ├── Text # 文本(支持嵌套和样式)
│ ├── Spacer # 弹性空白
│ ├── Static<T> # 增量渲染
│ └── FocusManager # 焦点管理
├── style/ # 样式定义
│ ├── Color # 16/256/RGB 颜色
│ ├── BorderStyle # 9 种边框样式
│ └── FlexDirection ... # Flexbox 枚举
├── layout/ # Flexbox 布局引擎
├── render/ # 渲染管道(VirtualScreen → ANSI)
├── input/ # 键盘输入(Key + KeyParser)
├── dom/ # 虚拟 DOM(ElementNode/TextNode)
├── ansi/ # ANSI 转义码工具
└── util/ # StringWidth(CJK 宽度计算)
| 功能 | ink | jink |
|---|---|---|
| 声明式 UI | JSX | Builder API |
| 状态管理 | useState | Component.setState() |
| 副作用 | useEffect | onMount/onUnmount |
| 输入处理 | useInput | onInput 方法 |
| Flexbox | Yoga (C++) | 纯 Java 实现 |
| 颜色 | chalk | 内置 Color |
| 边框 | boxen | 9 种 BorderStyle |
| 焦点 | useFocus | FocusManager |
| 静态内容 | <Static> | Static 组件 |
| CJK 宽度 | string-width | StringWidth |
| 终端控制 | 内置 | JLine 3 |
| 最低版本 | Node.js 18+ | Java 8+ |
- CopilotDemo 的鼠标滚轮依赖 JLine Windows 终端的原生鼠标追踪;其他终端的鼠标事件格式可能需要额外适配
- 目前仅在 Windows Terminal + JLine 3 上测试,Linux/macOS 终端行为可能不同
- 尚未发布到 Maven Central(即将发布)
Apache License, Version 2.0 - 详见 LICENSE 文件。



