widgets(terminal): use types for c0 and csi

This commit is contained in:
Tim Culverhouse 2024-06-07 11:53:41 -05:00
parent ecb2ea20e4
commit 36854123fe
4 changed files with 177 additions and 130 deletions

View file

@ -3,15 +3,16 @@ const Parser = @This();
const std = @import("std");
const Reader = std.io.AnyReader;
const ansi = @import("ansi.zig");
/// A terminal event
const Event = union(enum) {
print: []const u8,
c0: u8,
c0: ansi.C0,
escape: []const u8,
ss2: u8,
ss3: u8,
csi: []const u8,
csi: ansi.CSI,
osc: []const u8,
apc: []const u8,
};
@ -52,7 +53,7 @@ pub fn parseReader(self: *Parser, reader: Reader) !Event {
// C0 control
0x00...0x1a,
0x1c...0x1f,
=> return .{ .c0 = b },
=> return .{ .c0 = @enumFromInt(b) },
else => {
try self.buf.append(b);
return self.parseGround(reader);
@ -129,12 +130,24 @@ inline fn parseOsc(self: *Parser, reader: Reader) !Event {
}
inline fn parseCsi(self: *Parser, reader: Reader) !Event {
var intermediate: ?u8 = null;
var pm: ?u8 = null;
while (true) {
const b = try reader.readByte();
try self.buf.append(b);
switch (b) {
0x20...0x2F => intermediate = b,
0x30...0x3B => try self.buf.append(b),
0x3C...0x3F => pm = b, // we only allow one
// Really we should execute C0 controls, but we just ignore them
0x40...0xFF => return .{ .csi = self.buf.items },
0x40...0xFF => return .{
.csi = .{
.intermediate = intermediate,
.private_marker = pm,
.params = self.buf.items,
.final = b,
},
},
else => continue,
}
}

View file

@ -2,6 +2,8 @@ const std = @import("std");
const assert = std.debug.assert;
const vaxis = @import("../../main.zig");
const ansi = @import("ansi.zig");
const log = std.log.scoped(.terminal);
const Screen = @This();
@ -185,65 +187,24 @@ pub fn index(self: *Screen) !void {
self.cursor.row += 1;
}
fn Parameter(T: type) type {
return struct {
const Self = @This();
val: T,
// indicates the next parameter is a sub-parameter
has_sub: bool = false,
is_empty: bool = false,
const Iterator = struct {
bytes: []const u8,
idx: usize = 0,
fn next(self: *Iterator) ?Self {
const start = self.idx;
var val: T = 0;
while (self.idx < self.bytes.len) {
defer self.idx += 1; // defer so we trigger on return as well
const b = self.bytes[self.idx];
switch (b) {
0x30...0x39 => {
val = (val * 10) + (b - 0x30);
if (self.idx == self.bytes.len - 1) return .{ .val = val };
},
':', ';' => return .{
.val = val,
.is_empty = self.idx == start,
.has_sub = b == ':',
},
else => return null,
}
}
return null;
}
};
};
}
pub fn sgr(self: *Screen, seq: []const u8) void {
if (seq.len == 0) {
pub fn sgr(self: *Screen, seq: ansi.CSI) void {
if (seq.params.len == 0) {
self.cursor.style = .{};
return;
}
switch (seq[0]) {
0x30...0x39 => {},
else => return, // TODO: handle private indicator sequences
}
var iter: Parameter(u8).Iterator = .{ .bytes = seq };
var iter = seq.iterator(u8);
while (iter.next()) |ps| {
switch (ps.val) {
switch (ps) {
0 => self.cursor.style = .{},
1 => self.cursor.style.bold = true,
2 => self.cursor.style.dim = true,
3 => self.cursor.style.italic = true,
4 => {
const kind: vaxis.Style.Underline = if (ps.has_sub) blk: {
const ul = iter.next() orelse break :blk .single;
break :blk @enumFromInt(ul.val);
} else .single;
const kind: vaxis.Style.Underline = if (iter.next_is_sub)
@enumFromInt(iter.next() orelse 1)
else
.single;
self.cursor.style.ul_style = kind;
},
5 => self.cursor.style.blink = true,
@ -261,72 +222,58 @@ pub fn sgr(self: *Screen, seq: []const u8) void {
27 => self.cursor.style.reverse = false,
28 => self.cursor.style.invisible = false,
29 => self.cursor.style.strikethrough = false,
30...37 => self.cursor.style.fg = .{ .index = ps.val - 30 },
30...37 => self.cursor.style.fg = .{ .index = ps - 30 },
38 => {
// must have another parameter
const kind = iter.next() orelse return;
switch (kind.val) {
switch (kind) {
2 => { // rgb
const r = r: {
// First param can be empty
var ps_r = iter.next() orelse return;
while (ps_r.is_empty) {
if (iter.is_empty)
ps_r = iter.next() orelse return;
}
break :r ps_r.val;
};
const g = g: {
const ps_g = iter.next() orelse return;
break :g ps_g.val;
};
const b = b: {
const ps_b = iter.next() orelse return;
break :b ps_b.val;
break :r ps_r;
};
const g = iter.next() orelse return;
const b = iter.next() orelse return;
self.cursor.style.fg = .{ .rgb = .{ r, g, b } };
},
5 => {
const idx = iter.next() orelse return;
self.cursor.style.fg = .{ .index = idx.val };
self.cursor.style.fg = .{ .index = idx };
}, // index
else => return,
}
},
39 => self.cursor.style.fg = .default,
40...47 => self.cursor.style.bg = .{ .index = ps.val - 40 },
40...47 => self.cursor.style.bg = .{ .index = ps - 40 },
48 => {
// must have another parameter
const kind = iter.next() orelse return;
switch (kind.val) {
switch (kind) {
2 => { // rgb
const r = r: {
// First param can be empty
var ps_r = iter.next() orelse return;
while (ps_r.is_empty) {
if (iter.is_empty)
ps_r = iter.next() orelse return;
}
break :r ps_r.val;
};
const g = g: {
const ps_g = iter.next() orelse return;
break :g ps_g.val;
};
const b = b: {
const ps_b = iter.next() orelse return;
break :b ps_b.val;
break :r ps_r;
};
const g = iter.next() orelse return;
const b = iter.next() orelse return;
self.cursor.style.bg = .{ .rgb = .{ r, g, b } };
},
5 => { // index
5 => {
const idx = iter.next() orelse return;
self.cursor.style.bg = .{ .index = idx.val };
},
self.cursor.style.bg = .{ .index = idx };
}, // index
else => return,
}
},
49 => self.cursor.style.bg = .default,
90...97 => self.cursor.style.fg = .{ .index = ps.val - 90 + 8 },
100...107 => self.cursor.style.bg = .{ .index = ps.val - 100 + 8 },
90...97 => self.cursor.style.fg = .{ .index = ps - 90 + 8 },
100...107 => self.cursor.style.bg = .{ .index = ps - 100 + 8 },
else => continue,
}
}

View file

@ -3,6 +3,7 @@ const Terminal = @This();
const std = @import("std");
const builtin = @import("builtin");
const ansi = @import("ansi.zig");
pub const Command = @import("Command.zig");
const Parser = @import("Parser.zig");
const Pty = @import("Pty.zig");
@ -185,39 +186,24 @@ fn run(self: *Terminal) !void {
},
.c0 => |b| try self.handleC0(b),
.csi => |seq| {
const final = seq[seq.len - 1];
switch (final) {
switch (seq.final) {
'B' => { // CUD
switch (seq.len) {
0 => unreachable,
1 => self.back_screen.cursor.row += 1,
else => {
const delta = parseParam(u16, seq[2 .. seq.len - 1], 1) orelse 1;
var iter = seq.iterator(u16);
const delta = iter.next() orelse 1;
self.back_screen.cursor.row = @min(self.back_screen.height - 1, self.back_screen.cursor.row + delta);
},
}
},
'H' => { // CUP
const delim = std.mem.indexOfScalar(u8, seq, ';') orelse {
switch (seq.len) {
0 => unreachable,
1 => {
self.back_screen.cursor.row = 0;
self.back_screen.cursor.col = 0;
},
else => {
const row = parseParam(u16, seq[0 .. seq.len - 1], 1) orelse 1;
self.back_screen.cursor.row = row - 1;
},
}
continue;
};
const row = parseParam(u16, seq[0..delim], 1) orelse 1;
const col = parseParam(u16, seq[delim + 1 .. seq.len - 1], 1) orelse 1;
var iter = seq.iterator(u16);
const row = iter.next() orelse 1;
const col = iter.next() orelse 1;
self.back_screen.cursor.col = col - 1;
self.back_screen.cursor.row = row - 1;
},
'm' => self.back_screen.sgr(seq[0 .. seq.len - 1]),
'm' => {
if (seq.intermediate == null and seq.private_marker == null) {
self.back_screen.sgr(seq);
}
},
else => {},
}
},
@ -226,15 +212,15 @@ fn run(self: *Terminal) !void {
}
}
inline fn handleC0(self: *Terminal, b: u8) !void {
inline fn handleC0(self: *Terminal, b: ansi.C0) !void {
switch (b) {
0x00, 0x01, 0x02 => {}, // NUL, SOH, STX
0x05 => {}, // ENQ
0x07 => self.pending_events.bell.store(true, .unordered), // BEL
0x08 => self.back_screen.cursorLeft(1), // BS
0x09 => {}, // TODO: HT
0x0a, 0x0b, 0x0c => try self.back_screen.index(), // LF, VT, FF
0x0d => { // CR
.NUL, .SOH, .STX => {},
.ENQ => {},
.BEL => self.pending_events.bell.store(true, .unordered),
.BS => self.back_screen.cursorLeft(1),
.HT => {}, // TODO: HT
.LF, .VT, .FF => try self.back_screen.index(),
.CR => {
self.back_screen.cursor.pending_wrap = false;
self.back_screen.cursor.col = if (self.mode.origin)
self.back_screen.scrolling_region.left
@ -243,14 +229,8 @@ inline fn handleC0(self: *Terminal, b: u8) !void {
else
0;
},
0x0e => {}, // TODO: Charset shift out
0x0f => {}, // TODO: Charset shift in
else => log.warn("unhandled C0: 0x{x}", .{b}),
.SO => {}, // TODO: Charset shift out
.SI => {}, // TODO: Charset shift in
else => log.warn("unhandled C0: 0x{x}", .{@intFromEnum(b)}),
}
}
/// Parse a param buffer, returning a default value if the param was empty
inline fn parseParam(comptime T: type, buf: []const u8, default: ?T) ?T {
if (buf.len == 0) return default;
return std.fmt.parseUnsigned(T, buf, 10) catch return null;
}

View file

@ -0,0 +1,107 @@
/// Control bytes. See man 7 ascii
pub const C0 = enum(u8) {
NUL = 0x00,
SOH = 0x01,
STX = 0x02,
ETX = 0x03,
EOT = 0x04,
ENQ = 0x05,
ACK = 0x06,
BEL = 0x07,
BS = 0x08,
HT = 0x09,
LF = 0x0a,
VT = 0x0b,
FF = 0x0c,
CR = 0x0d,
SO = 0x0e,
SI = 0x0f,
DLE = 0x10,
DC1 = 0x11,
DC2 = 0x12,
DC3 = 0x13,
DC4 = 0x14,
NAK = 0x15,
SYN = 0x16,
ETB = 0x17,
CAN = 0x18,
EM = 0x19,
SUB = 0x1a,
ESC = 0x1b,
FS = 0x1c,
GS = 0x1d,
RS = 0x1e,
US = 0x1f,
};
pub const CSI = struct {
intermediate: ?u8 = null,
private_marker: ?u8 = null,
final: u8,
params: []const u8,
pub fn hasIntermediate(self: CSI, b: u8) bool {
return b == self.intermediate orelse return false;
}
pub fn hasPrivateMarker(self: CSI, b: u8) bool {
return b == self.private_marker orelse return false;
}
pub fn iterator(self: CSI, comptime T: type) ParamIterator(T) {
return .{ .bytes = self.params };
}
};
pub fn ParamIterator(T: type) type {
return struct {
const Self = @This();
bytes: []const u8,
idx: usize = 0,
/// indicates the next parameter will be a sub parameter of the current
next_is_sub: bool = false,
/// indicates the current parameter was an empty string
is_empty: bool = false,
pub fn next(self: *Self) ?T {
// reset state
self.next_is_sub = false;
self.is_empty = false;
const start = self.idx;
var val: T = 0;
while (self.idx < self.bytes.len) {
defer self.idx += 1; // defer so we trigger on return as well
const b = self.bytes[self.idx];
switch (b) {
0x30...0x39 => {
val = (val * 10) + (b - 0x30);
if (self.idx == self.bytes.len - 1) return val;
},
':', ';' => {
self.next_is_sub = b == ':';
self.is_empty = self.idx == start;
return val;
},
else => return null,
}
}
return null;
}
/// verifies there are at least n more parameters
pub fn hasAtLeast(self: *Self, n: usize) bool {
const start = self.idx;
defer self.idx = start;
var i: usize = 0;
while (self.next()) |_| {
i += 1;
if (i >= n) return true;
}
return i >= n;
}
};
}