Skip to content

Commit

Permalink
Rewrite UI with libvaxis
Browse files Browse the repository at this point in the history
This should have better support and make it easier to add new features
in the future.
  • Loading branch information
natecraddock committed Sep 9, 2024
1 parent 91bc980 commit cbcfbd3
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 450 deletions.
4 changes: 2 additions & 2 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
.version = "0.9.2",
.dependencies = .{
.vaxis = .{
.url = "https://github.com/rockorager/libvaxis/archive/refs/tags/v0.3.0.tar.gz",
.hash = "1220fbb1b748ced787d6a19d3be8a28839e000548b76bae0f183a6e13504e6bc7b20",
.url = "git+https://github.com/rockorager/libvaxis?ref=v0.4.1#f0eaa3c831f6376e7fa0519c275cdcb764580e12",
.hash = "1220523a3a301cbfbd83a374e2a69073fa14b211f2f6aaa4fc9497c936feaa67f738",
},
},
.paths = .{""},
Expand Down
4 changes: 1 addition & 3 deletions src/Previewer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const Loop = @import("Loop.zig");
const Previewer = @This();

allocator: Allocator,
loop: *Loop,

shell: []const u8,
cmd_parts: [2][]const u8,
Expand All @@ -25,7 +24,7 @@ child: ?Child = null,
stdout: ArrayList(u8),
stderr: ArrayList(u8),

pub fn init(allocator: Allocator, loop: *Loop, cmd: []const u8, arg: []const u8) !Previewer {
pub fn init(allocator: Allocator, cmd: []const u8, arg: []const u8) !Previewer {
const shell = process.getEnvVarOwned(allocator, "SHELL") catch "/bin/sh";

var iter = std.mem.tokenizeSequence(u8, cmd, "{}");
Expand All @@ -36,7 +35,6 @@ pub fn init(allocator: Allocator, loop: *Loop, cmd: []const u8, arg: []const u8)

var previewer = Previewer{
.allocator = allocator,
.loop = loop,
.shell = shell,
.cmd_parts = cmd_parts,
.stdout = ArrayList(u8).init(allocator),
Expand Down
52 changes: 24 additions & 28 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const ArrayList = std.ArrayList;
const SGRAttribute = term.SGRAttribute;
const Terminal = term.Terminal;

pub const std_options = .{
.log_level = .err,
};

const eql = std.mem.eql;

pub fn main() anyerror!void {
Expand Down Expand Up @@ -61,11 +65,9 @@ pub fn main() anyerror!void {
}
} else {
const prompt_str = std.process.getEnvVarOwned(allocator, "ZF_PROMPT") catch "> ";
_ = prompt_str;
const vi_mode = if (std.process.getEnvVarOwned(allocator, "ZF_VI_MODE")) |value| blk: {
break :blk value.len > 0;
} else |_| false;
_ = vi_mode;
const no_color = if (std.process.getEnvVarOwned(allocator, "NO_COLOR")) |value| blk: {
break :blk value.len > 0;
} else |_| false;
Expand All @@ -80,32 +82,26 @@ pub fn main() anyerror!void {
} else |_| .cyan;
_ = highlight_color;

// var terminal = try Terminal.init(highlight_color, no_color);
// const selected = ui.run(
// allocator,
// &terminal,
// candidates,
// config.keep_order,
// config.plain,
// config.height,
// config.preview,
// config.preview_width,
// prompt_str,
// vi_mode,
// ) catch |err| switch (err) {
// error.UnknownANSIEscape => {
// try terminal.deinit(0);
// try stderr.print("zf: unknown ANSI escape sequence in ZF_PROMPT\n", .{});
// std.process.exit(2);
// },
// else => {
// try terminal.deinit(0);
// return err;
// },
// };

// try terminal.deinit(config.height);
const selected: ?[1][]const u8 = .{"hello"};
const selected = ui.run(
allocator,
candidates,
config.keep_order,
config.plain,
config.height,
config.preview,
config.preview_width,
prompt_str,
vi_mode,
) catch |err| switch (err) {
error.UnknownANSIEscape => {
try stderr.print("zf: unknown ANSI escape sequence in ZF_PROMPT\n", .{});
std.process.exit(2);
},
else => {
return err;
},
};

if (selected) |selected_lines| {
for (selected_lines) |str| {
try stdout.print("{s}\n", .{str});
Expand Down
247 changes: 0 additions & 247 deletions src/term.zig
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
const std = @import("std");
const system = std.os.system;
const ziglyph = @import("ziglyph");

const c = switch (@import("builtin").os.tag) {
.linux => std.os.linux,
else => std.c,
};

const File = std.fs.File;

// Select Graphic Rendition (SGR) attributes
pub const SGRAttribute = enum(u8) {
reset = 0,
Expand Down Expand Up @@ -48,239 +37,3 @@ pub const InputBuffer = union(enum) {
shift_tab,
none,
};

pub const Terminal = struct {
tty: File,
writer: File.Writer,
termios: std.posix.termios,
raw_termios: std.posix.termios,

width: usize = undefined,
height: usize = undefined,

no_color: bool,
highlight_color: SGRAttribute,

/// buffered writes to the terminal for performance
buffer: [4096]u8 = undefined,
index: usize = 0,

pub fn init(highlight_color: SGRAttribute, no_color: bool) !Terminal {
var tty = try std.fs.openFileAbsolute("/dev/tty", .{ .mode = .read_write });

// store original terminal settings to restore later
const termios = try std.posix.tcgetattr(tty.handle);
var raw = termios;

raw.iflag.IGNBRK = false;
raw.iflag.BRKINT = false;
raw.iflag.PARMRK = false;
raw.iflag.ISTRIP = false;
raw.iflag.INLCR = false;
raw.iflag.IGNCR = false;
raw.iflag.ICRNL = false;
raw.iflag.IXON = false;

raw.oflag.OPOST = false;

raw.lflag.ECHO = false;
raw.lflag.ECHONL = false;
raw.lflag.ICANON = false;
raw.lflag.ISIG = false;
raw.lflag.IEXTEN = false;

raw.cflag.CSIZE = .CS8;
raw.cflag.PARENB = false;

raw.cc[@intFromEnum(c.V.MIN)] = 0;

try std.posix.tcsetattr(tty.handle, .NOW, raw);

var term = Terminal{
.tty = tty,
.writer = tty.writer(),
.termios = termios,
.raw_termios = raw,
.highlight_color = highlight_color,
.no_color = no_color,
};
term.getSize();

return term;
}

pub fn deinit(self: *Terminal, max_height: usize) !void {
const height = @min(self.height, max_height);

var i: usize = 0;
while (i < height) : (i += 1) {
self.clearLine();
self.cursorDown(1);
}
self.clearLine();
self.cursorUp(height);

self.flush();

std.posix.tcsetattr(self.tty.handle, .NOW, self.termios) catch return;
self.tty.close();
}

/// Buffered write interface
pub fn write(self: *Terminal, bytes: []const u8) void {
if (self.index + bytes.len > self.buffer.len) {
self.flush();
if (bytes.len > self.buffer.len) self.writer.writeAll(bytes) catch unreachable;
}

const new_index = self.index + bytes.len;
@memcpy(self.buffer[self.index..new_index], bytes);
self.index = new_index;
}

/// Formatted write interface≈
pub fn print(self: *Terminal, comptime fmt: []const u8, args: anytype) void {
var buf: [512]u8 = undefined;
const bytes = std.fmt.bufPrint(&buf, fmt, args) catch unreachable;
self.write(bytes);
}

pub fn flush(self: *Terminal) void {
if (self.index == 0) return;
self.writer.writeAll(self.buffer[0..self.index]) catch unreachable;
self.index = 0;
}

fn escape(self: *Terminal, args: anytype) void {
self.print("\x1b[{d}{c}", args);
}

pub fn clearLine(self: *Terminal) void {
self.cursorCol(1);
self.escape(.{ 2, 'K' });
}

pub fn clearToEndOfLine(self: *Terminal) void {
self.escape(.{ 0, 'K' });
}

pub fn clearToEndOfDisplay(self: *Terminal) void {
self.escape(.{ 0, 'J' });
}

pub fn scrollDown(self: *Terminal, num: usize) void {
var i: usize = 0;
while (i < num) : (i += 1) {
self.write("\n");
}
}

pub fn cursorUp(self: *Terminal, num: usize) void {
self.escape(.{ num, 'A' });
}

pub fn cursorDown(self: *Terminal, num: usize) void {
self.escape(.{ num, 'B' });
}

pub fn cursorRight(self: *Terminal, num: usize) void {
if (num == 0) return;
self.escape(.{ num, 'C' });
}

pub fn cursorLeft(self: *Terminal, num: usize) void {
self.escape(.{ num, 'D' });
}

pub fn cursorCol(self: *Terminal, col: usize) void {
self.escape(.{ col, 'G' });
}

pub fn cursorVisible(self: *Terminal, show: bool) void {
if (show) {
self.write("\x1b[?25h");
} else self.write("\x1b[?25l");
}

pub fn sgr(self: *Terminal, code: SGRAttribute) void {
self.escape(.{ @intFromEnum(code), 'm' });
}

pub fn getSize(self: *Terminal) void {
var size: c.winsize = undefined;
if (c.ioctl(self.tty.handle, c.T.IOCGWINSZ, @intFromPtr(&size)) == -1) unreachable;
self.width = size.ws_col;
self.height = size.ws_row;
}

// This function assumes the input is either a stream of printable/whitespace
// codepoints, or a control sequence. I don't expect the input to zf to be a mixed
// buffer. If that is the case this will need to be refactored.
pub fn read(self: *Terminal, buf: []u8) !InputBuffer {
const reader = self.tty.reader();

var index: usize = 0;
// Ensure at least 4 bytes of space in the buffer so it is safe
// to read a codepoint into it
while (index < buf.len - 3) {
const cp = ziglyph.readCodePoint(reader) catch |err| switch (err) {
// Ignore invalid codepoints
error.InvalidUtf8 => continue,
else => return err,
};
if (cp) |codepoint| {
// An escape sequence start
if (ziglyph.isControl(codepoint)) {
return self.readEscapeSequence(codepoint);
}

// Assert the codepoint is valid because we just read it
index += std.unicode.utf8Encode(codepoint, buf[index..]) catch unreachable;
} else break;
}

return .{ .str = buf[0..index] };
}

fn readEscapeSequence(self: *Terminal, cp: u21) InputBuffer {
const reader = self.tty.reader();

// escape sequences
switch (cp) {
// esc
0x1b => {
var seq: [2]u8 = undefined;
seq[0] = reader.readByte() catch return .esc;
seq[1] = reader.readByte() catch return .esc;

// DECCKM mode sends \x1bO* instead of \x1b[*
if (seq[0] == '[' or seq[0] == 'O') {
return switch (seq[1]) {
'A' => .up,
'B' => .down,
'C' => .right,
'D' => .left,
'3' => {
const byte = reader.readByte() catch return .esc;
if (byte == '~') return .delete;
return .esc;
},
'Z' => .shift_tab,
else => .esc,
};
}

return .esc;
},
'\t' => return .tab,
'\r' => return .enter,
127 => return .backspace,
else => {},
}

// keys pressed while holding control will always be below 0x20
if (cp <= 0x1f) return .{ .control = @intCast(cp & 0x1f) };

return .none;
}
};
Loading

0 comments on commit cbcfbd3

Please sign in to comment.