1 // Copyright 2022 Garrett D'Amore 2 // 3 // Distributed under the Boost Software License, Version 1.0. 4 // (See accompanying file LICENSE or https://www.boost.org/LICENSE_1_0.txt) 5 6 module dcell.termio; 7 8 import std.exception; 9 import std.range.interfaces; 10 import dcell.coord; 11 12 /** 13 * TtyImpl is the interface that implementations should 14 * override or supply to support terminal I/O ioctls or 15 * equivalent functionality. It is provided in this form, as 16 * some implementations may not be based on actual tty devices. 17 */ 18 interface TtyImpl 19 { 20 /** 21 * Save current tty settings. These can be subsequently 22 * restored using restore. 23 */ 24 void save(); 25 26 /** 27 * Restore tty settings saved with save(). 28 */ 29 void restore(); 30 31 /** 32 * Make the terminal suitable for raw mode input. 33 * In this mode the terminal is not suitable for 34 * typical interactive shell use, but is good if absolute 35 * control over input is needed. After this, reads 36 * will block until one character is presented. (Same 37 * effect as 'blocking(true)'. 38 */ 39 void raw(); 40 41 /** 42 * Make input blocking or non-blocking. Blocking input 43 * will cause reads against the terminal to block forever 44 * until at least one character is returned. Otherwise it 45 * will return in at most 46 */ 47 void blocking(bool b); 48 49 /** 50 * Read input. May return an empty slice if no data 51 * is present and blocking is disabled. 52 */ 53 string read(); 54 55 /** 56 * Write output. 57 */ 58 void write(string s); 59 60 /** 61 * Flush output. 62 */ 63 void flush(); 64 65 /** 66 * Get window size. 67 */ 68 Coord windowSize(); 69 70 /** 71 * Stop input scanning. This may close the tty device. 72 */ 73 void stop(); 74 75 /** 76 * Start termio. This will open the device. 77 */ 78 void start(); 79 80 /** 81 * Resized returns true if the window was resized since last checked. 82 * Normally resize will force the window into non-blocking mode so 83 * that the caller can see the resize in a timely fashion. 84 */ 85 bool resized(); 86 } 87 88 version (Posix) 89 { 90 import core.sys.posix.sys.ioctl; 91 import core.sys.posix.termios; 92 import core.sys.posix.unistd; 93 import std.stdio; 94 95 package class PosixTty : TtyImpl 96 { 97 this(string dev) 98 { 99 path = dev; 100 } 101 102 void start() 103 { 104 file = File(path, "r+b"); 105 fd = file.fileno(); 106 save(); 107 watchResize(fd); 108 } 109 110 void stop() 111 { 112 if (file.isOpen()) 113 { 114 flush(); 115 restore(); 116 file.close(); 117 } 118 ignoreResize(fd); 119 } 120 121 void save() 122 { 123 if (!isatty(fd)) 124 throw new Exception("not a tty device"); 125 enforce(tcgetattr(fd, &saved) >= 0, "failed to get termio state"); 126 } 127 128 void restore() 129 { 130 enforce(tcsetattr(fd, TCSANOW, &saved) >= 0, "failed to set termio state"); 131 } 132 133 void flush() 134 { 135 file.flush(); 136 } 137 138 void blocking(bool b) @trusted 139 { 140 termios tio; 141 enforce(tcgetattr(fd, &tio) >= 0); 142 tio.c_cc[VMIN] = b ? 1 : 0; 143 tio.c_cc[VTIME] = b ? 0 : 1; 144 enforce(tcsetattr(fd, TCSANOW, &tio) >= 0); 145 block = b; 146 } 147 148 void raw() @trusted 149 { 150 termios tio; 151 enforce(tcgetattr(fd, &tio) >= 0, "failed to get termio state"); 152 tio.c_iflag &= ~(IGNBRK | BRKINT | ISTRIP | INLCR | IGNCR | ICRNL | IXON); 153 tio.c_oflag &= ~OPOST; 154 tio.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); 155 tio.c_cflag &= ~(CSIZE | PARENB); 156 tio.c_cflag |= CS8; 157 tio.c_cc[VMIN] = 1; // at least one character 158 tio.c_cc[VTIME] = 0; // but block forever 159 enforce(tcsetattr(fd, TCSANOW, &tio) >= 0, "failed to set termios"); 160 } 161 162 Coord windowSize() 163 { 164 // If cores.sys.posix.sys.ioctl had more complete and accurate data... 165 // this structure is fairly consistent amongst all POSIX variants 166 struct winSz 167 { 168 ushort ws_row; 169 ushort ws_col; 170 ushort ws_xpix; 171 ushort ws_ypix; 172 } 173 174 version (linux) 175 { 176 // has TIOCGWINSZ already -- but it might be wrong 177 // Linux has different values for TIOCGWINSZ depending 178 // on architecture 179 // SPARC, PPC, and MIPS use legacy BSD based values. 180 // Others use a newer // value. 181 version (SPARC64) 182 enum TIOCGWINSZ = 0x40087468; 183 else version (SPARC) 184 enum TIOCGWINSZ = 0x40087468; 185 else version (PPC) 186 enum TIOCGWINSZ = 0x40087468; 187 else version (PPC64) 188 enum TIOCGWINSZ = 0x40087468; 189 else version (MIPS32) 190 enum TIOCGWINSZ = 0x40087468; 191 else version (MIPS64) 192 enum TIOCGWINSZ = 0x40087468; 193 else 194 enum TIOCGWINSZ = 0x5413; // everything else 195 } 196 else version (Darwin) 197 enum TIOCGWINSZ = 0x40087468; 198 else version (Solaris) 199 enum TIOCGWINSZ = 0x5468; 200 else version (OpenBSD) 201 enum TIOCGWINSZ = 0x40087468; 202 else version (DragonFlyBSD) 203 enum TIOCGWINSZ = 0x40087468; 204 else version (NetBSD) 205 enum TIOCGWINSZ = 0x40087468; 206 else version (FreeBSD) 207 enum TIOCGWINSZ = 0x40087468; 208 else version (AIX) 209 enum TIOCGWINSZ = 0x40087468; 210 211 winSz wsz; 212 enforce(ioctl(fd, TIOCGWINSZ, &wsz) >= 0); 213 return Coord(wsz.ws_col, wsz.ws_row); 214 } 215 216 string read() 217 { 218 // this has to use the underlying read system call 219 import unistd = core.sys.posix.unistd; 220 221 ubyte[] buf = new ubyte[128]; 222 auto rv = unistd.read(fd, cast(void*) buf.ptr, buf.length); 223 if (rv < 0) 224 return ""; 225 return cast(string) buf[0 .. rv]; 226 } 227 228 void write(string s) 229 { 230 file.rawWrite(s); 231 } 232 233 bool resized() 234 { 235 return wasResized(fd); 236 } 237 238 private: 239 string path; 240 File file; 241 int fd; 242 termios saved; 243 bool block; 244 } 245 246 TtyImpl newDevTty(string dev = "/dev/tty") 247 { 248 return new PosixTty(dev); 249 } 250 251 } 252 else 253 { 254 TtyImpl newDevTty(string p = "/dev/tty") 255 { 256 throw new Exception("not supported"); 257 } 258 } 259 260 version (Posix) 261 { 262 import core.atomic; 263 import core.sys.posix.signal; 264 265 private __gshared int sigRaised = 0; 266 private __gshared int sigFd = -1; 267 private extern (C) void handleSigWinCh(int sig) nothrow 268 { 269 int fd = sigFd; 270 atomicStore(sigRaised, 1); 271 termios tio; 272 // wake the input loop so it can see the signal 273 // this is crummy but its the best way to get this noticed. 274 if (tcgetattr(fd, &tio) >= 0) 275 { 276 tio.c_cc[VMIN] = 0; 277 tio.c_cc[VTIME] = 1; 278 tcsetattr(fd, TCSANOW, &tio); 279 } 280 } 281 282 // We don't have a stanrdard definition of SIGWINCH 283 version (linux) 284 { 285 // Legacy Linux is not even self-compatible ick. 286 version (MIPS_Any) 287 enum SIGWINCH = 20; 288 else 289 enum SIGWINCH = 28; 290 } 291 else version (Solaris) 292 enum SIGWINCH = 20; 293 else version (OSX) 294 enum SIGWINCH = 28; 295 else version (FreeBSD) 296 enum SIGWINCH = 28; 297 else version (NetBSD) 298 enum SIGWINCH = 28; 299 else version (DragonFlyBSD) 300 enum SIGWINCH = 28; 301 else version (OpenBSD) 302 enum SIGWINCH = 28; 303 else version (AIX) 304 enum SIGWINCH = 28; 305 else 306 static assert(0, "no version"); 307 308 void watchResize(int fd) 309 { 310 if (atomicLoad(sigFd) == -1) 311 { 312 sigFd = fd; 313 sigaction_t sa; 314 sa.sa_handler = &handleSigWinCh; 315 sigaction(SIGWINCH, &sa, null); 316 } 317 } 318 319 void ignoreResize(int fd) 320 { 321 if (atomicLoad(sigFd) == fd) 322 { 323 sigaction_t sa; 324 sa.sa_handler = SIG_IGN; 325 sigaction(SIGWINCH, &sa, null); 326 sigFd = -1; 327 } 328 } 329 330 bool wasResized(int fd) 331 { 332 if (fd == atomicLoad(sigFd) && fd != -1) 333 { 334 return atomicExchange(&sigRaised, 0) != 0; 335 } 336 else 337 { 338 return false; 339 } 340 } 341 }