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 }