From 8cf18400e26f822aeb9c5ea4483518a698246595 Mon Sep 17 00:00:00 2001
From: Jeeves <guydoodlesdev@gmail.com>
Date: Sun, 7 Apr 2024 14:03:38 -0600
Subject: [PATCH] colors

---
 src/color.zig    |  49 +++++++++++++
 src/main.zig     | 124 +++++++++++++++++---------------
 src/terminfo.zig | 184 +++++++++++++++++++++++++++++++++++++++++++++--
 3 files changed, 297 insertions(+), 60 deletions(-)
 create mode 100644 src/color.zig

diff --git a/src/color.zig b/src/color.zig
new file mode 100644
index 0000000..62fad2e
--- /dev/null
+++ b/src/color.zig
@@ -0,0 +1,49 @@
+const std = @import("std");
+const fmt = std.fmt;
+
+pub const RGB = struct {
+    r: u8,
+    g: u8,
+    b: u8,
+
+    pub fn init(comptime string: []const u8) !RGB {
+        std.debug.assert(string.len == 6 or string.len == 7);
+        comptime var index: usize = 0;
+        if (string[0] == '#') index += 1;
+        return .{
+            .r = try fmt.parseInt(u8, string[index .. index + 2], 16),
+            .g = try fmt.parseInt(u8, string[index + 2 .. index + 4], 16),
+            .b = try fmt.parseInt(u8, string[index + 4 .. index + 6], 16),
+        };
+    }
+};
+
+// pub const Default256 = [_]RGB{
+//     RGB{ .r = 0, .g = 0, .b = 0 },
+//     RGB{ .r = 204, .g = 4, .b = 3 },
+//     RGB{ .r = 25, .g = 203, .b = 0 },
+//     RGB{ .r = 206, .g = 203, .b = 0 },
+//     RGB{ .r = 13, .g = 115, .b = 204 },
+//     RGB{ .r = 203, .g = 30, .b = 209 },
+//     RGB{ .r = 13, .g = 205, .b = 205 },
+//     RGB{ .r = 221, .g = 221, .b = 221 },
+// };
+
+test "RGB init from string" {
+    const red = try RGB.init("#ff0000");
+    try std.testing.expectEqual(0xff, red.r);
+    try std.testing.expectEqual(0x00, red.g);
+    try std.testing.expectEqual(0x00, red.b);
+
+    const green = try RGB.init("00ff00");
+    try std.testing.expectEqual(0x00, green.r);
+    try std.testing.expectEqual(0xff, green.g);
+    try std.testing.expectEqual(0x00, green.b);
+
+    const blue = try RGB.init("#0000ff");
+    try std.testing.expectEqual(0x00, blue.r);
+    try std.testing.expectEqual(0x00, blue.g);
+    try std.testing.expectEqual(0xff, blue.b);
+
+    try std.testing.expectError(fmt.ParseIntError.InvalidCharacter, RGB.init("xyzxyz"));
+}
diff --git a/src/main.zig b/src/main.zig
index 0585017..3e91359 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -18,22 +18,32 @@ pub fn main() !void {
 
     var box1 = Terminal.Box.init(allocator);
     defer box1.deinit();
-    box1.content = "hi";
-    box1.top = 0;
-    box1.bottom = 0;
-    box1.left = 0;
-    box1.right = 0;
+    box1.border_fg = try Terminal.Color.init("#ff8855");
+    box1.top = 1;
+    box1.bottom = 1;
+    box1.left = 1;
+    box1.right = 1;
 
     var box2 = Terminal.Box.init(allocator);
     defer box2.deinit();
-    box2.content = "hi";
-    box2.top = 0;
-    box2.bottom = 0;
-    box2.left = 0;
-    box2.right = 0;
+    box2.border_fg = try Terminal.Color.init("#aaff55");
+    box2.top = 1;
+    box2.bottom = 1;
+    box2.left = 1;
+    box2.right = 1;
+
+    var box3 = Terminal.Box.init(allocator);
+    defer box3.deinit();
+    box3.border_bg = try Terminal.Color.init("#aa33ff");
+    box3.border_type = .bg;
+    box3.top = 1;
+    box3.bottom = 1;
+    box3.left = 1;
+    box3.right = 1;
 
     try term.box.addChild(&box1);
     try box1.addChild(&box2);
+    try box2.addChild(&box3);
 
     while (true) {
         const events = try term.getEvents();
@@ -93,7 +103,6 @@ pub const Terminal = struct {
         };
         errdefer term.tty.close();
         errdefer term.info.deinit();
-        term.box.position = .{ .x = 0, .y = 0, .width = 50, .height = 20 };
         try term.uncook();
         try attachSignalHandlers();
         try term.updateWinSize();
@@ -192,16 +201,18 @@ pub const Terminal = struct {
         const ret = os.linux.ioctl(0, os.linux.T.IOCGWINSZ, @intFromPtr(&sz));
         // std.debug.print("ret: {d}, {any}\r\n", .{ ret, sz });
         if (ret == 0) {
-            self.box.position = .{
+            self.box.rect = .{
                 .x = 0,
                 .y = 0,
-                .height = @intCast(sz.ws_row),
-                .width = @intCast(sz.ws_col),
+                .h = @intCast(sz.ws_row),
+                .w = @intCast(sz.ws_col),
             };
+            self.box.makeDirty();
         } else unreachable; // TODO: handle else case
     }
 
     pub fn draw(self: *Terminal) !void {
+        if (self.box.dirty) try self.clearScreen();
         try self.box.draw(self);
     }
 
@@ -277,18 +288,22 @@ pub const Terminal = struct {
     //     }
     // }
 
+    pub fn setFg(self: *Terminal, color: Color) !void {
+        try self.info.writeString(.set_rgb_foreground, self.tty.writer(), &[_]u32{ @intCast(color.r), @intCast(color.g), @intCast(color.b) });
+    }
+
+    pub fn setBg(self: *Terminal, color: Color) !void {
+        try self.info.writeString(.set_rgb_background, self.tty.writer(), &[_]u32{ @intCast(color.r), @intCast(color.g), @intCast(color.b) });
+    }
+
     pub const Info = @import("terminfo.zig");
+    pub const Color = @import("color.zig").RGB;
 
     pub const Box = struct {
         parent: ?*Box,
         children: std.ArrayList(*Box),
 
-        position: ?struct {
-            x: u32,
-            y: u32,
-            width: u32,
-            height: u32,
-        } = null,
+        rect: ?Rect = null, // TODO: maybe unionize this with .parent
         dirty: bool = true,
 
         left: u32,
@@ -298,8 +313,8 @@ pub const Terminal = struct {
 
         border_type: enum { line, bg } = .line,
         border_char: u8 = ' ',
-        border_bg: i16 = -1,
-        border_fg: i16 = -1,
+        border_bg: Color = Color{ .r = 0, .g = 0, .b = 0 },
+        border_fg: Color = Color{ .r = 0xff, .g = 0xff, .b = 0xff },
 
         content: []const u8 = "",
 
@@ -322,20 +337,6 @@ pub const Terminal = struct {
         pub fn addChild(self: *Box, child: *Box) !void {
             std.debug.assert(child.parent == null);
             child.parent = self;
-
-            std.debug.assert(self.position != null);
-            child.position = if (self.border_type == .line) .{
-                .x = self.position.?.x + child.left,
-                .y = self.position.?.y + child.top,
-                .width = self.position.?.width - child.right - child.left,
-                .height = self.position.?.height,
-            } else .{
-                .x = self.position.?.x + child.left,
-                .y = self.position.?.y + child.top,
-                .width = self.position.?.width,
-                .height = self.position.?.height,
-            };
-
             try self.children.append(child);
         }
 
@@ -361,11 +362,20 @@ pub const Terminal = struct {
             _ = idx;
         }
 
+        fn makeDirty(self: *Box) void {
+            self.dirty = true;
+            for (self.children.items) |child| child.makeDirty();
+        }
+
         pub fn draw(self: *Box, term: *Terminal) !void {
             if (self.dirty) {
+                // if (self != &term.box) self.calcRect();
                 const rect = self.getRect();
+                // std.debug.print("{any}\r\n", .{rect});
                 switch (self.border_type) {
                     .line => {
+                        try term.setFg(self.border_fg);
+                        try term.setBg(self.border_bg);
                         var x: u32 = 0;
                         var y: u32 = 0;
                         try term.cursorSet(rect.x, rect.y);
@@ -376,16 +386,25 @@ pub const Terminal = struct {
                         while (y < rect.h - 1) : (y += 1) {
                             try term.cursorSet(rect.x, rect.y + y);
                             try term.print("│", .{});
-                            try term.cursorSet(rect.w, rect.y + y);
+                            try term.cursorSet(rect.x + rect.w - 1, rect.y + y);
                             try term.print("│", .{});
                         }
                         x = 0;
-                        try term.cursorSet(rect.x, rect.h);
+                        try term.cursorSet(rect.x, rect.y + rect.h - 1);
                         try term.print("└", .{});
                         while (x < rect.w - 2) : (x += 1) try term.print("─", .{});
                         try term.print("┘", .{});
                     },
-                    .bg => {},
+                    .bg => {
+                        try term.setBg(self.border_bg);
+                        var y: u32 = 0;
+                        try term.cursorSet(rect.x, rect.y);
+                        while (y < rect.h) : (y += 1) {
+                            try term.cursorSet(rect.x, rect.y + y);
+                            var x: u32 = 0;
+                            while (x < rect.w) : (x += 1) try term.print(" ", .{});
+                        }
+                    },
                 }
                 self.dirty = false;
             }
@@ -393,25 +412,18 @@ pub const Terminal = struct {
         }
 
         fn getRect(self: *Box) Rect {
-            var rect = Rect{
-                .x = self.position.?.x,
-                .y = self.position.?.y,
-                .w = self.position.?.width,
-                .h = self.position.?.height,
-            };
-            // if (self.border_type == .line) {
-            //     rect.x += 1;
-            //     rect.y += 1;
-            //     rect.w -= 2;
-            //     rect.h -= 2;
-            // }
+            var rect: Rect = undefined;
             if (self.parent) |parent| {
                 const parent_rect = parent.getRect();
-                rect.x += parent_rect.x + self.left;
-                rect.y += parent_rect.y + self.top;
-                rect.w += parent_rect.x + self.right;
-                rect.h -= parent_rect.y + self.bottom;
-            }
+                rect.x = parent_rect.x + self.left;
+                rect.y = parent_rect.y + self.top;
+                rect.w = parent_rect.w + parent_rect.x - rect.x - self.right;
+                rect.h = parent_rect.h + parent_rect.y - rect.y - self.bottom;
+                // if (parent.parent) |p| {
+                //     rect.w += p.rect.?.x;
+                //     rect.h += p.rect.?.y;
+                // }
+            } else return self.rect.?;
             return rect;
         }
         const Rect = struct { x: u32, y: u32, w: u32, h: u32 };
diff --git a/src/terminfo.zig b/src/terminfo.zig
index 019bf61..306f0c7 100644
--- a/src/terminfo.zig
+++ b/src/terminfo.zig
@@ -200,7 +200,13 @@ pub fn writeString(self: *Self, string: String, writer: anytype, arguments: []co
         defer stack.deinit();
         var args = try self.allocator.dupe(u32, arguments);
         defer self.allocator.free(args);
+        var state: enum { none, expression, thenpart, elsepart } = .none;
+        var cond: ?bool = null;
 
+        // \E[ %?
+        //     %p1 %{8} %< %t3 %p1 %d
+        //     %e %p1 %{16} %< %t9 %p1 %{8} %- %d %e38;5; %p1 %d
+        // %; m
         var i: usize = 0;
         while (i < out.len) {
             if (out[i] == '%') {
@@ -222,8 +228,102 @@ pub fn writeString(self: *Self, string: String, writer: anytype, arguments: []co
                         i += 1;
                         try fmt.format(stack.writer(), "{d}", .{arg});
                     },
-                    'P' => {},
-                    'g' => {},
+                    '?' => {
+                        std.debug.assert(state == .none);
+                        std.debug.assert(cond == null);
+                        state = .expression;
+                    },
+                    't' => {
+                        std.debug.assert(state == .expression);
+                        std.debug.assert(cond == null);
+                        state = .thenpart;
+
+                        const res = stack.pop();
+                        cond = res != 0;
+
+                        if (!cond.?) {
+                            const end_idx = mem.indexOfPos(u8, out, i + 2, "%;");
+                            const else_idx = mem.indexOfPos(u8, out, i + 2, "%e");
+                            if (end_idx) |end| {
+                                if (else_idx) |el| {
+                                    if (el < end)
+                                        i = el
+                                    else
+                                        i = end;
+                                } else i = end;
+                            } else return error.InvalidFormatChar;
+                        }
+                    },
+                    'e' => {
+                        std.debug.assert(state == .thenpart);
+                        state = .elsepart;
+                        if (cond.?) {
+                            const end_idx = mem.indexOfPos(u8, out, i + 2, "%;");
+                            if (end_idx) |end| {
+                                i = end;
+                            } else return error.InvalidFormatChar;
+                        }
+                    },
+                    ';' => {
+                        std.debug.assert(state == .thenpart or state == .elsepart);
+                        state = .none;
+                        cond = null;
+                    },
+                    '{' => {
+                        const end = mem.indexOfScalarPos(u8, out, i + 2, '}');
+                        if (end) |e| {
+                            const num = try fmt.parseInt(u8, out[i + 2 .. e], 10);
+                            try stack.append(num);
+                        }
+                    },
+                    '+' => {
+                        const a = stack.pop();
+                        const b = stack.pop();
+                        try stack.append(a + b);
+                    },
+                    '-' => {
+                        const a = stack.pop();
+                        const b = stack.pop();
+                        try stack.append(a - b);
+                    },
+                    '*' => {
+                        const a = stack.pop();
+                        const b = stack.pop();
+                        try stack.append(a * b);
+                    },
+                    '/' => {
+                        const a = stack.pop();
+                        const b = stack.pop();
+                        try stack.append(a / b);
+                    },
+                    'm' => {
+                        const a = stack.pop();
+                        const b = stack.pop();
+                        try stack.append(a % b);
+                    },
+                    '=' => {
+                        const a = stack.pop();
+                        const b = stack.pop();
+                        try stack.append(if (a == b) 1 else 0);
+                    },
+                    '>' => {
+                        const a = stack.pop();
+                        const b = stack.pop();
+                        try stack.append(if (a > b) 1 else 0);
+                    },
+                    '<' => {
+                        const a = stack.pop();
+                        const b = stack.pop();
+                        try stack.append(if (a < b) 1 else 0);
+                    },
+                    // '!' => {
+                    //     const a = stack.pop();
+                    //     try stack.append(!a);
+                    // },
+                    '~' => {
+                        const a = stack.pop();
+                        try stack.append(~a);
+                    },
                     'i' => {
                         args[0] += 1;
                         args[1] += 1;
@@ -541,6 +641,41 @@ pub const String = enum {
     xon_character,
     zero_motion,
 
+    alt_scancode_esc,
+    bit_image_carriage_return,
+    bit_image_newline,
+    bit_image_repeat,
+    char_set_names,
+    code_set_init,
+    color_names,
+    define_bit_image_region,
+    device_type,
+    display_pc_char,
+    end_bit_image_region,
+    enter_pc_charset_mode,
+    enter_scancode_mode,
+    exit_pc_charset_mode,
+    exit_scancode_mode,
+    get_mouse,
+    key_mouse,
+    mouse_info,
+    pc_term_options,
+    pkey_plab,
+    req_mouse_pos,
+    scancode_escape,
+    set0_des_seq,
+    set1_des_seq,
+    set2_des_seq,
+    set3_des_seq,
+    set_a_background,
+    set_a_foreground,
+    set_rgb_background,
+    set_rgb_foreground,
+    set_color_band,
+    set_lr_margin,
+    set_page_length,
+    set_tb_margin,
+
     pub fn toCapName(self: String) ?[]const u8 {
         return switch (self) {
             .acs_chars => "acsc",
@@ -632,7 +767,6 @@ pub const String = enum {
             .fixed_pause => "pause",
             .flash_hook => "hook",
             .flash_screen => "flash",
-
             .form_feed => "ff",
             .from_status_line => "fsl",
             .goto_window => "wingo",
@@ -737,6 +871,41 @@ pub const String = enum {
             .xoff_character => "xoffc",
             .xon_character => "xonc",
             .zero_motion => "zerom",
+
+            .alt_scancode_esc => "scesa",
+            .bit_image_carriage_return => "bicr",
+            .bit_image_newline => "binel",
+            .bit_image_repeat => "birep",
+            .char_set_names => "csnm",
+            .code_set_init => "csin",
+            .color_names => "colornm",
+            .define_bit_image_region => "defbi",
+            .device_type => "devt",
+            .display_pc_char => "dispt",
+            .end_bit_image_region => "endbi",
+            .enter_pc_charset_mode => "smpch",
+            .enter_scancode_mode => "smsc",
+            .exit_pc_charset_mode => "rmpch",
+            .exit_scancode_mode => "rmsc",
+            .get_mouse => "getm",
+            .key_mouse => "kmous",
+            .mouse_info => "minfo",
+            .pc_term_options => "pctrm",
+            .pkey_plab => "pfxl",
+            .req_mouse_pos => "reqmp",
+            .scancode_escape => "scesc",
+            .set0_des_seq => "s0ds",
+            .set1_des_seq => "s1ds",
+            .set2_des_seq => "s2ds",
+            .set3_des_seq => "s3ds",
+            .set_a_background => "setab",
+            .set_a_foreground => "setaf",
+            .set_rgb_background => null,
+            .set_rgb_foreground => null,
+            .set_color_band => "setcolor",
+            .set_lr_margin => "smglr",
+            .set_page_length => "slines",
+            .set_tb_margin => "smgtb",
             // else => "",
         };
     }
@@ -758,12 +927,14 @@ pub const String = enum {
             .exit_blink_mode => "\x1b[25m",
             .exit_reverse_mode => "\x1b[27m",
             .exit_secure_mode => "\x1b[28m",
+            .set_rgb_background => "\x1b[48;2;%p1%d;%p2%d;%p3%dm",
+            .set_rgb_foreground => "\x1b[38;2;%p1%d;%p2%d;%p3%dm",
             else => null,
         };
     }
 };
 
-test "parse terminfo" {
+test "terminfo" {
     var terminfo = try parse(std.testing.allocator,
         \\#	Reconstructed via infocmp from file: /nix/store/c1d0bgq6whz4khqxncmqikpdsxmr1szw-kitty-0.32.2/lib/kitty/terminfo/x/xterm-kitty
         \\xterm-kitty|KovIdTTY,
@@ -819,4 +990,9 @@ test "parse terminfo" {
         \\	u8=\E[?%[;0123456789]c, u9=\E[c, vpa=\E[%i%p1%dd,
     );
     defer terminfo.deinit();
+
+    var output = std.ArrayList(u8).init(std.testing.allocator);
+    defer output.deinit();
+    try terminfo.writeString(.set_rgb_foreground, output.writer(), &[_]u32{ 32, 32, 32 });
+    // std.debug.print("{s}\n", .{output.items[1..]});
 }