12345678910111213141516171819const std = @import("std");2021pub const STDIN_FD = std.posix.STDIN_FILENO;2223var g_saved: ?std.posix.termios = null;2425262728fn restoreSavedTermios(fd: std.posix.fd_t) void {29 if (g_saved) |t| std.posix.tcsetattr(fd, .FLUSH, t) catch {};30}3132fn defaultAction() std.posix.Sigaction {33 return .{34 .handler = .{ .handler = std.posix.SIG.DFL },35 .mask = std.posix.sigemptyset(),36 .flags = 0,37 };38}394041424344fn restoreAndReraise(sig: std.posix.SIG) callconv(.c) void {45 restoreSavedTermios(STDIN_FD);46 var dfl = defaultAction();47 std.posix.sigaction(sig, &dfl, null);48 std.posix.raise(sig) catch {};49}5051fn installSignalHandlers() void {52 const act: std.posix.Sigaction = .{53 .handler = .{ .handler = restoreAndReraise },54 .mask = std.posix.sigemptyset(),55 .flags = 0,56 };57 std.posix.sigaction(.INT, &act, null);58 std.posix.sigaction(.TERM, &act, null);59 std.posix.sigaction(.HUP, &act, null);60}6162fn restoreDefaultSignalHandlers() void {63 var dfl = defaultAction();64 std.posix.sigaction(.INT, &dfl, null);65 std.posix.sigaction(.TERM, &dfl, null);66 std.posix.sigaction(.HUP, &dfl, null);67}6869707172737475pub fn rawModeEnable(fd: std.posix.fd_t, saved: *std.posix.termios) !void {76 var tio = try std.posix.tcgetattr(fd);77 saved.* = tio;78 g_saved = tio;79 installSignalHandlers();80 tio.lflag.ECHO = false;81 tio.lflag.ICANON = false;82 tio.lflag.IEXTEN = false;83 tio.iflag.ICRNL = false;84 tio.iflag.IXON = false;85 tio.cc[@intFromEnum(std.posix.V.MIN)] = 1;86 tio.cc[@intFromEnum(std.posix.V.TIME)] = 0;87 try std.posix.tcsetattr(fd, .FLUSH, tio);88}8990pub fn rawModeDisable(fd: std.posix.fd_t, saved: std.posix.termios) void {91 std.posix.tcsetattr(fd, .FLUSH, saved) catch {};92 restoreDefaultSignalHandlers();93 g_saved = null;94}9596979899100101102103104105106107108const testing = std.testing;109110extern "c" fn posix_openpt(flags: c_int) c_int;111extern "c" fn grantpt(fd: c_int) c_int;112extern "c" fn unlockpt(fd: c_int) c_int;113extern "c" fn ptsname(fd: c_int) ?[*:0]u8;114115116117extern "c" fn close(fd: c_int) c_int;118119const TestPty = struct {120 master: std.posix.fd_t,121 slave: std.posix.fd_t,122123 fn open() !TestPty {124 const O_RDWR_NOCTTY: std.posix.O = .{ .ACCMODE = .RDWR, .NOCTTY = true };125 const master = posix_openpt(@bitCast(O_RDWR_NOCTTY));126 if (master < 0) return error.OpenPtyFailed;127 errdefer _ = close(master);128 if (grantpt(master) != 0) return error.OpenPtyFailed;129 if (unlockpt(master) != 0) return error.OpenPtyFailed;130 const path = ptsname(master) orelse return error.OpenPtyFailed;131 const slave = try std.posix.openatZ(std.posix.AT.FDCWD, path, O_RDWR_NOCTTY, 0);132 return .{ .master = master, .slave = slave };133 }134135 fn closePty(self: TestPty) void {136 _ = close(self.slave);137 _ = close(self.master);138 }139};140141test "rawModeEnable stashes the pre-raw termios in the signal-reachable global and disables echo/canon while leaving ISIG on" {142 const pty = TestPty.open() catch |err| {143 144 145 std.log.warn("skipping rawterm pty test: {t}", .{err});146 return error.SkipZigTest;147 };148 defer pty.closePty();149150 try testing.expect(g_saved == null);151152 var saved: std.posix.termios = undefined;153 try rawModeEnable(pty.slave, &saved);154 defer rawModeDisable(pty.slave, saved);155156 try testing.expect(g_saved != null);157 const applied = try std.posix.tcgetattr(pty.slave);158 try testing.expect(!applied.lflag.ECHO);159 try testing.expect(!applied.lflag.ICANON);160 try testing.expect(applied.lflag.ISIG); 161}162163test "rawModeDisable restores the original termios and clears the signal-reachable global" {164 const pty = TestPty.open() catch |err| {165 std.log.warn("skipping rawterm pty test: {t}", .{err});166 return error.SkipZigTest;167 };168 defer pty.closePty();169170 const original = try std.posix.tcgetattr(pty.slave);171 var saved: std.posix.termios = undefined;172 try rawModeEnable(pty.slave, &saved);173 rawModeDisable(pty.slave, saved);174175 try testing.expect(g_saved == null);176 const restored = try std.posix.tcgetattr(pty.slave);177 try testing.expect(restored.lflag.ECHO == original.lflag.ECHO);178 try testing.expect(restored.lflag.ICANON == original.lflag.ICANON);179}180181test "restoreSavedTermios (the signal handler's own restore step) puts ECHO/ICANON back without a real signal" {182 const pty = TestPty.open() catch |err| {183 std.log.warn("skipping rawterm pty test: {t}", .{err});184 return error.SkipZigTest;185 };186 defer pty.closePty();187188 var saved: std.posix.termios = undefined;189 try rawModeEnable(pty.slave, &saved);190 191 192 193 restoreSavedTermios(pty.slave);194 restoreDefaultSignalHandlers();195 g_saved = null;196197 const restored = try std.posix.tcgetattr(pty.slave);198 try testing.expect(restored.lflag.ECHO == saved.lflag.ECHO);199 try testing.expect(restored.lflag.ICANON == saved.lflag.ICANON);200}