a modern tui library written in zig
1//! An ANSI VT Parser
2const Parser = @This();
3
4const std = @import("std");
5const Reader = std.Io.Reader;
6const ansi = @import("ansi.zig");
7
8/// A terminal event
9const Event = union(enum) {
10 print: []const u8,
11 c0: ansi.C0,
12 escape: []const u8,
13 ss2: u8,
14 ss3: u8,
15 csi: ansi.CSI,
16 osc: []const u8,
17 apc: []const u8,
18};
19
20buf: std.array_list.Managed(u8),
21/// a leftover byte from a ground event
22pending_byte: ?u8 = null,
23
24pub fn parseReader(self: *Parser, reader: *Reader) !Event {
25 self.buf.clearRetainingCapacity();
26 while (true) {
27 const b = if (self.pending_byte) |p| p else try reader.takeByte();
28 self.pending_byte = null;
29 switch (b) {
30 // Escape sequence
31 0x1b => {
32 const next = try reader.takeByte();
33 switch (next) {
34 0x4E => return .{ .ss2 = try reader.takeByte() },
35 0x4F => return .{ .ss3 = try reader.takeByte() },
36 0x50 => try skipUntilST(reader), // DCS
37 0x58 => try skipUntilST(reader), // SOS
38 0x5B => return self.parseCsi(reader), // CSI
39 0x5D => return self.parseOsc(reader), // OSC
40 0x5E => try skipUntilST(reader), // PM
41 0x5F => return self.parseApc(reader), // APC
42
43 0x20...0x2F => {
44 try self.buf.append(next);
45 return self.parseEscape(reader); // ESC
46 },
47 else => {
48 try self.buf.append(next);
49 return .{ .escape = self.buf.items };
50 },
51 }
52 },
53 // C0 control
54 0x00...0x1a,
55 0x1c...0x1f,
56 => return .{ .c0 = @enumFromInt(b) },
57 else => {
58 try self.buf.append(b);
59 return self.parseGround(reader);
60 },
61 }
62 }
63}
64
65inline fn parseGround(self: *Parser, reader: *Reader) !Event {
66 var buf: [1]u8 = undefined;
67 {
68 std.debug.assert(self.buf.items.len > 0);
69 // Handle first byte
70 const len = try std.unicode.utf8ByteSequenceLength(self.buf.items[0]);
71 var i: usize = 1;
72 while (i < len) : (i += 1) {
73 const read = try reader.readSliceShort(&buf);
74 if (read == 0) return error.EOF;
75 try self.buf.append(buf[0]);
76 }
77 }
78 while (true) {
79 if (reader.bufferedLen() == 0) return .{ .print = self.buf.items };
80 const n = try reader.readSliceShort(&buf);
81 if (n == 0) return error.EOF;
82 const b = buf[0];
83 switch (b) {
84 0x00...0x1f => {
85 self.pending_byte = b;
86 return .{ .print = self.buf.items };
87 },
88 else => {
89 try self.buf.append(b);
90 const len = try std.unicode.utf8ByteSequenceLength(b);
91 var i: usize = 1;
92 while (i < len) : (i += 1) {
93 const read = try reader.readSliceShort(&buf);
94 if (read == 0) return error.EOF;
95
96 try self.buf.append(buf[0]);
97 }
98 },
99 }
100 }
101}
102
103/// parse until b >= 0x30
104inline fn parseEscape(self: *Parser, reader: *Reader) !Event {
105 while (true) {
106 const b = try reader.takeByte();
107 switch (b) {
108 0x20...0x2F => continue,
109 else => {
110 try self.buf.append(b);
111 return .{ .escape = self.buf.items };
112 },
113 }
114 }
115}
116
117inline fn parseApc(self: *Parser, reader: *Reader) !Event {
118 while (true) {
119 const b = try reader.takeByte();
120 switch (b) {
121 0x00...0x17,
122 0x19,
123 0x1c...0x1f,
124 => continue,
125 0x1b => {
126 _ = try reader.discard(std.Io.Limit.limited(1));
127 return .{ .apc = self.buf.items };
128 },
129 else => try self.buf.append(b),
130 }
131 }
132}
133
134/// Skips sequences until we see an ST (String Terminator, ESC \)
135inline fn skipUntilST(reader: *Reader) !void {
136 _ = try reader.discardDelimiterExclusive('\x1b');
137 _ = try reader.discard(std.Io.Limit.limited(1));
138}
139
140/// Parses an OSC sequence
141inline fn parseOsc(self: *Parser, reader: *Reader) !Event {
142 while (true) {
143 const b = try reader.takeByte();
144 switch (b) {
145 0x00...0x06,
146 0x08...0x17,
147 0x19,
148 0x1c...0x1f,
149 => continue,
150 0x1b => {
151 _ = try reader.discard(std.Io.Limit.limited(1));
152 return .{ .osc = self.buf.items };
153 },
154 0x07 => return .{ .osc = self.buf.items },
155 else => try self.buf.append(b),
156 }
157 }
158}
159
160inline fn parseCsi(self: *Parser, reader: *Reader) !Event {
161 var intermediate: ?u8 = null;
162 var pm: ?u8 = null;
163
164 while (true) {
165 const b = try reader.takeByte();
166 switch (b) {
167 0x20...0x2F => intermediate = b,
168 0x30...0x3B => try self.buf.append(b),
169 0x3C...0x3F => pm = b, // we only allow one
170 // Really we should execute C0 controls, but we just ignore them
171 0x40...0xFF => return .{
172 .csi = .{
173 .intermediate = intermediate,
174 .private_marker = pm,
175 .params = self.buf.items,
176 .final = b,
177 },
178 },
179 else => continue,
180 }
181 }
182}