widgets(terminal): use types for c0 and csi
This commit is contained in:
parent
ecb2ea20e4
commit
36854123fe
4 changed files with 177 additions and 130 deletions
|
@ -3,15 +3,16 @@ const Parser = @This();
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Reader = std.io.AnyReader;
|
const Reader = std.io.AnyReader;
|
||||||
|
const ansi = @import("ansi.zig");
|
||||||
|
|
||||||
/// A terminal event
|
/// A terminal event
|
||||||
const Event = union(enum) {
|
const Event = union(enum) {
|
||||||
print: []const u8,
|
print: []const u8,
|
||||||
c0: u8,
|
c0: ansi.C0,
|
||||||
escape: []const u8,
|
escape: []const u8,
|
||||||
ss2: u8,
|
ss2: u8,
|
||||||
ss3: u8,
|
ss3: u8,
|
||||||
csi: []const u8,
|
csi: ansi.CSI,
|
||||||
osc: []const u8,
|
osc: []const u8,
|
||||||
apc: []const u8,
|
apc: []const u8,
|
||||||
};
|
};
|
||||||
|
@ -52,7 +53,7 @@ pub fn parseReader(self: *Parser, reader: Reader) !Event {
|
||||||
// C0 control
|
// C0 control
|
||||||
0x00...0x1a,
|
0x00...0x1a,
|
||||||
0x1c...0x1f,
|
0x1c...0x1f,
|
||||||
=> return .{ .c0 = b },
|
=> return .{ .c0 = @enumFromInt(b) },
|
||||||
else => {
|
else => {
|
||||||
try self.buf.append(b);
|
try self.buf.append(b);
|
||||||
return self.parseGround(reader);
|
return self.parseGround(reader);
|
||||||
|
@ -129,12 +130,24 @@ inline fn parseOsc(self: *Parser, reader: Reader) !Event {
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn parseCsi(self: *Parser, reader: Reader) !Event {
|
inline fn parseCsi(self: *Parser, reader: Reader) !Event {
|
||||||
|
var intermediate: ?u8 = null;
|
||||||
|
var pm: ?u8 = null;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const b = try reader.readByte();
|
const b = try reader.readByte();
|
||||||
try self.buf.append(b);
|
|
||||||
switch (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
|
// 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,
|
else => continue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ const std = @import("std");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const vaxis = @import("../../main.zig");
|
const vaxis = @import("../../main.zig");
|
||||||
|
|
||||||
|
const ansi = @import("ansi.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.terminal);
|
const log = std.log.scoped(.terminal);
|
||||||
|
|
||||||
const Screen = @This();
|
const Screen = @This();
|
||||||
|
@ -185,65 +187,24 @@ pub fn index(self: *Screen) !void {
|
||||||
self.cursor.row += 1;
|
self.cursor.row += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn Parameter(T: type) type {
|
pub fn sgr(self: *Screen, seq: ansi.CSI) void {
|
||||||
return struct {
|
if (seq.params.len == 0) {
|
||||||
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) {
|
|
||||||
self.cursor.style = .{};
|
self.cursor.style = .{};
|
||||||
return;
|
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| {
|
while (iter.next()) |ps| {
|
||||||
switch (ps.val) {
|
switch (ps) {
|
||||||
0 => self.cursor.style = .{},
|
0 => self.cursor.style = .{},
|
||||||
1 => self.cursor.style.bold = true,
|
1 => self.cursor.style.bold = true,
|
||||||
2 => self.cursor.style.dim = true,
|
2 => self.cursor.style.dim = true,
|
||||||
3 => self.cursor.style.italic = true,
|
3 => self.cursor.style.italic = true,
|
||||||
4 => {
|
4 => {
|
||||||
const kind: vaxis.Style.Underline = if (ps.has_sub) blk: {
|
const kind: vaxis.Style.Underline = if (iter.next_is_sub)
|
||||||
const ul = iter.next() orelse break :blk .single;
|
@enumFromInt(iter.next() orelse 1)
|
||||||
break :blk @enumFromInt(ul.val);
|
else
|
||||||
} else .single;
|
.single;
|
||||||
self.cursor.style.ul_style = kind;
|
self.cursor.style.ul_style = kind;
|
||||||
},
|
},
|
||||||
5 => self.cursor.style.blink = true,
|
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,
|
27 => self.cursor.style.reverse = false,
|
||||||
28 => self.cursor.style.invisible = false,
|
28 => self.cursor.style.invisible = false,
|
||||||
29 => self.cursor.style.strikethrough = 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 => {
|
38 => {
|
||||||
// must have another parameter
|
// must have another parameter
|
||||||
const kind = iter.next() orelse return;
|
const kind = iter.next() orelse return;
|
||||||
switch (kind.val) {
|
switch (kind) {
|
||||||
2 => { // rgb
|
2 => { // rgb
|
||||||
const r = r: {
|
const r = r: {
|
||||||
// First param can be empty
|
// First param can be empty
|
||||||
var ps_r = iter.next() orelse return;
|
var ps_r = iter.next() orelse return;
|
||||||
while (ps_r.is_empty) {
|
if (iter.is_empty)
|
||||||
ps_r = iter.next() orelse return;
|
ps_r = iter.next() orelse return;
|
||||||
}
|
break :r ps_r;
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
const g = iter.next() orelse return;
|
||||||
|
const b = iter.next() orelse return;
|
||||||
self.cursor.style.fg = .{ .rgb = .{ r, g, b } };
|
self.cursor.style.fg = .{ .rgb = .{ r, g, b } };
|
||||||
},
|
},
|
||||||
5 => {
|
5 => {
|
||||||
const idx = iter.next() orelse return;
|
const idx = iter.next() orelse return;
|
||||||
self.cursor.style.fg = .{ .index = idx.val };
|
self.cursor.style.fg = .{ .index = idx };
|
||||||
}, // index
|
}, // index
|
||||||
else => return,
|
else => return,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
39 => self.cursor.style.fg = .default,
|
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 => {
|
48 => {
|
||||||
// must have another parameter
|
// must have another parameter
|
||||||
const kind = iter.next() orelse return;
|
const kind = iter.next() orelse return;
|
||||||
switch (kind.val) {
|
switch (kind) {
|
||||||
2 => { // rgb
|
2 => { // rgb
|
||||||
const r = r: {
|
const r = r: {
|
||||||
// First param can be empty
|
// First param can be empty
|
||||||
var ps_r = iter.next() orelse return;
|
var ps_r = iter.next() orelse return;
|
||||||
while (ps_r.is_empty) {
|
if (iter.is_empty)
|
||||||
ps_r = iter.next() orelse return;
|
ps_r = iter.next() orelse return;
|
||||||
}
|
break :r ps_r;
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
const g = iter.next() orelse return;
|
||||||
|
const b = iter.next() orelse return;
|
||||||
self.cursor.style.bg = .{ .rgb = .{ r, g, b } };
|
self.cursor.style.bg = .{ .rgb = .{ r, g, b } };
|
||||||
},
|
},
|
||||||
5 => { // index
|
5 => {
|
||||||
const idx = iter.next() orelse return;
|
const idx = iter.next() orelse return;
|
||||||
self.cursor.style.bg = .{ .index = idx.val };
|
self.cursor.style.bg = .{ .index = idx };
|
||||||
},
|
}, // index
|
||||||
else => return,
|
else => return,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
49 => self.cursor.style.bg = .default,
|
49 => self.cursor.style.bg = .default,
|
||||||
90...97 => self.cursor.style.fg = .{ .index = ps.val - 90 + 8 },
|
90...97 => self.cursor.style.fg = .{ .index = ps - 90 + 8 },
|
||||||
100...107 => self.cursor.style.bg = .{ .index = ps.val - 100 + 8 },
|
100...107 => self.cursor.style.bg = .{ .index = ps - 100 + 8 },
|
||||||
else => continue,
|
else => continue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ const Terminal = @This();
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
const ansi = @import("ansi.zig");
|
||||||
pub const Command = @import("Command.zig");
|
pub const Command = @import("Command.zig");
|
||||||
const Parser = @import("Parser.zig");
|
const Parser = @import("Parser.zig");
|
||||||
const Pty = @import("Pty.zig");
|
const Pty = @import("Pty.zig");
|
||||||
|
@ -185,39 +186,24 @@ fn run(self: *Terminal) !void {
|
||||||
},
|
},
|
||||||
.c0 => |b| try self.handleC0(b),
|
.c0 => |b| try self.handleC0(b),
|
||||||
.csi => |seq| {
|
.csi => |seq| {
|
||||||
const final = seq[seq.len - 1];
|
switch (seq.final) {
|
||||||
switch (final) {
|
|
||||||
'B' => { // CUD
|
'B' => { // CUD
|
||||||
switch (seq.len) {
|
var iter = seq.iterator(u16);
|
||||||
0 => unreachable,
|
const delta = iter.next() orelse 1;
|
||||||
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);
|
self.back_screen.cursor.row = @min(self.back_screen.height - 1, self.back_screen.cursor.row + delta);
|
||||||
},
|
},
|
||||||
}
|
|
||||||
},
|
|
||||||
'H' => { // CUP
|
'H' => { // CUP
|
||||||
const delim = std.mem.indexOfScalar(u8, seq, ';') orelse {
|
var iter = seq.iterator(u16);
|
||||||
switch (seq.len) {
|
const row = iter.next() orelse 1;
|
||||||
0 => unreachable,
|
const col = iter.next() orelse 1;
|
||||||
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;
|
|
||||||
self.back_screen.cursor.col = col - 1;
|
self.back_screen.cursor.col = col - 1;
|
||||||
self.back_screen.cursor.row = row - 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 => {},
|
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) {
|
switch (b) {
|
||||||
0x00, 0x01, 0x02 => {}, // NUL, SOH, STX
|
.NUL, .SOH, .STX => {},
|
||||||
0x05 => {}, // ENQ
|
.ENQ => {},
|
||||||
0x07 => self.pending_events.bell.store(true, .unordered), // BEL
|
.BEL => self.pending_events.bell.store(true, .unordered),
|
||||||
0x08 => self.back_screen.cursorLeft(1), // BS
|
.BS => self.back_screen.cursorLeft(1),
|
||||||
0x09 => {}, // TODO: HT
|
.HT => {}, // TODO: HT
|
||||||
0x0a, 0x0b, 0x0c => try self.back_screen.index(), // LF, VT, FF
|
.LF, .VT, .FF => try self.back_screen.index(),
|
||||||
0x0d => { // CR
|
.CR => {
|
||||||
self.back_screen.cursor.pending_wrap = false;
|
self.back_screen.cursor.pending_wrap = false;
|
||||||
self.back_screen.cursor.col = if (self.mode.origin)
|
self.back_screen.cursor.col = if (self.mode.origin)
|
||||||
self.back_screen.scrolling_region.left
|
self.back_screen.scrolling_region.left
|
||||||
|
@ -243,14 +229,8 @@ inline fn handleC0(self: *Terminal, b: u8) !void {
|
||||||
else
|
else
|
||||||
0;
|
0;
|
||||||
},
|
},
|
||||||
0x0e => {}, // TODO: Charset shift out
|
.SO => {}, // TODO: Charset shift out
|
||||||
0x0f => {}, // TODO: Charset shift in
|
.SI => {}, // TODO: Charset shift in
|
||||||
else => log.warn("unhandled C0: 0x{x}", .{b}),
|
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;
|
|
||||||
}
|
|
||||||
|
|
107
src/widgets/terminal/ansi.zig
Normal file
107
src/widgets/terminal/ansi.zig
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue