1 /// Utils to work with POSIX terminal
2 module sily.terminal;
3 
4 static this() {
5     version(windows) {
6         import core.stdc.stdlib: exit;
7         exit(2);
8     }
9 
10     // To prevent from killing terminal by calling reset before set
11     tcgetattr(stdin.fileno, &originalTermios);
12 }
13 
14 /* ------------------------------ TERMINAL SIZE ----------------------------- */
15 import core.sys.posix.sys.ioctl: winsize, ioctl, TIOCGWINSZ;
16 
17 /// Returns bash terminal width
18 int terminalWidth() {
19     winsize w;
20     ioctl(0, TIOCGWINSZ, &w);
21     return w.ws_col;
22 }
23 
24 /// Returns bash terminal height
25 int terminalHeight() {
26     winsize w;
27     ioctl(0, TIOCGWINSZ, &w);
28     return w.ws_row;
29 }
30 
31 /* -------------------------------- RAW MODE -------------------------------- */
32 import core.stdc.stdio: setvbuf, _IONBF, _IOLBF;
33 import core.stdc.stdlib: atexit;
34 import core.stdc.string: memcpy;
35 import core.sys.posix.termios: termios, tcgetattr, tcsetattr, TCSANOW;
36 import core.sys.posix.unistd: read;
37 import core.sys.posix.sys.select: select, fd_set, FD_ZERO, FD_SET;
38 import core.sys.posix.sys.time: timeval;
39 
40 import std.stdio: stdin, stdout;
41 
42 private extern(C) void cfmakeraw(termios *termios_p);
43 
44 private termios originalTermios;
45 
46 private bool __isTermiosRaw = false;
47 
48 /// Is terminal in raw mode (have `setTerminalModeRaw` been called yet?)
49 bool isTerminalRaw() nothrow {
50     return __isTermiosRaw;
51 }
52 
53 /// Resets termios back to default and buffers stdout
54 extern(C) alias terminalModeReset = function() {
55     tcsetattr(0, TCSANOW, &originalTermios);
56     setvbuf(stdout.getFP, null, _IOLBF, 1024);
57     __isTermiosRaw = false;
58 };
59 
60 /** 
61 Creates new termios and unbuffers stdout. Required for `kbhit` and `getch`
62 DO NOT USE IF YOU DON'T KNOW WHAT YOU'RE DOING
63 
64 Note that in raw mode CRLF (`\r\n`) newline will be 
65 required instead of normal LF (`\n`)
66 Params:
67     removeStdoutBuffer = Sets stdout buffer to null allowing immediate render without flush()
68 */
69 void terminalModeSetRaw(bool removeStdoutBuffer = true) {
70     import core.sys.posix.termios;
71     termios newTermios;
72 
73     tcgetattr(stdin.fileno, &originalTermios);
74     memcpy(&newTermios, &originalTermios, termios.sizeof);
75 
76     cfmakeraw(&newTermios);
77 
78     newTermios.c_lflag &= ~(ICANON | ECHO | ISIG | IEXTEN);
79     // newTermios.c_lflag &= ~(ICANON | ECHO);
80     newTermios.c_iflag &= ~(ICRNL | INLCR | OPOST);
81     newTermios.c_cc[VMIN] = 1;
82     newTermios.c_cc[VTIME] = 0;
83 
84     if (removeStdoutBuffer) setvbuf(stdout.getFP, null, _IONBF, 0);
85 
86     tcsetattr(stdin.fileno, TCSANOW, &newTermios);
87 
88     atexit(terminalModeReset);
89     __isTermiosRaw = true;
90 }
91 
92 /// Returns 1 if any key was pressed
93 int kbhit() {
94     timeval tv = { 0, 0 };
95     fd_set fds;
96     FD_ZERO(&fds);
97     FD_SET(stdin.fileno, &fds);
98     return select(1, &fds, null, null, &tv);
99 }
100 
101 /// Returns last pressed key
102 int getch() {
103     int r;
104     uint c;
105 
106     if ((r = cast(int) read(stdin.fileno, &c, ubyte.sizeof)) < 0) {
107         return r;
108     } else {
109         return c;
110     }
111 }
112 
113 /* ---------------------------------- MISC ---------------------------------- */
114 import core.sys.posix.unistd: posixIsATTY = isatty;
115 import std.stdio: File;
116 import core.stdc.stdio: FILE, cfileno = fileno;
117 import core.stdc.stdlib: cexit = exit;
118 import core.stdc.errno;
119 import core.thread: Thread;
120 import core.time: dmsecs = msecs;
121 
122 /// Returns true if file is a tty
123 bool isatty(File file) {
124     return cast(bool) posixIsATTY(file.fileno);
125 }
126 /// Ditto
127 bool isatty(FILE* handle) {
128     return cast(bool) posixIsATTY(handle.cfileno);
129 }
130 /// Ditto
131 bool isatty(int fd_set) {
132     return cast(bool) posixIsATTY(fd_set);
133 }
134 
135 /// Forcefully closes application
136 void exit(ErrorCode code = ErrorCode.general) {
137     cexit(cast(int) code);
138 }
139 
140 /// Alias to ErrorCode enum
141 alias ExitCode = ErrorCode;
142 
143 /// Enum containing common exit codes
144 enum ErrorCode {
145     /// Program completed correctly
146     success = 0,
147     /// Catchall for general errors (misc errors, such as `x / 0`)
148     general = 1,
149     /// Operation not permitted (missing keyword/command or permission problem)
150     noperm = 2,
151     /// Command invoked cannot execute (permission problem or command is not executable)
152     noexec = 126,
153     /// Command not found (possible problem with `$PATH` or typo)
154     notfound = 127,
155     /// Invalid argument to exit (see ErrorCode.nocode)
156     noexit = 128,
157     /// Fatal error (further execution is not possible or might harm the OS)
158     fatal = 129,
159     /// Terminated with `Ctrl-C`
160     sigint = 130,
161     /// Exit status out of range (maximal exit code)
162     nocode = 255
163 }
164 
165 /// Sleeps for set amount of msecs
166 void sleep(uint msecs) {
167     Thread.sleep(msecs.dmsecs);
168 }