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..]}); }