a modern tui library written in zig
at main 5.6 kB view raw
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}