2024-07-25 19:48:17 +00:00
|
|
|
const std = @import("std");
|
|
|
|
const fs = std.fs;
|
|
|
|
const posix = std.posix;
|
2024-10-11 15:11:51 +00:00
|
|
|
const log = @import("log.zig");
|
2024-07-25 19:48:17 +00:00
|
|
|
const borders = @import("borders.zig");
|
|
|
|
const dim = @import("dimensions.zig");
|
|
|
|
const color = @import("colors.zig");
|
2024-08-03 22:29:43 +00:00
|
|
|
const term_test = @import("test/term.zig");
|
2024-07-25 19:48:17 +00:00
|
|
|
|
|
|
|
var orig_termios: posix.termios = undefined;
|
|
|
|
|
|
|
|
pub const MenuError = error{ForgottenMenuItem};
|
|
|
|
|
|
|
|
pub const KeyType = enum {
|
2024-07-27 20:41:13 +00:00
|
|
|
NO_KEY,
|
2024-07-25 19:48:17 +00:00
|
|
|
ASCII,
|
|
|
|
SEQUENCE,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub const SequenceKey = enum(u8) {
|
|
|
|
UP,
|
|
|
|
DOWN,
|
|
|
|
LEFT,
|
|
|
|
RIGHT,
|
|
|
|
DELETE,
|
|
|
|
|
|
|
|
UNKNOWN,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub const Key = struct {
|
|
|
|
type: KeyType,
|
|
|
|
value: u8,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub const PrintFlags = struct {
|
|
|
|
dim: bool = false,
|
|
|
|
bold: bool = false,
|
|
|
|
italic: bool = false,
|
|
|
|
underline: bool = false,
|
|
|
|
highlight: bool = false,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub const SelectMenuItem = struct {
|
|
|
|
name: []const u8,
|
|
|
|
selected: bool = false,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub const SelectMenu = struct {
|
|
|
|
title: []const u8,
|
|
|
|
items: []SelectMenuItem,
|
|
|
|
focused_item: usize = 0,
|
|
|
|
cancelled: bool = false,
|
|
|
|
|
|
|
|
pub fn init(title: []const u8, items: []SelectMenuItem) SelectMenu {
|
|
|
|
return .{ .title = title, .items = items };
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn reset(self: *SelectMenu) void {
|
|
|
|
self.focused_item = 0;
|
|
|
|
self.cancelled = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn refresh(self: *SelectMenu, term_io: *TermIO) !void {
|
|
|
|
term_io.clear();
|
|
|
|
|
2024-07-27 20:41:13 +00:00
|
|
|
try term_io.print("{s}", .{self.title});
|
2024-07-25 19:48:17 +00:00
|
|
|
for (self.items, 0..) |_, i| {
|
|
|
|
try self.printItem(i, term_io);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn show(self: *SelectMenu, term_io: *TermIO) !void {
|
|
|
|
try self.refresh(term_io);
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
const key: Key = term_io.getKey(true);
|
|
|
|
if (key.type == KeyType.SEQUENCE) {
|
|
|
|
const key_enum: SequenceKey = @enumFromInt(key.value);
|
|
|
|
switch (key_enum) {
|
|
|
|
SequenceKey.DOWN => try self.nextItem(term_io),
|
|
|
|
SequenceKey.UP => try self.prevItem(term_io),
|
|
|
|
else => continue,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (key.value) {
|
|
|
|
13 => break, // Enter
|
|
|
|
' ' => {
|
|
|
|
try self.toggleSelection(term_io);
|
|
|
|
},
|
|
|
|
0x1b, 'q' => {
|
|
|
|
self.cancelled = true;
|
|
|
|
return;
|
|
|
|
},
|
|
|
|
'j' => try self.nextItem(term_io),
|
|
|
|
'k' => try self.prevItem(term_io),
|
|
|
|
else => continue,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn printItem(self: *SelectMenu, item: usize, term_io: *TermIO) !void {
|
|
|
|
if (self.items[item].selected) {
|
|
|
|
try term_io.output(1, item + 2, "[X] {s}", .{self.items[item].name}, .{ .highlight = item == self.focused_item });
|
|
|
|
} else {
|
|
|
|
try term_io.output(1, item + 2, "[ ] {s}", .{self.items[item].name}, .{ .highlight = item == self.focused_item });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn toggleSelection(self: *SelectMenu, term_io: *TermIO) !void {
|
|
|
|
self.items[self.focused_item].selected = !self.items[self.focused_item].selected;
|
|
|
|
try self.printItem(self.focused_item, term_io);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn nextItem(self: *SelectMenu, term_io: *TermIO) !void {
|
|
|
|
if (self.focused_item < self.items.len - 1) {
|
|
|
|
try self.focusItem(self.focused_item + 1, term_io);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn prevItem(self: *SelectMenu, term_io: *TermIO) !void {
|
|
|
|
if (self.focused_item > 0) {
|
|
|
|
try self.focusItem(self.focused_item - 1, term_io);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn focusItem(self: *SelectMenu, item: usize, term_io: *TermIO) !void {
|
|
|
|
const previousSelection = self.focused_item;
|
|
|
|
self.focused_item = item;
|
|
|
|
|
|
|
|
if (previousSelection != self.focused_item) {
|
|
|
|
try self.printItem(previousSelection, term_io);
|
|
|
|
}
|
|
|
|
try self.printItem(self.focused_item, term_io);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
pub const Input = struct {
|
|
|
|
pos_x: usize,
|
|
|
|
pos_y: usize,
|
|
|
|
max_len: usize,
|
|
|
|
|
|
|
|
cursor_pos: usize = 0,
|
|
|
|
len: usize = 0,
|
|
|
|
text: []u8,
|
|
|
|
_allocator: std.mem.Allocator,
|
|
|
|
|
|
|
|
pub fn init(allocator: std.mem.Allocator, x: usize, y: usize, max_len: usize) !Input {
|
|
|
|
return Input{
|
|
|
|
.pos_x = x,
|
|
|
|
.pos_y = y,
|
|
|
|
.max_len = max_len,
|
|
|
|
.text = try allocator.alloc(u8, max_len),
|
|
|
|
._allocator = allocator,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn deinit(self: *Input) void {
|
|
|
|
self._allocator.free(self.text);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn reset(self: *Input) void {
|
|
|
|
@memset(self.text, ' ');
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn draw(self: *Input, term_io: *TermIO) !void {
|
|
|
|
try term_io.output(self.pos_x, self.pos_y, "{s}", .{self.text}, .{ .underline = true });
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn focus(self: *Input, term_io: *TermIO) !void {
|
|
|
|
try term_io.moveCursor(self.pos_x + @min(self.cursor_pos, self.max_len - 1), self.pos_y);
|
|
|
|
term_io.showCursor();
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn handleKey(self: *Input, term_io: *TermIO, key: Key) !void {
|
|
|
|
if (key.type == .ASCII) {
|
|
|
|
if (std.ascii.isPrint(key.value) and self.len < self.max_len) {
|
|
|
|
try self.insertChar(key.value);
|
|
|
|
try self.draw(term_io);
|
|
|
|
// try term_io.output(17 + input_len, 4, "{c}", .{key.value}, .{});
|
|
|
|
} else if (key.value == 0x7F and self.cursor_pos > 0 and self.len > 0) { // Backspace
|
|
|
|
try self.removeChar();
|
|
|
|
try self.draw(term_io);
|
|
|
|
// try term_io.output(17 + input_len, 4, " ", .{}, .{});
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const key_enum: SequenceKey = @enumFromInt(key.value);
|
|
|
|
if (key_enum == SequenceKey.RIGHT and self.cursor_pos < self.len) {
|
|
|
|
self.cursor_pos += 1;
|
|
|
|
} else if (key_enum == SequenceKey.LEFT and self.cursor_pos > 0) {
|
|
|
|
self.cursor_pos -= 1;
|
|
|
|
} else if (key_enum == SequenceKey.DELETE and self.cursor_pos < self.len) {
|
|
|
|
self.cursor_pos += 1;
|
|
|
|
try self.removeChar();
|
|
|
|
try self.draw(term_io);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn insertChar(self: *Input, char: u8) !void {
|
|
|
|
var i: usize = self.len;
|
|
|
|
while (i > self.cursor_pos) : (i -= 1) {
|
|
|
|
self.text[i] = self.text[i - 1];
|
|
|
|
}
|
|
|
|
self.text[self.cursor_pos] = char;
|
|
|
|
self.len += 1;
|
|
|
|
self.cursor_pos += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
fn removeChar(self: *Input) !void {
|
|
|
|
self.cursor_pos -= 1;
|
|
|
|
for (self.cursor_pos..self.len - 1) |i| {
|
|
|
|
self.text[i] = self.text[i + 1];
|
|
|
|
}
|
|
|
|
self.text[self.len - 1] = ' ';
|
|
|
|
self.len -= 1;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-07-27 20:41:13 +00:00
|
|
|
pub const TermFormat = packed struct {
|
|
|
|
bold: bool = false,
|
|
|
|
dim: bool = false,
|
|
|
|
italic: bool = false,
|
|
|
|
underline: bool = false,
|
|
|
|
highlight: bool = false,
|
|
|
|
invisible: bool = false,
|
|
|
|
strikethrough: bool = false,
|
|
|
|
};
|
|
|
|
|
2024-07-25 19:48:17 +00:00
|
|
|
pub const TermIO = struct {
|
2024-08-03 22:29:43 +00:00
|
|
|
stdin: std.io.AnyReader,
|
|
|
|
stdout: std.io.BufferedWriter(4096, std.io.AnyWriter).Writer,
|
|
|
|
//stdin: std.fs.File.Reader,
|
|
|
|
//stdout: std.io.BufferedWriter(4096, std.fs.File.Writer).Writer,
|
2024-07-25 19:48:17 +00:00
|
|
|
tty_file: fs.File,
|
2024-07-27 20:41:13 +00:00
|
|
|
current_format: TermFormat = .{},
|
2024-07-25 19:48:17 +00:00
|
|
|
current_background: color.Color = color.Default,
|
|
|
|
current_foreground: color.Color = color.Default,
|
|
|
|
escape_sequence_queued: bool = false,
|
|
|
|
|
|
|
|
pub fn close(self: *const TermIO) void {
|
|
|
|
self.tty_file.close();
|
|
|
|
}
|
|
|
|
|
2024-10-11 15:11:51 +00:00
|
|
|
pub fn flush(self: *TermIO) void {
|
|
|
|
self.stdout.context.flush() catch @panic("Failed to flush buffered writer\n");
|
|
|
|
// var key = self.getKey(true);
|
|
|
|
// while (key.type != .ASCII or key.value != 27) {
|
|
|
|
// key = self.getKey(true);
|
|
|
|
// }
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn enterRawMode(term_io: *const TermIO) void {
|
|
|
|
orig_termios = posix.tcgetattr(term_io.tty_file.handle) catch unreachable;
|
|
|
|
|
|
|
|
var raw = orig_termios;
|
|
|
|
raw.iflag.BRKINT = false;
|
|
|
|
raw.iflag.ICRNL = false;
|
|
|
|
raw.iflag.INPCK = false;
|
|
|
|
raw.iflag.ISTRIP = false;
|
|
|
|
raw.iflag.IXON = false;
|
|
|
|
|
|
|
|
// raw.oflag.OPOST = false;
|
|
|
|
|
|
|
|
raw.cflag.CSIZE = posix.CSIZE.CS8;
|
|
|
|
|
|
|
|
raw.lflag.ECHO = false;
|
|
|
|
raw.lflag.ICANON = false;
|
|
|
|
raw.lflag.IEXTEN = false;
|
|
|
|
raw.lflag.ISIG = false;
|
|
|
|
|
|
|
|
raw.cc[@intFromEnum(posix.V.MIN)] = 0;
|
|
|
|
raw.cc[@intFromEnum(posix.V.TIME)] = 1;
|
|
|
|
|
|
|
|
posix.tcsetattr(term_io.tty_file.handle, posix.TCSA.FLUSH, raw) catch unreachable;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn exitRawMode(term_io: *const TermIO) void {
|
|
|
|
posix.tcsetattr(term_io.tty_file.handle, posix.TCSA.FLUSH, orig_termios) catch unreachable;
|
|
|
|
}
|
|
|
|
|
2024-07-27 20:41:13 +00:00
|
|
|
pub fn enableFormats(term_io: *TermIO, formats: TermFormat) void {
|
|
|
|
if (formats.bold and !term_io.current_format.bold) {
|
|
|
|
term_io.current_format.bold = true;
|
|
|
|
term_io.print("\x1b[1m", .{});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (formats.dim and !term_io.current_format.dim) {
|
|
|
|
term_io.current_format.dim = true;
|
|
|
|
term_io.print("\x1b[2m", .{});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (formats.italic and !term_io.current_format.italic) {
|
|
|
|
term_io.current_format.italic = true;
|
|
|
|
term_io.print("\x1b[3m", .{});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (formats.underline and !term_io.current_format.underline) {
|
|
|
|
term_io.current_format.underline = true;
|
|
|
|
term_io.print("\x1b[4m", .{});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (formats.highlight and !term_io.current_format.highlight) {
|
|
|
|
term_io.current_format.highlight = true;
|
|
|
|
term_io.print("\x1b[7m", .{});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (formats.invisible and !term_io.current_format.invisible) {
|
|
|
|
term_io.current_format.invisible = true;
|
|
|
|
term_io.print("\x1b[8m", .{});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (formats.strikethrough and !term_io.current_format.strikethrough) {
|
|
|
|
term_io.current_format.strikethrough = true;
|
|
|
|
term_io.print("\x1b[9m", .{});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn disableFormats(term_io: *TermIO, formats: TermFormat) void {
|
|
|
|
if ((!term_io.current_format.bold or formats.bold) and (!term_io.current_format.dim or formats.dim) and
|
|
|
|
(!term_io.current_format.italic or formats.italic) and (!term_io.current_format.underline or formats.underline) and
|
|
|
|
(!term_io.current_format.highlight or formats.highlight) and (!term_io.current_format.invisible or formats.invisible) and
|
2024-10-11 15:11:51 +00:00
|
|
|
(!term_io.current_format.strikethrough or formats.strikethrough) and (term_io.current_background.equal(color.Default) and
|
|
|
|
(term_io.current_foreground.equal(color.Default))))
|
2024-07-27 20:41:13 +00:00
|
|
|
{
|
|
|
|
term_io.current_format = .{};
|
|
|
|
term_io.print("\x1b[0m", .{});
|
2024-10-11 15:11:51 +00:00
|
|
|
return;
|
2024-07-27 20:41:13 +00:00
|
|
|
}
|
|
|
|
|
2024-10-11 15:11:51 +00:00
|
|
|
if (formats.dim and term_io.current_format.dim) {
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.current_format.dim = false;
|
|
|
|
term_io.print("\x1b[22m", .{});
|
|
|
|
}
|
|
|
|
|
2024-10-11 15:11:51 +00:00
|
|
|
if (formats.italic and term_io.current_format.italic) {
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.current_format.italic = false;
|
|
|
|
term_io.print("\x1b[23m", .{});
|
|
|
|
}
|
|
|
|
|
2024-10-11 15:11:51 +00:00
|
|
|
if (formats.underline and term_io.current_format.underline) {
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.current_format.underline = false;
|
|
|
|
term_io.print("\x1b[24m", .{});
|
|
|
|
}
|
|
|
|
|
2024-10-11 15:11:51 +00:00
|
|
|
if (formats.highlight and term_io.current_format.highlight) {
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.current_format.highlight = false;
|
|
|
|
term_io.print("\x1b[27m", .{});
|
|
|
|
}
|
|
|
|
|
2024-10-11 15:11:51 +00:00
|
|
|
if (formats.invisible and term_io.current_format.invisible) {
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.current_format.invisible = false;
|
|
|
|
term_io.print("\x1b[28m", .{});
|
|
|
|
}
|
|
|
|
|
2024-10-11 15:11:51 +00:00
|
|
|
if (formats.strikethrough and term_io.current_format.strikethrough) {
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.current_format.strikethrough = false;
|
|
|
|
term_io.print("\x1b[29m", .{});
|
|
|
|
}
|
|
|
|
|
2024-10-11 15:11:51 +00:00
|
|
|
if (formats.bold and term_io.current_format.bold) {
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.current_format.bold = false;
|
|
|
|
term_io.print("\x1b[21m", .{});
|
|
|
|
}
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
|
2024-07-27 20:41:13 +00:00
|
|
|
pub fn saveScreen(term_io: *const TermIO) void {
|
|
|
|
term_io.print("\x1b[s", .{});
|
|
|
|
term_io.print("\x1b[?47h", .{});
|
|
|
|
term_io.print("\x1b[?1049h", .{});
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn restoreScreen(term_io: *const TermIO) void {
|
|
|
|
term_io.print("\x1b[?1049l", .{});
|
|
|
|
term_io.print("\x1b[?47l", .{});
|
|
|
|
term_io.print("\x1b[u", .{});
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn clear(term_io: *const TermIO) void {
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.print("\x1b[2J\x1b[H", .{});
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
|
2024-07-27 20:41:13 +00:00
|
|
|
pub fn setColor(term_io: *TermIO, background: color.Color, foreground: color.Color) void {
|
|
|
|
term_io.setBackgroundColor(background);
|
|
|
|
term_io.setForegroundColor(foreground);
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
|
2024-07-27 20:41:13 +00:00
|
|
|
pub fn setBackgroundColor(term_io: *TermIO, background: color.Color) void {
|
2024-07-25 19:48:17 +00:00
|
|
|
if (!background.equal(term_io.current_background)) {
|
|
|
|
switch (background.type) {
|
2024-07-27 20:41:13 +00:00
|
|
|
.Default => term_io.print("\x1b[49m", .{}),
|
2024-10-11 15:11:51 +00:00
|
|
|
.Inherit => {},
|
2024-07-27 20:41:13 +00:00
|
|
|
.RGB => term_io.print("\x1b[48;2;{};{};{}m", .{ background.red, background.green, background.blue }),
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
}
|
2024-07-27 20:41:13 +00:00
|
|
|
|
|
|
|
term_io.current_background = background;
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
|
2024-07-27 20:41:13 +00:00
|
|
|
pub fn setForegroundColor(term_io: *TermIO, foreground: color.Color) void {
|
2024-07-25 19:48:17 +00:00
|
|
|
if (!foreground.equal(term_io.current_foreground)) {
|
|
|
|
switch (foreground.type) {
|
2024-07-27 20:41:13 +00:00
|
|
|
.Default => term_io.print("\x1b[39m", .{}),
|
2024-10-11 15:11:51 +00:00
|
|
|
.Inherit => {},
|
2024-07-27 20:41:13 +00:00
|
|
|
.RGB => term_io.print("\x1b[38;2;{};{};{}m", .{ foreground.red, foreground.green, foreground.blue }),
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
}
|
2024-07-27 20:41:13 +00:00
|
|
|
|
|
|
|
term_io.current_foreground = foreground;
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn hideCursor(term_io: *const TermIO) void {
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.print("\x1b[?25l", .{});
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn showCursor(term_io: *const TermIO) void {
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.print("\x1b[?25h", .{});
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
|
2024-07-27 20:41:13 +00:00
|
|
|
pub fn moveCursor(term_io: *const TermIO, x: usize, y: usize) void {
|
|
|
|
term_io.print("\x1b[{d};{d}H", .{ y, x });
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
|
2024-07-27 20:41:13 +00:00
|
|
|
pub fn saveTitle(term_io: *const TermIO) void {
|
|
|
|
term_io.print("\x1b[22;2t", .{});
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
|
2024-07-27 20:41:13 +00:00
|
|
|
pub fn restoreTitle(term_io: *const TermIO) void {
|
|
|
|
term_io.print("\x1b[23;2t", .{});
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
|
2024-07-27 20:41:13 +00:00
|
|
|
pub fn setTitle(term_io: *const TermIO, title: []const u8) void {
|
|
|
|
term_io.print("\x1b]0;{s}\x1b\\", .{title});
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
|
2024-07-27 20:41:13 +00:00
|
|
|
pub fn print(term_io: *const TermIO, comptime format: []const u8, args: anytype) void {
|
|
|
|
term_io.stdout.print(format, args) catch @panic("Failed to print!!");
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
|
2024-07-27 20:41:13 +00:00
|
|
|
pub fn output(term_io: *const TermIO, x: usize, y: usize, comptime format: []const u8, args: anytype) void {
|
|
|
|
term_io.stdout.print("\x1b[{d};{d}H" ++ format, .{ y, x } ++ args) catch @panic("Failed to print!!");
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn fillBox(term_io: *const TermIO, dims: dim.CalculatedDimensions) void {
|
|
|
|
for (0..dims.size.height) |i| {
|
|
|
|
term_io.moveCursor(dims.pos.x, dims.pos.y + i);
|
|
|
|
term_io.print("\x1b[{d}X", .{dims.size.width});
|
|
|
|
}
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn drawBox(term_io: *TermIO, dims: dim.CalculatedDimensions, border: borders.Border, fill: bool) !void {
|
|
|
|
if (dims.size.width < 2 or dims.size.height < 2) {
|
|
|
|
return;
|
|
|
|
}
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.output(dims.pos.x, dims.pos.y, "{u}", .{border.top_left});
|
2024-07-25 19:48:17 +00:00
|
|
|
for (0..dims.size.width - 2) |_| {
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.print("{u}", .{border.top});
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.print("{u}", .{border.top_right});
|
2024-07-25 19:48:17 +00:00
|
|
|
for (1..dims.size.height - 1) |h| {
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.output(dims.pos.x, dims.pos.y + h, "{u}", .{border.left});
|
2024-07-25 19:48:17 +00:00
|
|
|
if (fill) {
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.print("\x1b[{d}X", .{dims.size.width - 2});
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.output(dims.pos.x + dims.size.width - 1, dims.pos.y + h, "{u}", .{border.right});
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.output(dims.pos.x, dims.pos.y + dims.size.height - 1, "{u}", .{border.bottom_left});
|
2024-07-25 19:48:17 +00:00
|
|
|
for (0..dims.size.width - 2) |_| {
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.print("{u}", .{border.bottom});
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
2024-07-27 20:41:13 +00:00
|
|
|
term_io.print("{u}", .{border.bottom_right});
|
2024-07-25 19:48:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn getKey(term_io: *TermIO, block: bool) Key {
|
|
|
|
var escape_sequence: [10]u8 = undefined;
|
|
|
|
var sequence_len: usize = 0;
|
|
|
|
|
|
|
|
// peek doesn't exist in 0.12.0 :(
|
|
|
|
while (true) {
|
|
|
|
if (!term_io.escape_sequence_queued) {
|
|
|
|
const c = term_io.stdin.readByte() catch |err| {
|
|
|
|
switch (err) {
|
2024-07-27 20:41:13 +00:00
|
|
|
error.WouldBlock, error.EndOfStream => if (!block) return .{ .type = KeyType.NO_KEY, .value = 0 },
|
2024-07-25 19:48:17 +00:00
|
|
|
else => {
|
2024-10-11 15:11:51 +00:00
|
|
|
log.err("{any}", .{err});
|
|
|
|
@panic("Unexpected error when reading from stdin");
|
2024-07-25 19:48:17 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
if (c != 0x1b and sequence_len == 0) {
|
|
|
|
return .{ .type = KeyType.ASCII, .value = c };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
term_io.escape_sequence_queued = false;
|
|
|
|
|
|
|
|
// Escape sequence
|
|
|
|
sequence_len = 0;
|
|
|
|
escape_sequence[sequence_len] = term_io.stdin.readByte() catch return .{ .type = KeyType.ASCII, .value = 0x1b };
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
sequence_len += 1;
|
|
|
|
const byte = term_io.stdin.readByte() catch
|
|
|
|
return decipherEscapeSequence(escape_sequence[0..sequence_len]);
|
|
|
|
if (byte == 0x1b) {
|
|
|
|
term_io.escape_sequence_queued = true;
|
|
|
|
return decipherEscapeSequence(escape_sequence[0..sequence_len]);
|
|
|
|
}
|
|
|
|
escape_sequence[sequence_len] = byte;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-10-11 15:11:51 +00:00
|
|
|
pub fn getTermSize(term_io: *const TermIO) dim.Size {
|
2024-07-25 19:48:17 +00:00
|
|
|
var ws: std.posix.winsize = undefined;
|
|
|
|
const ret = std.os.linux.ioctl(term_io.tty_file.handle, std.os.linux.T.IOCGWINSZ, @intFromPtr(&ws));
|
|
|
|
if (ret == -1 or ws.ws_col == 0) {
|
|
|
|
std.debug.print("Failed to get terminal size\n", .{});
|
|
|
|
unreachable;
|
|
|
|
}
|
|
|
|
|
2024-10-11 15:11:51 +00:00
|
|
|
return dim.Size{
|
|
|
|
.width = ws.ws_col,
|
|
|
|
.height = ws.ws_row,
|
2024-07-25 19:48:17 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-10-11 15:11:51 +00:00
|
|
|
pub fn getTermIO(stdin: std.io.AnyReader, stdout: std.io.BufferedWriter(4096, std.io.AnyWriter).Writer) !TermIO {
|
2024-07-25 19:48:17 +00:00
|
|
|
return TermIO{
|
2024-10-11 15:11:51 +00:00
|
|
|
.stdin = stdin,
|
|
|
|
.stdout = stdout,
|
2024-07-25 19:48:17 +00:00
|
|
|
.tty_file = try fs.cwd().openFile("/dev/tty", .{ .mode = .read_write }),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
fn decipherEscapeSequence(sequence: []u8) Key {
|
|
|
|
if (sequence.len == 2) {
|
|
|
|
if (std.mem.eql(u8, sequence, &[2]u8{ '[', 'A' })) return .{ .type = KeyType.SEQUENCE, .value = @intFromEnum(SequenceKey.UP) };
|
|
|
|
if (std.mem.eql(u8, sequence, &[2]u8{ '[', 'B' })) return .{ .type = KeyType.SEQUENCE, .value = @intFromEnum(SequenceKey.DOWN) };
|
|
|
|
if (std.mem.eql(u8, sequence, &[2]u8{ '[', 'C' })) return .{ .type = KeyType.SEQUENCE, .value = @intFromEnum(SequenceKey.RIGHT) };
|
|
|
|
if (std.mem.eql(u8, sequence, &[2]u8{ '[', 'D' })) return .{ .type = KeyType.SEQUENCE, .value = @intFromEnum(SequenceKey.LEFT) };
|
|
|
|
} else if (sequence.len == 3) {
|
|
|
|
if (std.mem.eql(u8, sequence, &[3]u8{ '[', '3', '~' })) return .{ .type = KeyType.SEQUENCE, .value = @intFromEnum(SequenceKey.DELETE) };
|
|
|
|
}
|
|
|
|
|
|
|
|
return .{ .type = KeyType.SEQUENCE, .value = @intFromEnum(SequenceKey.UNKNOWN) };
|
|
|
|
}
|
2024-08-03 22:29:43 +00:00
|
|
|
|
2024-10-11 15:11:51 +00:00
|
|
|
test "stdout" {
|
2024-08-03 22:29:43 +00:00
|
|
|
term_test.term_io.print("Hello {}", .{123});
|
2024-10-11 15:11:51 +00:00
|
|
|
term_test.term_io.flush();
|
2024-08-03 22:29:43 +00:00
|
|
|
try std.testing.expectEqualStrings("Hello 123", term_test.readAllTestStdout());
|
|
|
|
}
|
|
|
|
|
|
|
|
test "stdin" {
|
|
|
|
try term_test.writeTestStdin(&[_]u8{ 'a', '\x1b', '[', 'A' });
|
|
|
|
|
|
|
|
var key = term_test.term_io.getKey(false);
|
|
|
|
try std.testing.expect(key.type == .ASCII);
|
|
|
|
try std.testing.expect(key.value == 'a');
|
|
|
|
|
|
|
|
key = term_test.term_io.getKey(false);
|
|
|
|
try std.testing.expect(key.type == .SEQUENCE);
|
|
|
|
try std.testing.expect(@as(SequenceKey, @enumFromInt(key.value)) == SequenceKey.UP);
|
|
|
|
|
|
|
|
key = term_test.term_io.getKey(false);
|
|
|
|
try std.testing.expect(key.type == .NO_KEY);
|
|
|
|
}
|