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:
parent
e43f0907ec
commit
a0db41f87c
6 changed files with 207 additions and 20 deletions
|
@ -38,6 +38,7 @@ pub fn build(b: *std.Build) void {
|
||||||
|
|
||||||
// Examples
|
// Examples
|
||||||
const Example = enum {
|
const Example = enum {
|
||||||
|
cli,
|
||||||
image,
|
image,
|
||||||
main,
|
main,
|
||||||
nvim,
|
nvim,
|
||||||
|
|
106
examples/cli.zig
Normal file
106
examples/cli.zig
Normal 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,
|
||||||
|
};
|
|
@ -16,7 +16,7 @@ pub const InternalCell = struct {
|
||||||
uri_id: std.ArrayList(u8) = undefined,
|
uri_id: std.ArrayList(u8) = undefined,
|
||||||
// if we got skipped because of a wide character
|
// if we got skipped because of a wide character
|
||||||
skipped: bool = false,
|
skipped: bool = false,
|
||||||
default: bool = false,
|
default: bool = true,
|
||||||
|
|
||||||
pub fn eql(self: InternalCell, cell: Cell) bool {
|
pub fn eql(self: InternalCell, cell: Cell) bool {
|
||||||
// fastpath when both cells are default
|
// 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 = std.ArrayList(u8).init(alloc),
|
||||||
.uri_id = std.ArrayList(u8).init(alloc),
|
.uri_id = std.ArrayList(u8).init(alloc),
|
||||||
};
|
};
|
||||||
|
try screen.buf[i].char.append(' ');
|
||||||
}
|
}
|
||||||
screen.width = w;
|
screen.width = w;
|
||||||
screen.height = h;
|
screen.height = h;
|
||||||
|
|
|
@ -32,6 +32,10 @@ state: struct {
|
||||||
kitty_keyboard: bool = false,
|
kitty_keyboard: bool = false,
|
||||||
bracketed_paste: bool = false,
|
bracketed_paste: bool = false,
|
||||||
mouse: 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"
|
/// initializes a Tty instance by opening /dev/tty and "making it raw"
|
||||||
|
|
106
src/Vaxis.zig
106
src/Vaxis.zig
|
@ -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
|
/// 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
|
/// 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 {
|
pub fn resize(self: *Vaxis, alloc: std.mem.Allocator, winsize: Winsize) !void {
|
||||||
log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows });
|
log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows });
|
||||||
self.screen.deinit(alloc);
|
self.screen.deinit(alloc);
|
||||||
|
@ -105,7 +106,18 @@ pub fn resize(self: *Vaxis, alloc: std.mem.Allocator, winsize: Winsize) !void {
|
||||||
// every cell
|
// every cell
|
||||||
self.screen_last.deinit(alloc);
|
self.screen_last.deinit(alloc);
|
||||||
self.screen_last = try InternalScreen.init(alloc, winsize.cols, winsize.rows);
|
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
|
/// 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
|
// this if we have an update to make. We also need to hide cursor
|
||||||
// and then reshow it if needed
|
// and then reshow it if needed
|
||||||
_ = try tty.write(ctlseqs.hide_cursor);
|
_ = 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);
|
_ = try tty.write(ctlseqs.sgr_reset);
|
||||||
|
|
||||||
// initialize some variables
|
// initialize some variables
|
||||||
|
@ -225,6 +245,10 @@ pub fn render(self: *Vaxis) !void {
|
||||||
var col: usize = 0;
|
var col: usize = 0;
|
||||||
var cursor: Style = .{};
|
var cursor: Style = .{};
|
||||||
var link: Hyperlink = .{};
|
var link: Hyperlink = .{};
|
||||||
|
var cursor_pos: struct {
|
||||||
|
row: usize = 0,
|
||||||
|
col: usize = 0,
|
||||||
|
} = .{};
|
||||||
|
|
||||||
// Clear all images
|
// Clear all images
|
||||||
_ = try tty.write(ctlseqs.kitty_graphics_clear);
|
_ = try tty.write(ctlseqs.kitty_graphics_clear);
|
||||||
|
@ -232,15 +256,15 @@ pub fn render(self: *Vaxis) !void {
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
while (i < self.screen.buf.len) {
|
while (i < self.screen.buf.len) {
|
||||||
const cell = self.screen.buf[i];
|
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 {
|
defer {
|
||||||
// advance by the width of this char mod 1
|
// 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);
|
std.debug.assert(w > 0);
|
||||||
var j = i + 1;
|
var j = i + 1;
|
||||||
while (j < i + w) : (j += 1) {
|
while (j < i + w) : (j += 1) {
|
||||||
|
@ -277,7 +301,24 @@ pub fn render(self: *Vaxis) !void {
|
||||||
|
|
||||||
// reposition the cursor, if needed
|
// reposition the cursor, if needed
|
||||||
if (reposition) {
|
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| {
|
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 std.fmt.format(writer, ctlseqs.osc8, .{ ps, cell.link.uri });
|
||||||
}
|
}
|
||||||
_ = try tty.write(cell.char.grapheme);
|
_ = 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) {
|
if (self.screen.cursor_vis) {
|
||||||
try std.fmt.format(
|
if (tty.state.alt_screen) {
|
||||||
tty.buffered_writer.writer(),
|
try std.fmt.format(
|
||||||
ctlseqs.cup,
|
tty.buffered_writer.writer(),
|
||||||
.{
|
ctlseqs.cup,
|
||||||
self.screen.cursor_row + 1,
|
.{
|
||||||
self.screen.cursor_col + 1,
|
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);
|
_ = 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) {
|
if (self.screen.mouse_shape != self.screen_last.mouse_shape) {
|
||||||
try std.fmt.format(
|
try std.fmt.format(
|
||||||
|
@ -461,6 +528,7 @@ pub fn render(self: *Vaxis) !void {
|
||||||
);
|
);
|
||||||
self.screen_last.cursor_shape = self.screen.cursor_shape;
|
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 {
|
fn enableKittyKeyboard(self: *Vaxis, flags: Key.KittyFlags) !void {
|
||||||
|
|
|
@ -37,6 +37,13 @@ pub const cup = "\x1b[{d};{d}H";
|
||||||
pub const hide_cursor = "\x1b[?25l";
|
pub const hide_cursor = "\x1b[?25l";
|
||||||
pub const show_cursor = "\x1b[?25h";
|
pub const show_cursor = "\x1b[?25h";
|
||||||
pub const cursor_shape = "\x1b[{d} q";
|
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
|
// alt screen
|
||||||
pub const smcup = "\x1b[?1049h";
|
pub const smcup = "\x1b[?1049h";
|
||||||
|
|
Loading…
Reference in a new issue