1 /++
2     Helpers to set up a terminal environment.
3 
4     See_Also:
5         [kameloso.terminal.colours]
6 
7     Copyright: [JR](https://github.com/zorael)
8     License: [Boost Software License 1.0](https://www.boost.org/users/license.html)
9 
10     Authors:
11         [JR](https://github.com/zorael)
12  +/
13 module kameloso.terminal;
14 
15 private:
16 
17 import std.typecons : Flag, No, Yes;
18 
19 public:
20 
21 @safe:
22 
23 /// Special terminal control characters.
24 enum TerminalToken
25 {
26     /// Character that preludes a terminal colouring code.
27     format = '\033',
28 
29     /// Terminal bell/beep.
30     bell = '\007',
31 }
32 
33 
34 version(Windows)
35 {
36     // Taken from LDC: https://github.com/ldc-developers/ldc/pull/3086/commits/9626213a
37     // https://github.com/ldc-developers/ldc/pull/3086/commits/9626213a
38 
39     import core.sys.windows.wincon : SetConsoleCP, SetConsoleMode, SetConsoleOutputCP;
40 
41     /// Original codepage at program start.
42     private __gshared uint originalCP;
43 
44     /// Original output codepage at program start.
45     private __gshared uint originalOutputCP;
46 
47     /// Original console mode at program start.
48     private __gshared uint originalConsoleMode;
49 
50     /++
51         Sets the console codepage to display UTF-8 characters (åäö, 高所恐怖症, ...)
52         and the console mode to display terminal colours.
53      +/
54     void setConsoleModeAndCodepage() @system
55     {
56         import core.stdc.stdlib : atexit;
57         import core.sys.windows.winbase : GetStdHandle, INVALID_HANDLE_VALUE, STD_OUTPUT_HANDLE;
58         import core.sys.windows.wincon : ENABLE_VIRTUAL_TERMINAL_PROCESSING,
59             GetConsoleCP, GetConsoleMode, GetConsoleOutputCP;
60         import core.sys.windows.winnls : CP_UTF8;
61 
62         originalCP = GetConsoleCP();
63         originalOutputCP = GetConsoleOutputCP();
64 
65         cast(void)SetConsoleCP(CP_UTF8);
66         cast(void)SetConsoleOutputCP(CP_UTF8);
67 
68         auto stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE);
69         assert((stdoutHandle != INVALID_HANDLE_VALUE), "Failed to get standard output handle");
70 
71         immutable getModeRetval = GetConsoleMode(stdoutHandle, &originalConsoleMode);
72 
73         if (getModeRetval != 0)
74         {
75             // The console is a real terminal, not a pager (or Cygwin mintty)
76             cast(void)SetConsoleMode(stdoutHandle, originalConsoleMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
77         }
78 
79         // atexit handlers are also called when exiting via exit() etc.;
80         // that's the reason this isn't a RAII struct.
81         atexit(&resetConsoleModeAndCodepage);
82     }
83 
84     /++
85         Resets the console codepage and console mode to the values they had at
86         program start.
87      +/
88     extern(C)
89     private void resetConsoleModeAndCodepage() @system
90     {
91         import core.sys.windows.winbase : GetStdHandle, INVALID_HANDLE_VALUE, STD_OUTPUT_HANDLE;
92 
93         auto stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE);
94         assert((stdoutHandle != INVALID_HANDLE_VALUE), "Failed to get standard output handle");
95 
96         cast(void)SetConsoleCP(originalCP);
97         cast(void)SetConsoleOutputCP(originalOutputCP);
98         cast(void)SetConsoleMode(stdoutHandle, originalConsoleMode);
99     }
100 }
101 
102 
103 version(Posix)
104 {
105     // isTTY
106     /++
107         Determines whether or not the program is being run in a terminal (virtual TTY).
108 
109         "isatty() returns 1 if fd is an open file descriptor referring to a
110         terminal; otherwise 0 is returned, and errno is set to indicate the error."
111 
112         Returns:
113             `true` if the current environment appears to be a terminal;
114             `false` if not (e.g. pager or certain IDEs with terminal windows).
115      +/
116     bool isTTY() //@safe
117     {
118         import core.sys.posix.unistd : STDOUT_FILENO, isatty;
119         return (isatty(STDOUT_FILENO) == 1);
120     }
121 }
122 else version(Windows)
123 {
124     /// Ditto
125     bool isTTY() @system
126     {
127         import core.sys.windows.winbase : FILE_TYPE_PIPE, GetFileType, GetStdHandle, STD_OUTPUT_HANDLE;
128         auto handle = GetStdHandle(STD_OUTPUT_HANDLE);
129         return (GetFileType(handle) != FILE_TYPE_PIPE);
130     }
131 }
132 else
133 {
134     static assert(0, "Unsupported platform, please file a bug.");
135 }
136 
137 
138 // isTerminal
139 /++
140     Determines whether or not the program is being run in a terminal, be it a
141     real TTY or a whitelisted pseudo-TTY such as those employed in IDE terminal
142     emulators.
143 
144     Returns:
145         `true` if the environment is either a real TTY or one of a few whitelisted
146         pseudo-TTYs; `false` if not.
147  +/
148 bool isTerminal() @system
149 {
150     import kameloso.platform : currentPlatform;
151 
152     if (isTTY) return true;
153 
154     switch (currentPlatform)
155     {
156     case "Msys":
157     case "Cygwin":
158     case "vscode":
159         return true;
160 
161     default:
162         return false;
163     }
164 }
165 
166 
167 // applyMonochromeAndFlushOverrides
168 /++
169     Override [kameloso.pods.CoreSettings.monochrome|CoreSettings.monochrome] and
170     potentially [kameloso.pods.CoreSettings.flush|CoreSettings.flush] if the
171     terminal seems to not truly be a terminal (such as a pager, or a non-whitelisted
172     IDE terminal emulator).
173 
174     The idea is to generally override monochrome to true if it's a pager, but
175     keep monochrome and override flush to true if it's a whitelisted environment.
176 
177     Params:
178         monochrome = Reference to monochrome setting bool.
179         flush = Reference to flush setting bool.
180  +/
181 void applyMonochromeAndFlushOverrides(ref bool monochrome, ref bool flush) @system
182 {
183     import kameloso.platform : currentPlatform;
184 
185     if (!isTTY)
186     {
187         switch (currentPlatform)
188         {
189         case "Msys":
190             // Requires manual flushing despite setvbuf
191             // No need to set monochrome though
192             flush = true;
193             break;
194 
195         case "Cygwin":
196         case "vscode":
197             // Probably no longer needs modifications
198             break;
199 
200         default:
201             // Non-whitelisted non-TTY; set monochrome
202             monochrome = true;
203             break;
204         }
205     }
206 }
207 
208 
209 // ensureAppropriateBuffering
210 /++
211     Ensures select non-TTY environments (like Cygwin) are line-buffered.
212  +/
213 void ensureAppropriateBuffering() @system
214 {
215     import kameloso.constants : BufferSize;
216     import kameloso.platform : currentPlatform;
217     import std.stdio : stdout;
218     import core.stdc.stdio : _IOLBF;
219 
220     if (!isTTY)
221     {
222         switch (currentPlatform)
223         {
224         case "Msys":
225         case "Cygwin":
226         case "vscode":
227             /+
228                 Some terminal environments require us to flush standard out after
229                 writing to it, as they are likely pagers and not TTYs behind the
230                 scene. Whitelist some and set standard out to be line-buffered
231                 for those.
232              +/
233             stdout.setvbuf(BufferSize.vbufStdout, _IOLBF);
234             break;
235 
236         default:
237             // Non-whitelisted non-TTY (a pager), leave as-is.
238             break;
239         }
240     }
241 }
242 
243 
244 // setTitle
245 /++
246     Sets the terminal title to a given string. Supposedly.
247 
248     Example:
249     ---
250     setTitle("kameloso IRC bot");
251     ---
252 
253     Params:
254         title = String to set the title to.
255  +/
256 void setTitle(const string title) @system
257 {
258     version(Posix)
259     {
260         import std.stdio : stdout, write;
261 
262         write("\033]0;", title, "\007");
263         stdout.flush();
264     }
265     else version(Windows)
266     {
267         import std.string : toStringz;
268         import core.sys.windows.wincon : SetConsoleTitleA;
269 
270         SetConsoleTitleA(title.toStringz);
271     }
272     else
273     {
274         static assert(0, "Unsupported platform, please file a bug.");
275     }
276 }