const std = @import("std"); const os = std.os; const io = std.io; const fs = std.fs; const mem = std.mem; const heap = std.heap; const fmt = std.fmt; const posix = std.posix; pub fn main() !void { var gpa = heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var term = try Terminal.init(allocator); defer term.deinit(); try term.clearScreen(); try term.cursorHide(); var box1 = Terminal.Box.init(allocator); defer box1.deinit(); box1.border_bg = try Terminal.Color.init("#3ace37ff"); box1.top = 0; box1.bottom = 0; box1.left = 0; box1.right = 0; var box2 = Terminal.Box.init(allocator); defer box2.deinit(); box2.border_bg = try Terminal.Color.init("#000000c0"); box2.content = "hi"; box2.top = 1; box2.bottom = 3; box2.left = 20; box2.right = 2; var box3 = Terminal.Box.init(allocator); defer box3.deinit(); box3.border_bg = try Terminal.Color.init("#48d5eaa0"); box3.content = "hello"; box3.content_halign = .center; box3.content_valign = .center; box3.top = 2; box3.bottom = 1; box3.left = 20; box3.right = 20; try term.box.addChild(&box1); try box1.addChild(&box2); try box2.addChild(&box3); while (true) { const events = try term.getEvents(); _ = events; // for (events) |ev| if (ev.system == .int) break; try term.draw(); std.time.sleep(1000); } } pub const Terminal = struct { tty: fs.File, original_termios: os.linux.termios, info: Info, allocator: mem.Allocator, box: Box, events: std.ArrayList(Event), pub fn init(allocator: mem.Allocator) !Terminal { var term = Terminal{ .tty = try fs.openFileAbsolute("/dev/tty", .{ .mode = .read_write }), .original_termios = undefined, .info = try Info.init(allocator), .allocator = allocator, .box = Box.init(allocator), .events = std.ArrayList(Event).init(allocator), }; errdefer term.tty.close(); errdefer term.info.deinit(); try term.uncook(); try attachSignalHandlers(); try term.updateWinSize(); return term; } pub fn deinit(self: *Terminal) void { self.events.deinit(); self.box.deinit(); self.cook() catch @panic("failed to restore termios"); self.cursorShow() catch @panic("failed to unhide cursor"); self.tty.close(); self.info.deinit(); } fn uncook(self: *Terminal) !void { self.original_termios = try posix.tcgetattr(self.tty.handle); var raw = self.original_termios; raw.lflag.ECHO = false; raw.lflag.ICANON = false; // raw.lflag.ISIG = false; raw.lflag.ISIG = true; raw.lflag.IEXTEN = false; raw.iflag.IXON = false; raw.iflag.ICRNL = false; raw.iflag.BRKINT = false; raw.iflag.INPCK = false; raw.iflag.ISTRIP = false; raw.oflag.OPOST = false; raw.cc[@intFromEnum(os.linux.V.TIME)] = 0; raw.cc[@intFromEnum(os.linux.V.MIN)] = 1; try posix.tcsetattr(self.tty.handle, .FLUSH, raw); } fn cook(self: *Terminal) !void { try posix.tcsetattr(self.tty.handle, .FLUSH, self.original_termios); } fn attachSignalHandlers() !void { var act = posix.Sigaction{ .handler = .{ .handler = &S.handlerFn }, .mask = posix.empty_sigset, .flags = 0, }; try posix.sigaction(posix.SIG.INT, &act, null); try posix.sigaction(posix.SIG.USR1, &act, null); try posix.sigaction(posix.SIG.USR2, &act, null); try posix.sigaction(posix.SIG.WINCH, &act, null); } pub fn getEvents(self: *Terminal) ![]Event { if (S.ev) |ev| { switch (ev) { Event.system => { switch (ev.system) { .winch => try self.updateWinSize(), else => try self.events.append(ev), } }, // else => try self.events.append(ev); } S.ev = null; } return try self.events.toOwnedSlice(); } const S = struct { var ev: ?Event = null; fn handlerFn(sig: i32) callconv(.C) void { switch (sig) { posix.SIG.INT => ev = Event{ .system = .int }, posix.SIG.USR1 => ev = Event{ .system = .usr1 }, posix.SIG.USR2 => ev = Event{ .system = .usr2 }, posix.SIG.WINCH => ev = Event{ .system = .winch }, else => {}, } } }; pub const Event = union(enum) { system: enum { int, usr1, usr2, winch, }, }; fn updateWinSize(self: *Terminal) !void { var sz: os.linux.winsize = undefined; 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.rect = .{ .x = 0, .y = 0, .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); } pub fn print(self: *Terminal, comptime format: []const u8, args: anytype) !void { const formatted = try fmt.allocPrint(self.allocator, format, args); defer self.allocator.free(formatted); try self.tty.writeAll(formatted); } pub fn cursorShow(self: *Terminal) !void { try self.info.writeString(.cursor_visible, self.tty.writer(), &[_]u32{}); } pub fn cursorHide(self: *Terminal) !void { try self.info.writeString(.cursor_invisible, self.tty.writer(), &[_]u32{}); } pub fn cursorUp(self: *Terminal) !void { try self.info.writeString(.cursor_up, self.tty.writer(), &[_]u32{}); } pub fn cursorDown(self: *Terminal) !void { try self.info.writeString(.cursor_down, self.tty.writer(), &[_]u32{}); } pub fn cursorLeft(self: *Terminal) !void { try self.info.writeString(.cursor_left, self.tty.writer(), &[_]u32{}); } pub fn cursorRight(self: *Terminal) !void { try self.info.writeString(.cursor_right, self.tty.writer(), &[_]u32{}); } pub fn cursorSet(self: *Terminal, x: u32, y: u32) !void { try self.info.writeString(.cursor_address, self.tty.writer(), &[_]u32{ y, x }); } pub fn blinkOn(self: *Terminal) !void { try self.info.writeString(.enter_blink_mode, self.tty.writer(), &[_]u32{}); } pub fn blinkOff(self: *Terminal) !void { try self.info.writeString(.exit_blink_mode, self.tty.writer(), &[_]u32{}); } pub fn boldOn(self: *Terminal) !void { try self.info.writeString(.enter_bold_mode, self.tty.writer(), &[_]u32{}); } pub fn boldOff(self: *Terminal) !void { try self.info.writeString(.exit_bold_mode, self.tty.writer(), &[_]u32{}); } pub fn italicsOn(self: *Terminal) !void { try self.info.writeString(.enter_italics_mode, self.tty.writer(), &[_]u32{}); } pub fn italicsOff(self: *Terminal) !void { try self.info.writeString(.exit_italics_mode, self.tty.writer(), &[_]u32{}); } pub fn underlineOn(self: *Terminal) !void { try self.info.writeString(.enter_underline_mode, self.tty.writer(), &[_]u32{}); } pub fn underlineOff(self: *Terminal) !void { try self.info.writeString(.exit_underline_mode, self.tty.writer(), &[_]u32{}); } pub fn clearScreen(self: *Terminal) !void { try self.info.writeString(.clear_screen, self.tty.writer(), &[_]u32{}); } // pub fn clearRegion(self: *Terminal, x: u32, y: u32, width: u32, height: u32) !void { // var row = y; // var i: u32 = 0; // while (i < height) { // i += 1; // row += 1; // } // } pub fn setFg(self: *Terminal, color: Color) !void { try self.info.writeString(.set_rgb_foreground, self.tty.writer(), &[_]u32{ @intFromFloat(color.r * 0xff), @intFromFloat(color.g * 0xff), @intFromFloat(color.b * 0xff), }); } pub fn setBg(self: *Terminal, color: Color) !void { try self.info.writeString(.set_rgb_background, self.tty.writer(), &[_]u32{ @intFromFloat(color.r * 0xff), @intFromFloat(color.g * 0xff), @intFromFloat(color.b * 0xff), }); } pub const Info = @import("terminfo.zig"); pub const Color = @import("color.zig").RGBA; pub const Box = struct { parent: ?*Box, children: std.ArrayList(*Box), rect: ?Rect = null, // TODO: maybe unionize this with .parent dirty: bool = true, left: u32, right: u32, top: u32, bottom: u32, border_type: enum { line, bg } = .bg, border_char: u8 = ' ', border_bg: Color = Color{ .r = 0.0, .g = 0.0, .b = 0.0 }, border_fg: Color = Color{ .r = 1.0, .g = 1.0, .b = 1.0 }, content: []const u8 = "", content_halign: enum { left, center, right } = .left, content_valign: enum { top, center, bottom } = .top, pub fn init(allocator: mem.Allocator) Box { return .{ .parent = null, .children = std.ArrayList(*Box).init(allocator), .left = 2, .right = 2, .top = 1, .bottom = 1, }; } pub fn deinit(self: *Box) void { for (self.children.items) |child| child.deinit(); self.children.deinit(); } pub fn addChild(self: *Box, child: *Box) !void { std.debug.assert(child.parent == null); child.parent = self; try self.children.append(child); } pub fn removeChild(self: *Box, child: *Box) !void { _ = self; _ = child; } pub fn removeChildByIndex(self: *Box, child: usize) !void { _ = self; _ = child; } pub fn moveChild(self: *Box, child: *Box, idx: usize) !void { _ = self; _ = child; _ = idx; } pub fn moveChildByIndex(self: *Box, child: usize, idx: usize) !void { _ = self; _ = child; _ = 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) { const rect = self.getRect(); switch (self.border_type) { .line => { try term.setFg(self.getBorderFgColor()); try term.setBg(self.getBorderBgColor()); var x: u32 = 0; var y: u32 = 0; try term.cursorSet(rect.x, rect.y); try term.print("┌", .{}); while (x < rect.w - 2) : (x += 1) try term.print("─", .{}); try term.print("┐", .{}); y += 1; while (y < rect.h - 1) : (y += 1) { try term.cursorSet(rect.x, rect.y + y); try term.print("│", .{}); try term.cursorSet(rect.x + rect.w - 1, rect.y + y); try term.print("│", .{}); } x = 0; 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 => { try term.setBg(self.getBorderBgColor()); 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(" ", .{}); } try term.cursorSet( switch (self.content_halign) { .left => rect.x, .center => rect.x + (rect.w / 2) - (@as(u32, @intCast(self.content.len)) / 2), .right => rect.x + rect.w - (@as(u32, @intCast(self.content.len))), }, switch (self.content_valign) { .top => rect.y, .center => rect.y + (rect.h / 2), .bottom => rect.y + rect.h - 1, }, ); try term.print("{s}", .{self.content}); }, } self.dirty = false; } for (self.children.items) |child| try child.draw(term); } fn getRect(self: *Box) Rect { 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.w + parent_rect.x - rect.x - self.right; rect.h = parent_rect.h + parent_rect.y - rect.y - self.bottom; } else return self.rect.?; return rect; } const Rect = struct { x: u32, y: u32, w: u32, h: u32 }; fn getBorderFgColor(self: *Box) Color { if (self.parent) |parent| { const parent_color = parent.getBorderFgColor(); return parent_color.blend(&self.border_fg); } else return self.border_fg; } fn getBorderBgColor(self: *Box) Color { if (self.parent) |parent| { const parent_color = parent.getBorderBgColor(); return parent_color.blend(&self.border_bg); } else return self.border_bg; } }; };