diff --git a/src/borders.zig b/src/borders.zig new file mode 100644 index 0000000..8990489 --- /dev/null +++ b/src/borders.zig @@ -0,0 +1,54 @@ +pub const Border = struct { + top_left: u21, + top_right: u21, + bottom_left: u21, + bottom_right: u21, + top: u21, + bottom: u21, + left: u21, + right: u21, +}; + +pub const BasicBorder = Border{ + .top_left = '┌', + .top_right = '┐', + .bottom_left = '└', + .bottom_right = '┘', + .top = '─', + .bottom = '─', + .left = '│', + .right = '│', +}; + +pub const BoldBorder = Border{ + .top_left = '┏', + .top_right = '┓', + .bottom_left = '┗', + .bottom_right = '┛', + .top = '━', + .bottom = '━', + .left = '┃', + .right = '┃', +}; + +pub const DoubleBorder = Border{ + .top_left = '╔', + .top_right = '╗', + .bottom_left = '╚', + .bottom_right = '╝', + .top = '═', + .bottom = '═', + .left = '║', + .right = '║', +}; + +pub const BigBorder = Border{ + .top_left = '▛', + .top_right = '▜', + .bottom_left = '▙', + .bottom_right = '▟', + .top = '▀', + .bottom = '▄', + .left = '▌', + .right = '▐', +}; diff --git a/src/colors.zig b/src/colors.zig new file mode 100644 index 0000000..8973abc --- /dev/null +++ b/src/colors.zig @@ -0,0 +1,34 @@ +pub const Color = struct { + type: enum { Default, RGB }, + red: u8, + green: u8, + blue: u8, + + pub fn equal(self: Color, other: Color) bool { + if (self.type != other.type) { + return false; + } + + if (self.type == .Default) { + return true; + } + + return self.red == other.red and self.green == other.green and self.blue == self.blue; + } +}; + +pub const Default = Color{ + .type = .Default, + .red = undefined, + .green = undefined, + .blue = undefined, +}; + +pub inline fn RGB(red: u8, green: u8, blue: u8) Color { + return .{ + .type = .RGB, + .red = red, + .green = green, + .blue = blue, + }; +} diff --git a/src/dimensions.zig b/src/dimensions.zig new file mode 100644 index 0000000..103da5b --- /dev/null +++ b/src/dimensions.zig @@ -0,0 +1,41 @@ +pub const Anchor = enum { + TopLeft, + TopCenter, + TopRight, + CenterLeft, + Center, + CenterRight, + BottomLeft, + BottomCenter, + BottomRight, +}; + +pub const Position = struct { + x: usize, + y: usize, +}; + +pub const Size = struct { + width: usize, + height: usize, +}; + +pub const Dimensions = struct { + size_type: enum { Relative, Absolute }, + size: Size, + anchor: Anchor, +}; + +pub const CalculatedDimensions = struct { + size: Size, + pos: Position, +}; + +pub const Fill = Dimensions{ + .size_type = .Relative, + .anchor = .TopLeft, + .size = .{ + .width = 100, + .height = 100, + }, +}; diff --git a/src/main.zig b/src/main.zig index c8a3f67..0c1a6c8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,24 +1,73 @@ const std = @import("std"); +const term = @import("term.zig"); +const borders = @import("borders.zig"); +const pane = @import("pane.zig"); +const color = @import("colors.zig"); +const dim = @import("dimensions.zig"); + +var term_io: term.TermIO = undefined; + +pub fn panic(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn { + pane.cleanup(&term_io); + std.builtin.default_panic(msg, error_return_trace, ret_addr); +} pub fn main() !void { - // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) - std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + defer { + if (gpa.deinit() == .leak) { + std.debug.print("Memory was leaked D:\n", .{}); + } + } - // stdout is for the actual output of your application, for example if you - // are implementing gzip, then only the compressed bytes should be sent to - // stdout, not any debugging messages. - const stdout_file = std.io.getStdOut().writer(); - var bw = std.io.bufferedWriter(stdout_file); - const stdout = bw.writer(); + term_io = try term.getTermIO(); - try stdout.print("Run `zig build test` to run the tests.\n", .{}); + try pane.init(&term_io); + defer pane.cleanup(&term_io); - try bw.flush(); // don't forget to flush! -} - -test "simple test" { - var list = std.ArrayList(i32).init(std.testing.allocator); - defer list.deinit(); // try commenting this out and see if zig detects the memory leak! - try list.append(42); - try std.testing.expectEqual(@as(i32, 42), list.pop()); + var top = pane.Pane{ + .dimensions = dim.Fill, + .border = borders.BoldBorder, + .children = std.ArrayList(*pane.Pane).init(allocator), + .background = color.RGB(30, 30, 30), + .foreground = color.RGB(0, 255, 0), + }; + + var child = pane.Pane{ + .parent = &top, + .children = null, + .dimensions = .{ + .anchor = .Center, + .size_type = .Relative, + .size = .{ .width = 50, .height = 50 }, + }, + .border = borders.BoldBorder, + .background = color.RGB(125, 0, 125), + .foreground = color.RGB(255, 125, 10), + }; + + pane.top_pane = ⊤ + try top.children.?.append(&child); + + const childWriter = child.writer(&term_io); + var key = term_io.getKey(false); + while (key.value != 113 and !pane.should_exit) { + try pane.tick(&term_io); + if (key.type == .ASCII and key.value == 111) { + try std.fmt.format(childWriter, "\x1b[2J", .{}); + try term_io.flush(); + } + if (key.type == .ASCII and key.value == 110) { + try std.fmt.format(childWriter, "Hello", .{}); + try term_io.flush(); + } + if (key.type == .ASCII and key.value == 109) { + try std.fmt.format(childWriter, "Hello\n", .{}); + try term_io.flush(); + } + key = term_io.getKey(false); + } + + top.children.?.deinit(); } diff --git a/src/pane.zig b/src/pane.zig new file mode 100644 index 0000000..ffdff09 --- /dev/null +++ b/src/pane.zig @@ -0,0 +1,220 @@ +const std = @import("std"); +const color = @import("colors.zig"); +const dim = @import("dimensions.zig"); +const term = @import("term.zig"); +const Border = @import("borders.zig").Border; + +const TermIO = term.TermIO; + +pub var top_pane: *Pane = undefined; +pub var focused_pane: ?*Pane = null; +pub var should_exit: bool = false; +var needs_redraw = true; +var redraw_count: usize = 0; + +fn resize_signal(sig: i32) callconv(.C) void { + _ = sig; + needs_redraw = true; +} + +fn exit_signal(sig: i32) callconv(.C) void { + _ = sig; + should_exit = true; +} + +pub fn init(term_io: *TermIO) !void { + var resize_handler = std.posix.Sigaction{ + .handler = .{ .handler = resize_signal }, + .mask = std.posix.empty_sigset, + .flags = 0, + }; + try std.posix.sigaction(std.posix.SIG.WINCH, &resize_handler, null); + + var exit_handler = std.posix.Sigaction{ + .handler = .{ .handler = exit_signal }, + .mask = std.posix.empty_sigset, + .flags = 0, + }; + try std.posix.sigaction(std.posix.SIG.INT, &exit_handler, null); + + term_io.enterRawMode(); + term_io.saveScreen() catch {}; + term_io.hideCursor(); + try term_io.flush(); +} + +pub fn cleanup(term_io: *TermIO) void { + term_io.showCursor(); + term_io.restoreScreen() catch {}; + term_io.exitRawMode(); + term_io.flush() catch {}; + term_io.close(); +} + +pub fn tick(term_io: *TermIO) !void { + if (needs_redraw) { + needs_redraw = false; + const size = term.getTermSize(term_io); + + term_io.clear(); + try top_pane.ReDraw(term_io, size); + + redraw_count += 1; + try std.fmt.format(top_pane.writer(term_io), "Resized {} times", .{redraw_count}); + try term_io.flush(); + } +} + +pub const Overflow = struct { + x: enum { Wrap, Hidden }, + y: enum { Scroll, Hidden }, +}; + +pub const Cursor = struct { + x: usize, + y: usize, +}; + +pub const Pane = struct { + parent: ?*Pane = null, + children: ?std.ArrayList(*Pane), + border: ?Border = null, + cursor: Cursor = .{ .x = 0, .y = 0 }, + overflow: Overflow = .{ .x = .Hidden, .y = .Hidden }, + dimensions: dim.Dimensions, + calcDims: dim.CalculatedDimensions = undefined, + background: color.Color = color.Default, + foreground: color.Color = color.Default, + + pub const WriterContext = struct { pane: *Pane, term_io: *const TermIO }; + pub const Writer = std.io.Writer(WriterContext, std.fs.File.WriteError, write); + + pub fn ReDraw(self: *Pane, term_io: *TermIO, parentDims: dim.CalculatedDimensions) !void { + self.calcDims = .{ .pos = parentDims.pos, .size = self.dimensions.size }; + if (self.dimensions.size_type == .Relative) { + std.debug.assert(self.dimensions.size.width <= 100 and self.dimensions.size.height <= 100); + self.calcDims.size.width = (parentDims.size.width * self.dimensions.size.width) / 100; + self.calcDims.size.height = (parentDims.size.height * self.dimensions.size.height) / 100; + } + + try term_io.setColor(self.background, self.foreground); + + switch (self.dimensions.anchor) { + .TopLeft => {}, + .TopCenter => { + self.calcDims.pos.x = parentDims.pos.x + (parentDims.size.width - self.calcDims.size.width) / 2; + }, + .TopRight => { + self.calcDims.pos.x = parentDims.pos.x + parentDims.size.width - self.calcDims.size.width; + }, + .CenterLeft => { + self.calcDims.pos.y = parentDims.pos.y + (parentDims.size.height - self.calcDims.size.height) / 2; + }, + .Center => { + self.calcDims.pos.x = parentDims.pos.x + (parentDims.size.width - self.calcDims.size.width) / 2; + self.calcDims.pos.y = parentDims.pos.y + (parentDims.size.height - self.calcDims.size.height) / 2; + }, + .CenterRight => { + self.calcDims.pos.x = parentDims.pos.x + parentDims.size.width - self.calcDims.size.width; + self.calcDims.pos.y = parentDims.pos.y + (parentDims.size.height - self.calcDims.size.height) / 2; + }, + .BottomLeft => { + self.calcDims.pos.y = parentDims.pos.y + parentDims.size.height - self.calcDims.size.height; + }, + .BottomCenter => { + self.calcDims.pos.y = parentDims.pos.y + parentDims.size.height - self.calcDims.size.height; + self.calcDims.pos.x = parentDims.pos.x + (parentDims.size.width - self.calcDims.size.width) / 2; + }, + .BottomRight => { + self.calcDims.pos.y = parentDims.pos.y + parentDims.size.height - self.calcDims.size.height; + self.calcDims.pos.x = parentDims.pos.x + parentDims.size.width - self.calcDims.size.width; + }, + } + + if (self.border) |border| { + var fill = false; + if (self.parent) |parent| { + fill = !self.background.equal(parent.background); + } else { + fill = !self.background.equal(color.Default); + } + try term_io.drawBox(self.calcDims, border, fill); + } + + try term_io.flush(); + //_ = term_io.getKey(true); + + if (self.children) |children| { + for (children.items) |child| { + try child.ReDraw(term_io, self.calcDims); + try child.focus(term_io); + } + } + } + + pub fn focus(self: *Pane, term_io: *const TermIO) !void { + focused_pane = self; + self.cursor = .{ .x = 0, .y = 0 }; + + self.moveCursor(term_io); + try term_io.setColor(self.background, self.foreground); + } + + fn nextLine(self: *Pane, term_io: *const TermIO) void { + self.cursor.x = 0; + self.cursor.y += 1; + + self.moveCursor(term_io); + } + + pub fn moveCursor(self: *Pane, term_io: *const TermIO) void { + const borderWidth: u1 = if (self.border != null) 1 else 0; + term_io.moveCursor(self.calcDims.pos.x + self.cursor.x + borderWidth, self.calcDims.pos.y + self.cursor.y + borderWidth) catch {}; + } + + pub fn print(self: *Pane, term_io: *const TermIO, string: []const u8) !void { + const borderWidth: u2 = if (self.border != null) 1 else 0; + var space = self.calcDims.size.width - self.cursor.x - (2 * borderWidth); + var i: usize = 0; + + while (string.len - i >= space) { + try term_io.print("{s}", .{string[i .. i + space]}, .{}); + self.nextLine(term_io); + i += space; + space = self.calcDims.size.width - (2 * borderWidth); + } + + if (i < string.len) { + try term_io.print("{s}", .{string[i..]}, .{}); + self.cursor.x += string.len - i; + } + } + + pub fn writer(self: *Pane, term_io: *const TermIO) Writer { + return .{ + .context = .{ .pane = self, .term_io = term_io }, + }; + } + + pub fn write(self: WriterContext, bytes: []const u8) std.fs.File.WriteError!usize { + if (self.pane != focused_pane) { + try self.pane.focus(self.term_io); + } + + const borderWidth: u2 = if (self.pane.border != null) 1 else 0; + for (bytes) |byte| { + switch (byte) { + '\x1b' => continue, + '\n' => self.pane.nextLine(self.term_io), + else => { + self.pane.cursor.x += try self.term_io.stdout.write(&[_]u8{byte}); + if (self.pane.cursor.x >= self.pane.calcDims.size.width - (2 * borderWidth)) { + self.pane.nextLine(self.term_io); + } + }, + } + } + + return bytes.len; + } +}; diff --git a/src/term.zig b/src/term.zig new file mode 100644 index 0000000..bf41b44 --- /dev/null +++ b/src/term.zig @@ -0,0 +1,563 @@ +const std = @import("std"); +const fs = std.fs; +const posix = std.posix; +const borders = @import("borders.zig"); +const dim = @import("dimensions.zig"); +const color = @import("colors.zig"); + +var orig_termios: posix.termios = undefined; + +pub const MenuError = error{ForgottenMenuItem}; + +pub const KeyType = enum { + 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 MenuItem = struct { + name: []const u8, + value: usize, + disabled: 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(); + + try term_io.print("{s}", .{self.title}, .{ .underline = true }); + 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 Menu = struct { + title: []const u8, + items: []MenuItem, + selected_item: usize = 0, + selected_value: usize = 0, + + pub fn init(title: []const u8, items: []MenuItem) Menu { + for (items) |item| { + std.debug.assert(item.value != 0); + } + + return .{ .title = title, .items = items }; + } + + pub fn reset(self: *Menu) void { + self.selected_item = 0; + self.selected_value = 0; + } + + pub fn refresh(self: *Menu, term_io: *TermIO) !void { + term_io.clear(); + + try term_io.print("{s}", .{self.title}, .{ .underline = true }); + for (self.items, 0..) |_, i| { + try self.printItem(i, term_io); + } + } + + pub fn show(self: *Menu, term_io: *TermIO) !void { + if (self.items[self.selected_item].disabled) { + try self.nextItem(term_io); + } + if (self.items[self.selected_item].disabled) { + self.selected_item = 0; + return; + } + + 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 + '0' => { + if (!self.items[9].disabled) { + try self.selectItem(9, term_io); + break; + } + }, + '1'...'9' => { + if (!self.items[key.value - '1'].disabled) { + try self.selectItem(key.value - '1', term_io); + break; + } + }, + 0x1b, 'q' => { + self.selected_value = 0; + return; + }, + 'j' => try self.nextItem(term_io), + 'k' => try self.prevItem(term_io), + else => continue, + } + } + + self.selected_value = self.items[self.selected_item].value; + } + + fn printItem(self: *Menu, item: usize, term_io: *TermIO) !void { + if (item < 10) { + try term_io.output(1, item + 2, "{d}: {s}", .{ item + 1 % 10, self.items[item].name }, .{ .dim = self.items[item].disabled, .highlight = item == self.selected_item }); + } else { + try term_io.output(1, item + 2, " : {s}", .{self.items[item].name}, .{ .dim = self.items[item].disabled, .highlight = item == self.selected_item }); + } + } + + fn nextItem(self: *Menu, term_io: *TermIO) !void { + var i: usize = self.selected_item; + while (i < self.items.len and (self.items[i].disabled or i == self.selected_item)) { + i += 1; + } + if (!self.items[i].disabled) { + try self.selectItem(i, term_io); + } + } + + fn prevItem(self: *Menu, term_io: *TermIO) !void { + var i: usize = self.selected_item; + while (i > 0 and (self.items[i].disabled or i == self.selected_item)) { + i -= 1; + } + if (!self.items[i].disabled) { + try self.selectItem(i, term_io); + } + } + + fn selectItem(self: *Menu, item: usize, term_io: *TermIO) !void { + const previousSelection = self.selected_item; + self.selected_item = item; + + if (previousSelection != self.selected_item) { + try self.printItem(previousSelection, term_io); + } + try self.printItem(self.selected_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; + } +}; + +pub const TermIO = struct { + stdin: fs.File.Reader, + stdout: std.io.BufferedWriter(4096, fs.File.Writer).Writer, + tty_file: fs.File, + 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(); + } + + pub fn flush(self: *TermIO) !void { + try self.stdout.context.flush(); + } + + 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; + } + + pub fn saveScreen(term_io: *const TermIO) !void { + try term_io.stdout.print("\x1b[s", .{}); + try term_io.stdout.print("\x1b[?47h", .{}); + try term_io.stdout.print("\x1b[?1049h", .{}); + } + + pub fn restoreScreen(term_io: *const TermIO) !void { + try term_io.stdout.print("\x1b[?1049l", .{}); + try term_io.stdout.print("\x1b[?47l", .{}); + try term_io.stdout.print("\x1b[u", .{}); + } + + pub fn clear(term_io: *const TermIO) void { + term_io.stdout.print("\x1b[2J\x1b[H", .{}) catch unreachable; + } + + pub fn setColor(term_io: *const TermIO, background: color.Color, foreground: color.Color) !void { + try term_io.setBackgroundColor(background); + try term_io.setForegroundColor(foreground); + } + + pub fn setBackgroundColor(term_io: *const TermIO, background: color.Color) !void { + if (!background.equal(term_io.current_background)) { + switch (background.type) { + .Default => try term_io.print("\x1b[49m", .{}, .{}), + .RGB => try term_io.print("\x1b[48;2;{};{};{}m", .{ background.red, background.green, background.blue }, .{}), + } + } + } + + pub fn setForegroundColor(term_io: *const TermIO, foreground: color.Color) !void { + if (!foreground.equal(term_io.current_foreground)) { + switch (foreground.type) { + .Default => try term_io.print("\x1b[39m", .{}, .{}), + .RGB => try term_io.print("\x1b[38;2;{};{};{}m", .{ foreground.red, foreground.green, foreground.blue }, .{}), + } + } + } + + pub fn hideCursor(term_io: *const TermIO) void { + term_io.stdout.print("\x1b[?25l", .{}) catch unreachable; + } + + pub fn showCursor(term_io: *const TermIO) void { + term_io.stdout.print("\x1b[?25h", .{}) catch unreachable; + } + + pub fn moveCursor(term_io: *const TermIO, x: usize, y: usize) !void { + try term_io.stdout.print("\x1b[{d};{d}H", .{ y, x }); + } + + pub fn saveTitle(term_io: *const TermIO) !void { + try term_io.stdout.print("\x1b[22;2t", .{}); + } + + pub fn restoreTitle(term_io: *const TermIO) !void { + try term_io.stdout.print("\x1b[23;2t", .{}); + } + + pub fn setTitle(term_io: *const TermIO, title: []const u8) !void { + try term_io.stdout.print("\x1b]0;{s}\x1b\\", .{title}); + } + + pub fn print(term_io: *const TermIO, comptime format: []const u8, args: anytype, flags: PrintFlags) !void { + // try term_io.stdout.print("{s}{s}{s}{s}{s}" ++ format ++ "\x1b[0m", .{ if (flags.bold) "\x1b[1m" else "", if (flags.dim) "\x1b[2m" else "", if (flags.italic) "\x1b[3m" else "", if (flags.underline) "\x1b[4m" else "", if (flags.highlight) "\x1b[7m" else "" } ++ args); + _ = flags; + try term_io.stdout.print(format, args); + } + + pub fn output(term_io: *const TermIO, x: usize, y: usize, comptime format: []const u8, args: anytype, flags: PrintFlags) !void { + // try term_io.stdout.print("\x1b[{d};{d}H{s}{s}{s}{s}{s}" ++ format ++ "\x1b[0m", .{ y, x, if (flags.bold) "\x1b[1m" else "", if (flags.dim) "\x1b[2m" else "", if (flags.italic) "\x1b[3m" else "", if (flags.underline) "\x1b[4m" else "", if (flags.highlight) "\x1b[7m" else "" } ++ args); + _ = flags; + try term_io.stdout.print("\x1b[{d};{d}H" ++ format, .{ y, x } ++ args); + } + + 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; + } + try term_io.output(dims.pos.x, dims.pos.y, "{u}", .{border.top_left}, .{}); + for (0..dims.size.width - 2) |_| { + try term_io.print("{u}", .{border.top}, .{}); + } + try term_io.print("{u}", .{border.top_right}, .{}); + for (1..dims.size.height - 1) |h| { + try term_io.output(dims.pos.x, dims.pos.y + h, "{u}", .{border.left}, .{}); + if (fill) { + try term_io.print("\x1b[{d}X", .{dims.size.width - 2}, .{}); + } + try term_io.output(dims.pos.x + dims.size.width - 1, dims.pos.y + h, "{u}", .{border.right}, .{}); + + if (term_io.stdout.context.buf.len - term_io.stdout.context.end < dims.size.width * 3) { + try term_io.flush(); + _ = term_io.getKey(true); + } + } + try term_io.output(dims.pos.x, dims.pos.y + dims.size.height - 1, "{u}", .{border.bottom_left}, .{}); + for (0..dims.size.width - 2) |_| { + try term_io.print("{u}", .{border.bottom}, .{}); + } + try term_io.print("{u}", .{border.bottom_right}, .{}); + } + + 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) { + error.WouldBlock => if (!block) return .{ .type = KeyType.ASCII, .value = 0 }, + error.EndOfStream => if (!block) return .{ .type = KeyType.ASCII, .value = 0 }, + else => { + std.debug.print("Error: {}\n", .{err}); + unreachable; + }, + } + 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; + } + } + } +}; + +pub fn getTermSize(term_io: *const TermIO) dim.CalculatedDimensions { + 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; + } + + return dim.CalculatedDimensions{ + .pos = .{ .x = 1, .y = 1 }, + .size = .{ .width = ws.ws_col, .height = ws.ws_row }, + }; +} + +pub fn getTermIO() !TermIO { + var bw = std.io.bufferedWriter(std.io.getStdOut().writer()); + return TermIO{ + .stdin = std.io.getStdIn().reader(), + .stdout = bw.writer(), + .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) }; +} diff --git a/zig-out/bin/panes b/zig-out/bin/panes new file mode 100755 index 0000000..a1f18d3 Binary files /dev/null and b/zig-out/bin/panes differ diff --git a/zig-out/lib/libpanes.a b/zig-out/lib/libpanes.a new file mode 100644 index 0000000..e7bf993 Binary files /dev/null and b/zig-out/lib/libpanes.a differ