a modern tui library written in zig
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}