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