render: complete the render loop

This loop matches the go version of Vaxis to a tee. :chefs-kiss:

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
This commit is contained in:
Tim Culverhouse 2024-01-19 19:43:35 -06:00
parent 13e9dadbb1
commit de91cbe2d0
4 changed files with 219 additions and 3 deletions

View file

@ -22,12 +22,18 @@ pub fn main() !void {
try vx.enterAltScreen(); try vx.enterAltScreen();
var color_idx: u8 = 0;
const msg = "Hello, world!"; const msg = "Hello, world!";
outer: while (true) { outer: while (true) {
const event = vx.nextEvent(); const event = vx.nextEvent();
log.debug("event: {}\r\n", .{event}); log.debug("event: {}\r\n", .{event});
switch (event) { switch (event) {
.key_press => |key| { .key_press => |key| {
if (color_idx == 255) {
color_idx = 0;
} else {
color_idx += 1;
}
if (key.codepoint == 'c' and key.mods.ctrl) { if (key.codepoint == 'c' and key.mods.ctrl) {
break :outer; break :outer;
} }
@ -42,7 +48,10 @@ pub fn main() !void {
win.clear(); win.clear();
const child = win.initChild(win.width / 2 - msg.len / 2, win.height / 2, .expand, .expand); const child = win.initChild(win.width / 2 - msg.len / 2, win.height / 2, .expand, .expand);
for (msg, 0..) |_, i| { for (msg, 0..) |_, i| {
const cell: Cell = .{ .char = .{ .grapheme = msg[i .. i + 1] } }; const cell: Cell = .{
.char = .{ .grapheme = msg[i .. i + 1] },
.style = .{ .fg = .{ .index = color_idx } },
};
child.writeCell(i, 0, cell); child.writeCell(i, 0, cell);
} }
try vx.render(); try vx.render();

View file

@ -24,6 +24,14 @@ pub const Style = struct {
ul_style: Underline = .off, ul_style: Underline = .off,
url: ?[]const u8 = null, url: ?[]const u8 = null,
url_params: ?[]const u8 = null, url_params: ?[]const u8 = null,
bold: bool = false,
dim: bool = false,
italic: bool = false,
blink: bool = false,
reverse: bool = false,
invisible: bool = false,
strikethrough: bool = false,
}; };
pub const Color = union(enum) { pub const Color = union(enum) {

View file

@ -11,7 +11,52 @@ pub const csi_u_pop = "\x1b[<u";
// Cursor // Cursor
pub const home = "\x1b[H"; pub const home = "\x1b[H";
pub const cup = "\x1b[{d};{d}H"; pub const cup = "\x1b[{d};{d}H";
pub const hide_cursor = "\x1b[?25l";
pub const show_cursor = "\x1b[?25h";
// alt screen // alt screen
pub const smcup = "\x1b[?1049h"; pub const smcup = "\x1b[?1049h";
pub const rmcup = "\x1b[?1049l"; pub const rmcup = "\x1b[?1049l";
// colors
pub const fg_base = "\x1b[3{d}m";
pub const fg_bright = "\x1b[9{d}m";
pub const bg_base = "\x1b[4{d}m";
pub const bg_bright = "\x1b[10{d}m";
pub const fg_reset = "\x1b[39m";
pub const bg_reset = "\x1b[49m";
pub const ul_reset = "\x1b[59m";
pub const fg_indexed = "\x1b[38;5;{d}m";
pub const bg_indexed = "\x1b[48:5:{d}m";
pub const ul_indexed = "\x1b[58:5:{d}m";
pub const fg_rgb = "\x1b[38:2:{d}:{d}:{d}m";
pub const bg_rgb = "\x1b[48:2:{d}:{d}:{d}m";
pub const ul_rgb = "\x1b[58:2:{d}:{d}:{d}m";
// Underlines
pub const ul_off = "\x1b[24m"; // NOTE: this could be \x1b[4:0m but is not as widely supported
pub const ul_single = "\x1b[4m";
pub const ul_double = "\x1b[4:2m";
pub const ul_curly = "\x1b[4:3m";
pub const ul_dotted = "\x1b[4:4m";
pub const ul_dashed = "\x1b[4:5m";
// Attributes
pub const bold_set = "\x1b[1m";
pub const dim_set = "\x1b[2m";
pub const italic_set = "\x1b[3m";
pub const blink_set = "\x1b[5m";
pub const reverse_set = "\x1b[7m";
pub const invisible_set = "\x1b[8m";
pub const strikethrough_set = "\x1b[9m";
pub const bold_dim_reset = "\x1b[22m";
pub const italic_reset = "\x1b[23m";
pub const blink_reset = "\x1b[25m";
pub const reverse_reset = "\x1b[27m";
pub const invisible_reset = "\x1b[28m";
pub const strikethrough_reset = "\x1b[29m";
// OSC sequences
pub const osc8 = "\x1b]8;{s};{s}\x1b\\";
pub const osc8_clear = "\x1b]8;;\x1b\\";

View file

@ -8,6 +8,7 @@ const Key = @import("Key.zig");
const Screen = @import("Screen.zig"); const Screen = @import("Screen.zig");
const Window = @import("Window.zig"); const Window = @import("Window.zig");
const Options = @import("Options.zig"); const Options = @import("Options.zig");
const Style = @import("cell.zig").Style;
/// Vaxis is the entrypoint for a Vaxis application. The provided type T should /// Vaxis is the entrypoint for a Vaxis application. The provided type T should
/// be a tagged union which contains all of the events the application will /// be a tagged union which contains all of the events the application will
@ -135,18 +136,30 @@ pub fn Vaxis(comptime T: type) type {
self.alt_screen = false; self.alt_screen = false;
} }
/// draws the screen to the terminal
pub fn render(self: *Self) !void { pub fn render(self: *Self) !void {
var tty = self.tty orelse return; var tty = self.tty orelse return;
// TODO: optimize writes // TODO: optimize writes
// Send the cursor to 0,0 // Send the cursor to 0,0
// TODO: this needs to move after we optimize writes. We only do
// this if we have an update to make. We also need to hide cursor
// and then reshow it if needed
_ = try tty.write(ctlseqs.hide_cursor);
_ = try tty.write(ctlseqs.home); _ = try tty.write(ctlseqs.home);
// initialize some variables
var reposition: bool = false; var reposition: bool = false;
var row: usize = 0; var row: usize = 0;
var col: usize = 0; var col: usize = 0;
for (self.screen.buf, 0..) |cell, i| { var cursor: Style = .{};
col += 1;
var i: usize = 0;
while (i < self.screen.buf.len) : (i += 1) {
const cell = self.screen.buf[i];
defer col += 1;
defer cursor = cell.style;
if (col == self.screen.width) { if (col == self.screen.width) {
row += 1; row += 1;
col = 0; col = 0;
@ -155,13 +168,154 @@ pub fn Vaxis(comptime T: type) type {
// anything // anything
if (std.meta.eql(cell, self.screen_last.buf[i])) { if (std.meta.eql(cell, self.screen_last.buf[i])) {
reposition = true; reposition = true;
// Close any osc8 sequence we might be in before
// repositioning
if (cursor.url) |_| {
_ = try tty.write(ctlseqs.osc8_clear);
}
continue; continue;
} }
// Set this cell in the last frame // Set this cell in the last frame
self.screen_last.buf[i] = cell; self.screen_last.buf[i] = cell;
// reposition the cursor, if needed
if (reposition) { if (reposition) {
try std.fmt.format(tty.writer(), ctlseqs.cup, .{ row + 1, col + 1 }); try std.fmt.format(tty.writer(), ctlseqs.cup, .{ row + 1, col + 1 });
} }
// something is different, so let's loop throuugh everything and
// find out what
// foreground
if (!std.meta.eql(cursor.fg, cell.style.fg)) {
switch (cell.style.fg) {
.default => _ = try tty.write(ctlseqs.fg_reset),
.index => |idx| {
switch (idx) {
0...7 => try std.fmt.format(tty.writer(), ctlseqs.fg_base, .{idx}),
8...15 => try std.fmt.format(tty.writer(), ctlseqs.fg_bright, .{idx}),
else => try std.fmt.format(tty.writer(), ctlseqs.fg_indexed, .{idx}),
}
},
.rgb => |rgb| {
try std.fmt.format(tty.writer(), ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] });
},
}
}
// background
if (!std.meta.eql(cursor.bg, cell.style.bg)) {
switch (cell.style.bg) {
.default => _ = try tty.write(ctlseqs.bg_reset),
.index => |idx| {
switch (idx) {
0...7 => try std.fmt.format(tty.writer(), ctlseqs.bg_base, .{idx}),
8...15 => try std.fmt.format(tty.writer(), ctlseqs.bg_bright, .{idx}),
else => try std.fmt.format(tty.writer(), ctlseqs.bg_indexed, .{idx}),
}
},
.rgb => |rgb| {
try std.fmt.format(tty.writer(), ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] });
},
}
}
// underline color
if (!std.meta.eql(cursor.ul, cell.style.ul)) {
switch (cell.style.bg) {
.default => _ = try tty.write(ctlseqs.ul_reset),
.index => |idx| {
try std.fmt.format(tty.writer(), ctlseqs.ul_indexed, .{idx});
},
.rgb => |rgb| {
try std.fmt.format(tty.writer(), ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] });
},
}
}
// underline style
if (!std.meta.eql(cursor.ul_style, cell.style.ul_style)) {
const seq = switch (cell.style.ul_style) {
.off => ctlseqs.ul_off,
.single => ctlseqs.ul_single,
.double => ctlseqs.ul_double,
.curly => ctlseqs.ul_curly,
.dotted => ctlseqs.ul_dotted,
.dashed => ctlseqs.ul_dashed,
};
_ = try tty.write(seq);
}
// bold
if (cursor.bold != cell.style.bold) {
const seq = switch (cell.style.bold) {
true => ctlseqs.bold_set,
false => ctlseqs.bold_dim_reset,
};
_ = try tty.write(seq);
if (cell.style.dim) {
_ = try tty.write(ctlseqs.dim_set);
}
}
// dim
if (cursor.dim != cell.style.dim) {
const seq = switch (cell.style.dim) {
true => ctlseqs.dim_set,
false => ctlseqs.bold_dim_reset,
};
_ = try tty.write(seq);
if (cell.style.bold) {
_ = try tty.write(ctlseqs.bold_set);
}
}
// dim
if (cursor.italic != cell.style.italic) {
const seq = switch (cell.style.italic) {
true => ctlseqs.italic_set,
false => ctlseqs.italic_reset,
};
_ = try tty.write(seq);
}
// dim
if (cursor.blink != cell.style.blink) {
const seq = switch (cell.style.blink) {
true => ctlseqs.blink_set,
false => ctlseqs.blink_reset,
};
_ = try tty.write(seq);
}
// reverse
if (cursor.reverse != cell.style.reverse) {
const seq = switch (cell.style.reverse) {
true => ctlseqs.reverse_set,
false => ctlseqs.reverse_reset,
};
_ = try tty.write(seq);
}
// invisible
if (cursor.invisible != cell.style.invisible) {
const seq = switch (cell.style.invisible) {
true => ctlseqs.invisible_set,
false => ctlseqs.invisible_reset,
};
_ = try tty.write(seq);
}
// strikethrough
if (cursor.strikethrough != cell.style.strikethrough) {
const seq = switch (cell.style.strikethrough) {
true => ctlseqs.strikethrough_set,
false => ctlseqs.strikethrough_reset,
};
_ = try tty.write(seq);
}
// url
if (!std.meta.eql(cursor.url, cell.style.url)) {
const url = cell.style.url orelse "";
var ps = cell.style.url_params orelse "";
if (url.len == 0) {
// Empty out the params no matter what if we don't have
// a url
ps = "";
}
try std.fmt.format(tty.writer(), ctlseqs.osc8, .{ ps, url });
}
_ = try tty.write(cell.char.grapheme); _ = try tty.write(cell.char.grapheme);
} }
} }