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 }