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
|
||||
const Example = enum {
|
||||
cli,
|
||||
image,
|
||||
main,
|
||||
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,
|
||||
// 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;
|
||||
|
|
|
@ -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"
|
||||
|
|
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
|
||||
/// 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 {
|
||||
|
|
|
@ -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";
|
||||
|
|
Loading…
Reference in a new issue