render: add cli renderer

Add a renderer for applications running on the primary screen. This
renderer uses relative cursor positioning, and works with all the same
primitives as the alternate screen.

Fix a bug where repositioning was never turned back to false. This was a
nasty one with huge perf implications.

Set internal cells to a space and default on init. This prevents us from
writing them. As a result, we now issue a hardware clear on resize.
This commit is contained in:
Tim Culverhouse 2024-05-02 12:50:33 -05:00
parent e43f0907ec
commit a0db41f87c
6 changed files with 207 additions and 20 deletions

View file

@ -38,6 +38,7 @@ pub fn build(b: *std.Build) void {
// Examples
const Example = enum {
cli,
image,
main,
nvim,

106
examples/cli.zig Normal file
View file

@ -0,0 +1,106 @@
const std = @import("std");
const vaxis = @import("vaxis");
const Cell = vaxis.Cell;
const TextInput = vaxis.widgets.TextInput;
const log = std.log.scoped(.main);
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const deinit_status = gpa.deinit();
if (deinit_status == .leak) {
log.err("memory leak", .{});
}
}
const alloc = gpa.allocator();
var vx = try vaxis.init(alloc, .{});
defer vx.deinit(alloc);
var loop: vaxis.Loop(Event) = .{ .vaxis = &vx };
try loop.run();
defer loop.stop();
try vx.queryTerminal();
var text_input = TextInput.init(alloc, &vx.unicode);
defer text_input.deinit();
var selected_option: ?usize = null;
const options = [_][]const u8{
"option 1",
"option 2",
"option 3",
};
// The main event loop. Vaxis provides a thread safe, blocking, buffered
// queue which can serve as the primary event queue for an application
while (true) {
// nextEvent blocks until an event is in the queue
const event = loop.nextEvent();
// exhaustive switching ftw. Vaxis will send events if your Event
// enum has the fields for those events (ie "key_press", "winsize")
switch (event) {
.key_press => |key| {
if (key.codepoint == 'c' and key.mods.ctrl) {
break;
} else if (key.matches(vaxis.Key.tab, .{})) {
if (selected_option == null) {
selected_option = 0;
} else {
selected_option.? = @min(options.len - 1, selected_option.? + 1);
}
} else if (key.matches(vaxis.Key.tab, .{ .shift = true })) {
if (selected_option == null) {
selected_option = 0;
} else {
selected_option.? = selected_option.? -| 1;
}
} else if (key.matches(vaxis.Key.enter, .{})) {
if (selected_option) |i| {
log.err("enter", .{});
try text_input.insertSliceAtCursor(options[i]);
selected_option = null;
}
} else {
if (selected_option == null)
try text_input.update(.{ .key_press = key });
}
},
.winsize => |ws| {
try vx.resize(alloc, ws);
},
else => {},
}
const win = vx.window();
win.clear();
text_input.draw(win);
if (selected_option) |i| {
win.hideCursor();
for (options, 0..) |opt, j| {
log.err("i = {d}, j = {d}, opt = {s}", .{ i, j, opt });
var seg = [_]vaxis.Segment{.{
.text = opt,
.style = if (j == i) .{ .reverse = true } else .{},
}};
_ = try win.print(&seg, .{ .row_offset = j + 1 });
}
}
try vx.render();
}
}
// Our Event. 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,
focus_in,
foo: u8,
};

View file

@ -16,7 +16,7 @@ pub const InternalCell = struct {
uri_id: std.ArrayList(u8) = undefined,
// if we got skipped because of a wide character
skipped: bool = false,
default: bool = false,
default: bool = true,
pub fn eql(self: InternalCell, cell: Cell) bool {
// fastpath when both cells are default
@ -55,6 +55,7 @@ pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !InternalScreen {
.uri = std.ArrayList(u8).init(alloc),
.uri_id = std.ArrayList(u8).init(alloc),
};
try screen.buf[i].char.append(' ');
}
screen.width = w;
screen.height = h;

View file

@ -32,6 +32,10 @@ state: struct {
kitty_keyboard: bool = false,
bracketed_paste: bool = false,
mouse: bool = false,
cursor: struct {
row: usize = 0,
col: usize = 0,
} = .{},
} = .{},
/// initializes a Tty instance by opening /dev/tty and "making it raw"

View file

@ -94,7 +94,8 @@ pub fn deinit(self: *Vaxis, alloc: ?std.mem.Allocator) void {
/// resize allocates a slice of cells equal to the number of cells
/// required to display the screen (ie width x height). Any previous screen is
/// freed when resizing
/// freed when resizing. The cursor will be sent to it's home position and a
/// hardware clear-below-cursor will be sent
pub fn resize(self: *Vaxis, alloc: std.mem.Allocator, winsize: Winsize) !void {
log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows });
self.screen.deinit(alloc);
@ -105,7 +106,18 @@ pub fn resize(self: *Vaxis, alloc: std.mem.Allocator, winsize: Winsize) !void {
// every cell
self.screen_last.deinit(alloc);
self.screen_last = try InternalScreen.init(alloc, winsize.cols, winsize.rows);
// try self.screen_last.resize(alloc, winsize.cols, winsize.rows);
var tty = self.tty orelse return;
if (tty.state.alt_screen)
_ = try tty.write(ctlseqs.home)
else {
_ = try tty.buffered_writer.write("\r");
var i: usize = 0;
while (i < tty.state.cursor.row) : (i += 1) {
_ = try tty.buffered_writer.write(ctlseqs.ri);
}
}
_ = try tty.write(ctlseqs.erase_below_cursor);
try tty.flush();
}
/// returns a Window comprising of the entire terminal screen
@ -216,7 +228,15 @@ pub fn render(self: *Vaxis) !void {
// 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);
if (tty.state.alt_screen)
_ = try tty.write(ctlseqs.home)
else {
_ = try tty.write("\r");
var i: usize = 0;
while (i < tty.state.cursor.row) : (i += 1) {
_ = try tty.write(ctlseqs.ri);
}
}
_ = try tty.write(ctlseqs.sgr_reset);
// initialize some variables
@ -225,6 +245,10 @@ pub fn render(self: *Vaxis) !void {
var col: usize = 0;
var cursor: Style = .{};
var link: Hyperlink = .{};
var cursor_pos: struct {
row: usize = 0,
col: usize = 0,
} = .{};
// Clear all images
_ = try tty.write(ctlseqs.kitty_graphics_clear);
@ -232,15 +256,15 @@ pub fn render(self: *Vaxis) !void {
var i: usize = 0;
while (i < self.screen.buf.len) {
const cell = self.screen.buf[i];
const w = blk: {
if (cell.char.width != 0) break :blk cell.char.width;
const method: gwidth.Method = self.caps.unicode;
const width = gwidth.gwidth(cell.char.grapheme, method, &self.unicode.width_data) catch 1;
break :blk @max(1, width);
};
defer {
// advance by the width of this char mod 1
const w = blk: {
if (cell.char.width != 0) break :blk cell.char.width;
const method: gwidth.Method = self.caps.unicode;
const width = gwidth.gwidth(cell.char.grapheme, method, &self.unicode.width_data) catch 1;
break :blk @max(1, width);
};
std.debug.assert(w > 0);
var j = i + 1;
while (j < i + w) : (j += 1) {
@ -277,7 +301,24 @@ pub fn render(self: *Vaxis) !void {
// reposition the cursor, if needed
if (reposition) {
try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cup, .{ row + 1, col + 1 });
reposition = false;
if (tty.state.alt_screen)
try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cup, .{ row + 1, col + 1 })
else {
if (cursor_pos.row == row) {
const n = col - cursor_pos.col;
try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cuf, .{n});
} else {
const n = row - cursor_pos.row;
var _i: usize = 0;
_ = try tty.buffered_writer.write("\r");
while (_i < n) : (_i += 1) {
_ = try tty.buffered_writer.write("\n");
}
if (col > 0)
try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cuf, .{col});
}
}
}
if (cell.image) |img| {
@ -433,17 +474,43 @@ pub fn render(self: *Vaxis) !void {
try std.fmt.format(writer, ctlseqs.osc8, .{ ps, cell.link.uri });
}
_ = try tty.write(cell.char.grapheme);
cursor_pos.col = col + w;
cursor_pos.row = row;
log.debug("cursor: row: {}, col: {}, grapheme: '{s}'", .{ cursor_pos.row, cursor_pos.col, cell.char.grapheme });
}
if (self.screen.cursor_vis) {
try std.fmt.format(
tty.buffered_writer.writer(),
ctlseqs.cup,
.{
self.screen.cursor_row + 1,
self.screen.cursor_col + 1,
},
);
if (tty.state.alt_screen) {
try std.fmt.format(
tty.buffered_writer.writer(),
ctlseqs.cup,
.{
self.screen.cursor_row + 1,
self.screen.cursor_col + 1,
},
);
} else {
// TODO: position cursor relative to current location
_ = try tty.write("\r");
var r: usize = 0;
if (self.screen.cursor_row >= cursor_pos.row) {
while (r < (self.screen.cursor_row - cursor_pos.row)) : (r += 1) {
_ = try tty.write("\n");
}
} else {
while (r < (cursor_pos.row - self.screen.cursor_row)) : (r += 1) {
_ = try tty.write(ctlseqs.ri);
}
}
if (self.screen.cursor_col > 0) {
try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cuf, .{self.screen.cursor_col});
}
}
self.tty.?.state.cursor.row = self.screen.cursor_row;
self.tty.?.state.cursor.col = self.screen.cursor_col;
_ = try tty.write(ctlseqs.show_cursor);
} else {
self.tty.?.state.cursor.row = cursor_pos.row;
self.tty.?.state.cursor.col = cursor_pos.col;
}
if (self.screen.mouse_shape != self.screen_last.mouse_shape) {
try std.fmt.format(
@ -461,6 +528,7 @@ pub fn render(self: *Vaxis) !void {
);
self.screen_last.cursor_shape = self.screen.cursor_shape;
}
log.debug("tty_cursor: row: {}, col: {}", .{ self.tty.?.state.cursor.row, self.tty.?.state.cursor.col });
}
fn enableKittyKeyboard(self: *Vaxis, flags: Key.KittyFlags) !void {

View file

@ -37,6 +37,13 @@ pub const cup = "\x1b[{d};{d}H";
pub const hide_cursor = "\x1b[?25l";
pub const show_cursor = "\x1b[?25h";
pub const cursor_shape = "\x1b[{d} q";
pub const ri = "\x1bM";
pub const ind = "\n";
pub const cuf = "\x1b[{d}C";
pub const cub = "\x1b[{d}D";
// Erase
pub const erase_below_cursor = "\x1b[J";
// alt screen
pub const smcup = "\x1b[?1049h";