diff --git a/src/widgets/terminal/Parser.zig b/src/widgets/terminal/Parser.zig index 0395fee..ee645c0 100644 --- a/src/widgets/terminal/Parser.zig +++ b/src/widgets/terminal/Parser.zig @@ -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, } } diff --git a/src/widgets/terminal/Screen.zig b/src/widgets/terminal/Screen.zig index 8dd3f97..00b754f 100644 --- a/src/widgets/terminal/Screen.zig +++ b/src/widgets/terminal/Screen.zig @@ -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, } } diff --git a/src/widgets/terminal/Terminal.zig b/src/widgets/terminal/Terminal.zig index 35d1d09..08b385b 100644 --- a/src/widgets/terminal/Terminal.zig +++ b/src/widgets/terminal/Terminal.zig @@ -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; - self.back_screen.cursor.row = @min(self.back_screen.height - 1, self.back_screen.cursor.row + delta); - }, - } + 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; -} diff --git a/src/widgets/terminal/ansi.zig b/src/widgets/terminal/ansi.zig new file mode 100644 index 0000000..fbdbe36 --- /dev/null +++ b/src/widgets/terminal/ansi.zig @@ -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; + } + }; +}