diff --git a/.gitignore b/.gitignore index 3389c86..2a3a3ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .zig-cache/ zig-out/ +panes.log diff --git a/justfile b/justfile new file mode 100644 index 0000000..2c1deb4 --- /dev/null +++ b/justfile @@ -0,0 +1,15 @@ +build OPTIMIZE="Debug": + zig build -Doptimize={{OPTIMIZE}} + +run OPTIMIZE="Debug": (build OPTIMIZE) + ./zig-out/bin/panes + cat panes.log + +debug: (build "Debug") + gdb ./zig-out/bin/panes + +test: + zig build test --summary new + +clean: + rm -r .zig-cache zig-out diff --git a/src/borders.zig b/src/borders.zig index 8990489..6809449 100644 --- a/src/borders.zig +++ b/src/borders.zig @@ -52,3 +52,14 @@ pub const BigBorder = Border{ .left = '▌', .right = '▐', }; + +pub const RoundBorder = Border{ + .top_left = '╭', + .top_right = '╮', + .bottom_left = '╰', + .bottom_right = '╯', + .top = '─', + .bottom = '─', + .left = '│', + .right = '│', +}; diff --git a/src/button.zig b/src/button.zig new file mode 100644 index 0000000..fdeee21 --- /dev/null +++ b/src/button.zig @@ -0,0 +1,65 @@ +const std = @import("std"); +const log = @import("log.zig"); +const dim = @import("dimensions.zig"); +const pane = @import("pane.zig"); +const term = @import("term.zig"); +const Pane = pane.Pane; +const TermIO = term.TermIO; + +pub const Button = struct { + pane: Pane, + text: []const u8, + callback: *const fn () void, + + pub fn init(parent: ?*Pane, dimensions: dim.Dimensions, text: []const u8, callback: *const fn () void) Button { + return .{ + .pane = .{ + .parent = parent, + .children = null, + .dimensions = dimensions, + .focusable = true, + .vtable = .{ + .draw = draw, + .focus = focus, + .update = update, + }, + }, + .text = text, + .callback = callback, + }; + } + + fn draw(pane_ptr: *Pane, term_io: *TermIO) !void { + var self: *Button = @fieldParentPtr("pane", pane_ptr); + + var writer = self.pane.writer(term_io); + self.pane.cursor = .{ .x = 0, .y = 0 }; + self.pane.moveCursor(term_io); + + try writer.writeAll(self.text); + term_io.flush(); + } + + fn update(pane_ptr: *Pane, term_io: *TermIO, key: term.Key) !bool { + _ = term_io; + var self: *Button = @fieldParentPtr("pane", pane_ptr); + + if (key.type == .ASCII and key.value == '\r') { + self.callback(); + } + + return false; + } + + fn focus(pane_ptr: *Pane, term_io: *TermIO) !void { + var self: *Button = @fieldParentPtr("pane", pane_ptr); + + var writer = self.pane.writer(term_io); + self.pane.cursor = .{ .x = 0, .y = 0 }; + self.pane.moveCursor(term_io); + + term_io.enableFormats(.{ .highlight = pane_ptr.focused }); + try writer.writeAll(self.text); + term_io.disableFormats(.{ .highlight = true }); + } +}; diff --git a/src/colors.zig b/src/colors.zig index 8973abc..d5afe0f 100644 --- a/src/colors.zig +++ b/src/colors.zig @@ -1,5 +1,5 @@ pub const Color = struct { - type: enum { Default, RGB }, + type: enum { Inherit, Default, RGB }, red: u8, green: u8, blue: u8, @@ -17,6 +17,13 @@ pub const Color = struct { } }; +pub const Inherit = Color{ + .type = .Inherit, + .red = undefined, + .green = undefined, + .blue = undefined, +}; + pub const Default = Color{ .type = .Default, .red = undefined, diff --git a/src/dimensions.zig b/src/dimensions.zig index 103da5b..3ad75f5 100644 --- a/src/dimensions.zig +++ b/src/dimensions.zig @@ -20,22 +20,25 @@ pub const Size = struct { height: usize, }; +pub const Dimension = struct { + value: usize, + type: enum { Relative, Absolute } = .Absolute, +}; + pub const Dimensions = struct { - size_type: enum { Relative, Absolute }, - size: Size, - anchor: Anchor, + x: Dimension, + y: Dimension, + anchor: Anchor = .TopLeft, }; pub const CalculatedDimensions = struct { size: Size, + internal_size: Size, pos: Position, + internal_pos: Position, }; -pub const Fill = Dimensions{ - .size_type = .Relative, - .anchor = .TopLeft, - .size = .{ - .width = 100, - .height = 100, - }, +pub const Fill = Dimension{ + .type = .Relative, + .value = 100, }; diff --git a/src/line.zig b/src/line.zig new file mode 100644 index 0000000..7856543 --- /dev/null +++ b/src/line.zig @@ -0,0 +1,38 @@ +const dim = @import("dimensions.zig"); +const pane = @import("pane.zig"); +const term = @import("term.zig"); +const borders = @import("borders.zig"); +const TermIO = term.TermIO; + +pub const HorizontalLine = struct { + pane: pane.Pane, + border: borders.Border, + + pub fn create(parent: *pane.Pane, dimensions: dim.Dimensions, border: borders.Border) HorizontalLine { + return .{ + .pane = pane.Pane{ + .parent = parent, + .children = null, + .dimensions = dimensions, + .vtable = .{ + .draw = draw, + }, + }, + .border = border, + }; + } + + fn draw(pane_ptr: *pane.Pane, term_io: *TermIO) !void { + const self: *HorizontalLine = @fieldParentPtr("pane", pane_ptr); + const y = pane_ptr.calcDims.size.height / 2; + pane_ptr.cursor = .{ .x = 0, .y = y }; + pane_ptr.moveCursor(term_io); + for (0..pane_ptr.calcDims.size.width) |_| { + term_io.print("{u}", .{self.border.top}); + } + } +}; + +pub const VerticalLine = struct { + pane: pane.Pane, +}; diff --git a/src/log.zig b/src/log.zig new file mode 100644 index 0000000..8759a44 --- /dev/null +++ b/src/log.zig @@ -0,0 +1,153 @@ +const std = @import("std"); +const fs = std.fs; + +var log_level: Level = .Debug; +var writer: ?std.fs.File.Writer = null; + +const Level = enum { + Debug, + Info, + Warn, + Error, + Disable, +}; + +pub fn init(log_file: []const u8, level: Level) !void { + log_level = level; + const file = try fs.cwd().createFile(log_file, .{}); + writer = file.writer(); +} + +pub fn setLevel(level: Level) void { + log_level = level; +} + +pub fn deinit() void { + if (writer) |w| { + w.context.close(); + } +} + +pub fn debug(comptime format: []const u8, args: anytype) void { + if (@intFromEnum(log_level) <= @intFromEnum(Level.Debug)) { + if (writer) |w| { + _ = w.write("[Debug] ") catch {}; + std.fmt.format(w, format, args) catch {}; + _ = w.writeByte('\n') catch {}; + } + } +} + +pub fn info(comptime format: []const u8, args: anytype) void { + if (@intFromEnum(log_level) <= @intFromEnum(Level.Info)) { + if (writer) |w| { + _ = w.write("\x1b[1m[Info] ") catch {}; + std.fmt.format(w, format, args) catch {}; + _ = w.write("\x1b[0m\n") catch {}; + } + } +} + +pub fn warn(comptime format: []const u8, args: anytype) void { + if (@intFromEnum(log_level) <= @intFromEnum(Level.Warn)) { + if (writer) |w| { + _ = w.write("\x1b[1;33m[Warn] ") catch {}; + std.fmt.format(w, format, args) catch {}; + _ = w.write("\x1b[0m\n") catch {}; + } + } +} + +pub fn err(comptime format: []const u8, args: anytype) void { + if (@intFromEnum(log_level) <= @intFromEnum(Level.Error)) { + if (writer) |w| { + _ = w.write("\x1b[1;31m[Error] ") catch {}; + std.fmt.format(w, format, args) catch {}; + _ = w.write("\x1b[0m\n") catch {}; + } + } +} + +test "debug" { + try init("test.log", .Debug); + setLevel(.Debug); + + debug("Hello", .{}); + info("Hello", .{}); + warn("Hello", .{}); + err("Hello", .{}); + + deinit(); + + const file = try fs.cwd().openFile("test.log", .{}); + defer file.close(); + const contents = try file.readToEndAlloc(std.testing.allocator, 1024 * 1024); + defer std.testing.allocator.free(contents); + + try std.testing.expectEqualStrings("[Debug] Hello\n\x1b[1m[Info] Hello\x1b[0m\n\x1b[1;33m[Warn] Hello\x1b[0m\n\x1b[1;31m[Error] Hello\x1b[0m\n", contents); + + try fs.cwd().deleteFile("test.log"); +} + +test "info" { + try init("test.log", .Info); + setLevel(.Info); + + debug("Hello", .{}); + info("Hello", .{}); + warn("Hello", .{}); + err("Hello", .{}); + + deinit(); + + const file = try fs.cwd().openFile("test.log", .{}); + defer file.close(); + const contents = try file.readToEndAlloc(std.testing.allocator, 1024 * 1024); + defer std.testing.allocator.free(contents); + + try std.testing.expectEqualStrings("\x1b[1m[Info] Hello\x1b[0m\n\x1b[1;33m[Warn] Hello\x1b[0m\n\x1b[1;31m[Error] Hello\x1b[0m\n", contents); + + try fs.cwd().deleteFile("test.log"); +} + +test "warn" { + try init("test.log", .Warn); + setLevel(.Warn); + + debug("Hello", .{}); + info("Hello", .{}); + warn("Hello", .{}); + err("Hello", .{}); + + deinit(); + + const file = try fs.cwd().openFile("test.log", .{}); + defer file.close(); + const contents = try file.readToEndAlloc(std.testing.allocator, 1024 * 1024); + defer std.testing.allocator.free(contents); + + try std.testing.expectEqualStrings("\x1b[1;33m[Warn] Hello\x1b[0m\n\x1b[1;31m[Error] Hello\x1b[0m\n", contents); + + try fs.cwd().deleteFile("test.log"); +} + +test "error" { + try init("test.log", .Error); + setLevel(.Error); + + debug("Hello", .{}); + info("Hello", .{}); + warn("Hello", .{}); + err("Hello", .{}); + + deinit(); + + const file = try fs.cwd().openFile("test.log", .{}); + defer file.close(); + const contents = try file.readToEndAlloc(std.testing.allocator, 1024 * 1024); + defer std.testing.allocator.free(contents); + + try std.testing.expectEqualStrings("\x1b[1;31m[Error] Hello\x1b[0m\n", contents); + + try fs.cwd().deleteFile("test.log"); +} diff --git a/src/main.zig b/src/main.zig index 317d824..f10727e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,79 +5,108 @@ const pane = @import("pane.zig"); const color = @import("colors.zig"); const dim = @import("dimensions.zig"); const menu = @import("menu.zig"); +const btn = @import("button.zig"); +const log = @import("log.zig"); +const line = @import("line.zig"); var term_io: term.TermIO = undefined; +var side_menu: menu.Menu = undefined; +var m: menu.Menu = 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); } +fn on_click() void { + log.debug("Button was pressed", .{}); +} + +fn on_side_select(item: usize) void { + log.debug("Side menu: Item {d} was selected", .{item}); + if (item == 1) { + pane.focus(&m.pane, &term_io); + } +} + +fn on_select(item: usize) void { + log.debug("Main menu: Item {d} was selected", .{item}); + if (item == 1) { + pane.focus(&side_menu.pane, &term_io); + } +} + pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); defer { if (gpa.deinit() == .leak) { + log.warn("Memory was leaked D:", .{}); std.debug.print("Memory was leaked D:\n", .{}); } } - term_io = try term.getTermIO(); + try log.init("panes.log", .Debug); + defer log.deinit(); - try pane.init(&term_io); - defer pane.cleanup(&term_io); + const stdin = std.io.getStdIn().reader(); + var stdout = std.io.bufferedWriter(std.io.getStdOut().writer().any()); + term_io = try term.getTermIO(stdin.any(), stdout.writer()); var top = pane.Pane{ - .dimensions = dim.Fill, + .dimensions = .{ + .x = dim.Fill, + .y = 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 items2 = [_]menu.MenuItem{ .{ .name = "Item 1", .value = 1 }, .{ .name = "Item ab", .value = 2 } }; + side_menu = menu.Menu.init(&top, dim.Dimensions{ .anchor = .TopLeft, .x = .{ .value = 20, .type = .Relative }, .y = .{ .value = 100, .type = .Relative } }, .{ .title = "Menu", .align_text = .Right, .expand_highlight = true }, &items2, on_side_select); + side_menu.pane.background = color.RGB(80, 80, 80); + side_menu.pane.foreground = top.foreground; + var child = pane.Pane{ .parent = &top, .children = std.ArrayList(*pane.Pane).init(allocator), .dimensions = .{ - .anchor = .Center, - .size_type = .Relative, - .size = .{ .width = 50, .height = 50 }, + .anchor = .CenterRight, + .x = .{ .value = 80, .type = .Relative }, + .y = .{ .value = 100, .type = .Relative }, }, .border = borders.BoldBorder, .background = color.RGB(125, 0, 125), .foreground = color.RGB(255, 125, 10), }; - var items = [_]menu.MenuItem{ .{ .name = "Item 1", .value = 1 }, .{ .name = "Item 2", .value = 2 } }; - var m = menu.Menu.init(&child, dim.Fill, "Test", &items); + var button = btn.Button.init(&child, .{ .anchor = .BottomLeft, .x = .{ .value = 8 }, .y = .{ .value = 1 } }, "