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 }