diff --git a/examples/main.zig b/examples/main.zig index 5abbf5f..2340d8e 100644 --- a/examples/main.zig +++ b/examples/main.zig @@ -14,26 +14,36 @@ pub fn main() !void { } const alloc = gpa.allocator(); + // Initialize Vaxis var vx = try vaxis.init(Event, .{}); defer vx.deinit(alloc); + // Start the read loop. This puts the terminal in raw mode and begins + // reading user input try vx.start(); defer vx.stop(); + // Optionally enter the alternate screen try vx.enterAltScreen(); + // We'll adjust the color index every keypress var color_idx: u8 = 0; const msg = "Hello, world!"; + + // The main event loop. Vaxis provides a thread safe, blocking, buffered + // queue which can serve as the primary event queue for an application outer: while (true) { + // nextEvent blocks until an event is in the queue const event = vx.nextEvent(); log.debug("event: {}\r\n", .{event}); + // exhaustive switching ftw. Vaxis will send events if your EventType + // enum has the fields for those events (ie "key_press", "winsize") switch (event) { .key_press => |key| { - if (color_idx == 255) { - color_idx = 0; - } else { - color_idx += 1; - } + color_idx = switch (color_idx) { + 255 => 0, + else => color_idx + 1, + }; if (key.codepoint == 'c' and key.mods.ctrl) { break :outer; } @@ -44,20 +54,42 @@ pub fn main() !void { else => {}, } + // vx.window() returns the root window. This window is the size of the + // terminal and can spawn child windows as logical areas. Child windows + // cannot draw outside of their bounds const win = vx.window(); + // Clear the entire space because we are drawing in immediate mode. + // vaxis double buffers the screen. This new frame will be compared to + // the old and only updated cells will be drawn win.clear(); + + // Create some child window. .expand means the height and width will + // fill the remaining space of the parent. Child windows do not store a + // reference to their parent: this is true immediate mode. Do not store + // windows, always create new windows each render cycle const child = win.initChild(win.width / 2 - msg.len / 2, win.height / 2, .expand, .expand); + // Loop through the message and print the cells to the screen for (msg, 0..) |_, i| { const cell: Cell = .{ + // each cell takes a _grapheme_ as opposed to a single + // codepoint. This allows Vaxis to handle emoji properly, + // particularly with terminals that the Unicode Core extension + // (IE Mode 2027) .char = .{ .grapheme = msg[i .. i + 1] }, - .style = .{ .fg = .{ .index = color_idx } }, + .style = .{ + .fg = .{ .index = color_idx }, + }, }; child.writeCell(i, 0, cell); } + // Render the screen try vx.render(); } } +// Our EventType. This can contain internal events as well as Vaxis events. +// Internal events can be posted into the same queue as vaxis events to allow +// for a single event loop with exhaustive switching. Booya const Event = union(enum) { key_press: vaxis.Key, winsize: vaxis.Winsize, diff --git a/src/Tty.zig b/src/Tty.zig index dc0bbe5..096ed7a 100644 --- a/src/Tty.zig +++ b/src/Tty.zig @@ -9,6 +9,10 @@ const log = std.log.scoped(.tty); const Tty = @This(); +const Writer = std.io.Writer(os.fd_t, os.WriteError, os.write); + +const BufferedWriter = std.io.BufferedWriter(4096, Writer); + /// the original state of the terminal, prior to calling makeRaw termios: os.termios, @@ -18,6 +22,8 @@ fd: os.fd_t, /// the write end of a pipe to signal the tty should exit it's run loop quit_fd: ?os.fd_t = null, +buffered_writer: BufferedWriter, + /// initializes a Tty instance by opening /dev/tty and "making it raw" pub fn init() !Tty { // Open our tty @@ -29,6 +35,7 @@ pub fn init() !Tty { return Tty{ .fd = fd, .termios = termios, + .buffered_writer = std.io.bufferedWriter(Writer{ .context = fd }), }; } @@ -173,16 +180,15 @@ pub fn run( } } -const Writer = std.io.Writer(os.fd_t, os.WriteError, os.write); - -pub fn writer(self: *Tty) Writer { - return .{ .context = self.fd }; -} -/// write to the tty -// -// TODO: buffer the writes +/// write to the tty. These writes are buffered and require calling flush to +/// flush writes to the tty pub fn write(self: *Tty, bytes: []const u8) !usize { - return os.write(self.fd, bytes); + return self.buffered_writer.write(bytes); +} + +/// flushes the write buffer to the tty +pub fn flush(self: *Tty) !void { + try self.buffered_writer.flush(); } /// makeRaw enters the raw state for the terminal. diff --git a/src/vaxis.zig b/src/vaxis.zig index 723b0b0..6eda74e 100644 --- a/src/vaxis.zig +++ b/src/vaxis.zig @@ -41,6 +41,10 @@ pub fn Vaxis(comptime T: type) type { alt_screen: bool, + // statistics + renders: usize = 0, + render_dur: i128 = 0, + /// Initialize Vaxis with runtime options pub fn init(_: Options) !Self { return Self{ @@ -61,6 +65,7 @@ pub fn Vaxis(comptime T: type) type { var tty = &self.tty.?; if (self.alt_screen) { _ = tty.write(ctlseqs.rmcup) catch {}; + tty.flush() catch {}; } tty.deinit(); } @@ -68,6 +73,11 @@ pub fn Vaxis(comptime T: type) type { self.screen.deinit(a); self.screen_last.deinit(a); } + if (self.renders > 0) { + const tpr = @divTrunc(self.render_dur, self.renders); + log.info("total renders = {d}", .{self.renders}); + log.info("microseconds per render = {d}", .{tpr}); + } } /// spawns the input thread to start listening to the tty for input @@ -126,6 +136,7 @@ pub fn Vaxis(comptime T: type) type { if (self.alt_screen) return; var tty = self.tty orelse return; _ = try tty.write(ctlseqs.smcup); + try tty.flush(); self.alt_screen = true; } @@ -134,14 +145,20 @@ pub fn Vaxis(comptime T: type) type { if (!self.alt_screen) return; var tty = self.tty orelse return; _ = try tty.write(ctlseqs.rmcup); + try tty.flush(); self.alt_screen = false; } /// draws the screen to the terminal pub fn render(self: *Self) !void { var tty = self.tty orelse return; + self.renders += 1; + const timer_start = std.time.microTimestamp(); + defer { + self.render_dur += std.time.microTimestamp() - timer_start; + } - // TODO: optimize writes + defer tty.flush() catch {}; // Send the cursor to 0,0 // TODO: this needs to move after we optimize writes. We only do @@ -181,7 +198,7 @@ pub fn Vaxis(comptime T: type) type { // reposition the cursor, if needed if (reposition) { - try std.fmt.format(tty.writer(), ctlseqs.cup, .{ row + 1, col + 1 }); + try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cup, .{ row + 1, col + 1 }); } // something is different, so let's loop throuugh everything and @@ -189,45 +206,48 @@ pub fn Vaxis(comptime T: type) type { // foreground if (!std.meta.eql(cursor.fg, cell.style.fg)) { + const writer = tty.buffered_writer.writer(); 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}), + 0...7 => try std.fmt.format(writer, ctlseqs.fg_base, .{idx}), + 8...15 => try std.fmt.format(writer, ctlseqs.fg_bright, .{idx}), + else => try std.fmt.format(writer, ctlseqs.fg_indexed, .{idx}), } }, .rgb => |rgb| { - try std.fmt.format(tty.writer(), ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }); + try std.fmt.format(writer, ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }); }, } } // background if (!std.meta.eql(cursor.bg, cell.style.bg)) { + const writer = tty.buffered_writer.writer(); 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}), + 0...7 => try std.fmt.format(writer, ctlseqs.bg_base, .{idx}), + 8...15 => try std.fmt.format(writer, ctlseqs.bg_bright, .{idx}), + else => try std.fmt.format(writer, ctlseqs.bg_indexed, .{idx}), } }, .rgb => |rgb| { - try std.fmt.format(tty.writer(), ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }); + try std.fmt.format(writer, ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }); }, } } // underline color if (!std.meta.eql(cursor.ul, cell.style.ul)) { + const writer = tty.buffered_writer.writer(); switch (cell.style.bg) { .default => _ = try tty.write(ctlseqs.ul_reset), .index => |idx| { - try std.fmt.format(tty.writer(), ctlseqs.ul_indexed, .{idx}); + try std.fmt.format(writer, ctlseqs.ul_indexed, .{idx}); }, .rgb => |rgb| { - try std.fmt.format(tty.writer(), ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }); + try std.fmt.format(writer, ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }); }, } } @@ -315,7 +335,8 @@ pub fn Vaxis(comptime T: type) type { // a url ps = ""; } - try std.fmt.format(tty.writer(), ctlseqs.osc8, .{ ps, url }); + const writer = tty.buffered_writer.writer(); + try std.fmt.format(writer, ctlseqs.osc8, .{ ps, url }); } _ = try tty.write(cell.char.grapheme); }