a modern tui library written in zig
at v0.2.1 177 lines 5.0 kB view raw
1//! TTY implementation conforming to posix standards 2const Posix = @This(); 3 4const std = @import("std"); 5const builtin = @import("builtin"); 6 7const posix = std.posix; 8const Winsize = @import("../main.zig").Winsize; 9 10/// the original state of the terminal, prior to calling makeRaw 11termios: posix.termios, 12 13/// The file descriptor of the tty 14fd: posix.fd_t, 15 16pub const SignalHandler = struct { 17 context: *anyopaque, 18 callback: *const fn (context: *anyopaque) void, 19}; 20 21/// global signal handlers 22var handlers: [8]SignalHandler = undefined; 23var handler_mutex: std.Thread.Mutex = .{}; 24var handler_idx: usize = 0; 25 26/// global tty instance, used in case of a panic. Not guaranteed to work if 27/// for some reason there are multiple TTYs open under a single vaxis 28/// compilation unit - but this is better than nothing 29pub var global_tty: ?Posix = null; 30 31/// initializes a Tty instance by opening /dev/tty and "making it raw". A 32/// signal handler is installed for SIGWINCH. No callbacks are installed, be 33/// sure to register a callback when initializing the event loop 34pub fn init() !Posix { 35 // Open our tty 36 const fd = try posix.open("/dev/tty", .{ .ACCMODE = .RDWR }, 0); 37 38 // Set the termios of the tty 39 const termios = try makeRaw(fd); 40 41 var act = posix.Sigaction{ 42 .handler = .{ .handler = Posix.handleWinch }, 43 .mask = switch (builtin.os.tag) { 44 .macos => 0, 45 .linux => posix.empty_sigset, 46 else => @compileError("os not supported"), 47 }, 48 .flags = 0, 49 }; 50 try posix.sigaction(posix.SIG.WINCH, &act, null); 51 52 const self: Posix = .{ 53 .fd = fd, 54 .termios = termios, 55 }; 56 57 global_tty = self; 58 59 return self; 60} 61 62/// release resources associated with the Tty return it to its original state 63pub fn deinit(self: Posix) void { 64 posix.tcsetattr(self.fd, .FLUSH, self.termios) catch |err| { 65 std.log.err("couldn't restore terminal: {}", .{err}); 66 }; 67 if (builtin.os.tag != .macos) // closing /dev/tty may block indefinitely on macos 68 posix.close(self.fd); 69} 70 71/// Write bytes to the tty 72pub fn write(self: *const Posix, bytes: []const u8) !usize { 73 return posix.write(self.fd, bytes); 74} 75 76pub fn opaqueWrite(ptr: *const anyopaque, bytes: []const u8) !usize { 77 const self: *const Posix = @ptrCast(@alignCast(ptr)); 78 return posix.write(self.fd, bytes); 79} 80 81pub fn anyWriter(self: *const Posix) std.io.AnyWriter { 82 return .{ 83 .context = self, 84 .writeFn = Posix.opaqueWrite, 85 }; 86} 87 88pub fn read(self: *const Posix, buf: []u8) !usize { 89 return posix.read(self.fd, buf); 90} 91 92pub fn opaqueRead(ptr: *const anyopaque, buf: []u8) !usize { 93 const self: *const Posix = @ptrCast(@alignCast(ptr)); 94 return posix.read(self.fd, buf); 95} 96 97pub fn anyReader(self: *const Posix) std.io.AnyReader { 98 return .{ 99 .context = self, 100 .readFn = Posix.opaqueRead, 101 }; 102} 103 104/// Install a signal handler for winsize. A maximum of 8 handlers may be 105/// installed 106pub fn notifyWinsize(handler: SignalHandler) !void { 107 handler_mutex.lock(); 108 defer handler_mutex.unlock(); 109 if (handler_idx == handlers.len) return error.OutOfMemory; 110 handlers[handler_idx] = handler; 111 handler_idx += 1; 112} 113 114fn handleWinch(_: c_int) callconv(.C) void { 115 handler_mutex.lock(); 116 defer handler_mutex.unlock(); 117 var i: usize = 0; 118 while (i < handler_idx) : (i += 1) { 119 const handler = handlers[i]; 120 handler.callback(handler.context); 121 } 122} 123 124/// makeRaw enters the raw state for the terminal. 125pub fn makeRaw(fd: posix.fd_t) !posix.termios { 126 const state = try posix.tcgetattr(fd); 127 var raw = state; 128 // see termios(3) 129 raw.iflag.IGNBRK = false; 130 raw.iflag.BRKINT = false; 131 raw.iflag.PARMRK = false; 132 raw.iflag.ISTRIP = false; 133 raw.iflag.INLCR = false; 134 raw.iflag.IGNCR = false; 135 raw.iflag.ICRNL = false; 136 raw.iflag.IXON = false; 137 138 raw.oflag.OPOST = false; 139 140 raw.lflag.ECHO = false; 141 raw.lflag.ECHONL = false; 142 raw.lflag.ICANON = false; 143 raw.lflag.ISIG = false; 144 raw.lflag.IEXTEN = false; 145 146 raw.cflag.CSIZE = .CS8; 147 raw.cflag.PARENB = false; 148 149 raw.cc[@intFromEnum(posix.V.MIN)] = 1; 150 raw.cc[@intFromEnum(posix.V.TIME)] = 0; 151 try posix.tcsetattr(fd, .FLUSH, raw); 152 return state; 153} 154 155/// Get the window size from the kernel 156pub fn getWinsize(fd: posix.fd_t) !Winsize { 157 var winsize = posix.winsize{ 158 .ws_row = 0, 159 .ws_col = 0, 160 .ws_xpixel = 0, 161 .ws_ypixel = 0, 162 }; 163 164 const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize)); 165 if (posix.errno(err) == .SUCCESS) 166 return Winsize{ 167 .rows = winsize.ws_row, 168 .cols = winsize.ws_col, 169 .x_pixel = winsize.ws_xpixel, 170 .y_pixel = winsize.ws_ypixel, 171 }; 172 return error.IoctlError; 173} 174 175pub fn bufferedWriter(self: *const Posix) std.io.BufferedWriter(4096, std.io.AnyWriter) { 176 return std.io.bufferedWriter(self.anyWriter()); 177}