在 Zig 中使用 ANSI 转义码
Zig 标准库没有颜色辅助,但字节字面量是一等公民 —— `"\x1b[31m"` 是 `*const [5:0]u8`,可直接通过 `std.io.getStdOut().writer().print`(或 `std.debug.print` 用于快速原型)输出,无需任何编码处理。自 Zig 0.11 起,规范的能力门控是 `std.io.tty.detectConfig(std.io.getStdOut())` —— 它返回带标签的联合(`.no_color`、`.escape_codes`、`.windows_api`),遵守 `NO_COLOR`、检查 `isatty(2)`,并在 VT 之前的 Windows Conhost 上选择正确的后端。将写入站点包在 `switch (config) { ... }` 内,仅在 `.escape_codes` 分支输出原始转义。 超出零散 print 的场景:**libvaxis**(rockorager/libvaxis)是现代全屏 TUI 库 —— 纯 Zig 实现、可在支持时使用 kitty 键盘协议、自带渲染管线。**mibu**(xyaman/mibu)是小而精的 ANSI 辅助库,用于非全屏输出(`mibu.color.print(.{ .fg = .red }, "error", .{})`)。**zig-spoon** 提供最小化的终端控制(原始模式 + 光标定位),无需完整 TUI 框架。三者都建立在本站记录的字节序列之上。
推荐库
- std.io.tty
标准库能力检测器 —— `std.io.tty.detectConfig(file)` 返回 `.no_color` / `.escape_codes` / `.windows_api`。遵守 `NO_COLOR`、检查 `isatty`、在旧 Conhost 上选择 Win32 控制台 API。自 Zig 0.11 起,每个颜色写入站点都应放在该门控之后。
- libvaxis
现代全屏 TUI 库 —— 纯 Zig、无 C 依赖。在支持的终端上支持 kitty 键盘协议、鼠标、粘贴、OSC 8 超链接、sixel 图像。`zit` git TUI、`vaxe` 编辑器以及多个内部工具 TUI 的底层。紧跟 Zig master 分支。
- mibu
小型 ANSI 辅助 —— 颜色、光标、屏幕、原始模式 termios 封装成友好的 Zig API。`mibu.color.print(.{ .fg = .red }, "error", .{})`、`mibu.cursor.goto(stdout, x, y)`。与 std.io.tty.detectConfig 搭配:mibu 输出原始转义,你在调用点做门控。
- zig-spoon
极简终端控制库 —— 原始模式切换、光标定位、按键解析,无需完整 TUI 框架。在你需要原始键盘输入 + 少量转义写入(文本编辑器、REPL、行内选择器),但不需要 Window / Panel 层级时使用。
常用写法
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
// \x1b is the standard hex escape — Zig string literals are []const u8,
// bytes pass through untouched. No \e shortcut, no encoding work.
try stdout.print("\x1b[1;31merror:\x1b[0m permission denied\n", .{});
try stdout.print("\x1b[1;32mok:\x1b[0m all 142 tests passed\n", .{});
}const std = @import("std");
pub fn main() !void {
const stdout_file = std.io.getStdOut();
const stdout = stdout_file.writer();
const config = std.io.tty.detectConfig(stdout_file);
// config is a tagged union: .no_color, .escape_codes, .windows_api.
// setColor() routes to the right backend automatically.
try config.setColor(stdout, .bold);
try config.setColor(stdout, .red);
try stdout.writeAll("error:");
try config.setColor(stdout, .reset);
try stdout.writeAll(" permission denied\n");
}// build.zig.zon: .mibu = .{ .url = "https://github.com/xyaman/mibu/...", ... }
const std = @import("std");
const mibu = @import("mibu");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try mibu.color.print(stdout, .{ .fg = .red, .style = .bold }, "error:", .{});
try stdout.writeAll(" permission denied\n");
// Truecolor:
try mibu.color.print(stdout, .{
.fg = .{ .rgb = .{ 203, 166, 247 } },
}, "lavender truecolor\n", .{});
}// build.zig.zon: .vaxis = .{ .url = "https://github.com/rockorager/libvaxis/...", ... }
const std = @import("std");
const vaxis = @import("vaxis");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
var vx = try vaxis.init(alloc, .{});
defer vx.deinit(alloc, std.io.tty.tty());
var tty = try vaxis.Tty.init();
defer tty.deinit();
try vx.enterAltScreen(tty.anyWriter());
defer vx.exitAltScreen(tty.anyWriter()) catch {};
var loop: vaxis.Loop(vaxis.Event) = .{ .tty = &tty, .vaxis = &vx };
try loop.init();
try loop.start();
defer loop.stop();
while (true) {
const event = loop.nextEvent();
switch (event) {
.key_press => |k| if (k.matches('q', .{})) break,
.winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
else => {},
}
const win = vx.window();
win.clear();
_ = try win.printSegment(.{ .text = "press q to quit", .style = .{ .fg = .{ .index = 2 } } }, .{});
try vx.render(tty.anyWriter());
}
}