1 /** 2 * Basic cross-platform color output 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.sys.term; 15 16 import std.stdio; 17 18 /// Base class for a terminal implementation. 19 /// The interface only contains the common subset of features 20 /// available on both platforms (Windows API and ANSI). 21 abstract class Term 22 { 23 /// The 16 standard VGA/ANSI colors plus a "none" pseudo-color. 24 enum Color : byte 25 { 26 none = -1 , /// transparent or default 27 28 black = 0 , /// 29 30 red = 1 << 0 , /// 31 green = 1 << 1 , /// 32 blue = 1 << 2 , /// 33 34 yellow = red | green , /// 35 magenta = red | blue, /// 36 cyan = green | blue, /// 37 gray = red | green | blue, /// 38 39 darkGray = bright | black , /// 40 brightRed = bright | red , /// 41 brightGreen = bright | green , /// 42 brightBlue = bright | blue , /// 43 brightYellow = bright | yellow , /// 44 brightMagenta = bright | magenta , /// 45 brightCyan = bright | cyan , /// 46 white = bright | gray , /// 47 48 // Place this definition after the colors so that e.g. 49 // std.conv prefers the color name over this flag name. 50 bright = 1 << 3, /// (mask) 51 } 52 53 private enum mixColorAliases(T) = (){ 54 import std.traits : EnumMembers; 55 string s; 56 foreach (i, m; EnumMembers!Color) 57 { 58 enum name = __traits(identifier, EnumMembers!Color[i]); 59 s ~= `enum ` ~ name ~ ` = ` ~ T.stringof ~ `(Color.` ~ name ~ `);`; 60 } 61 62 return s; 63 }(); 64 65 /// Color wrapping formatting operations for put. 66 struct ForegroundColor { Color c; /***/ mixin(mixColorAliases!ForegroundColor); } 67 struct BackgroundColor { Color c; /***/ mixin(mixColorAliases!BackgroundColor); } /// ditto 68 69 /// Shorthands 70 alias fg = ForegroundColor; 71 alias bg = BackgroundColor; /// ditto 72 mixin(mixColorAliases!ForegroundColor); /// ditto 73 74 /// Puts some combination of colors and stringifiable objects in order. 75 void put(Args...)(auto ref Args args) 76 { 77 putImpl(InitState.init, args); 78 } 79 80 private: 81 void putImpl(State, Args...)(State state, ref Args args) 82 { 83 // Use a tail recursion and maintain state using a type to 84 // allow congesting consecutive formatting operations into a 85 // single terminal operation. 86 // E.g.: `fg.a, bg.b` (or swapped) gets encoded as `\e[3a;4bm` 87 // instead of `\e[3am\e[4bm` 88 // E.g.: `fg.a, fg.b` gets encoded as `\e[3bm` instead of 89 // `\e[3am\e[3bm` 90 static if (Args.length == 0) 91 flush(state); 92 else 93 { 94 alias A = Args[0]; 95 static if (is(A == ForegroundColor)) 96 { 97 static struct NextState 98 { 99 ForegroundColor fg; 100 typeof(State.bg) bg; 101 } 102 return putImpl(NextState(args[0], state.bg), args[1..$]); 103 } 104 else 105 static if (is(A == BackgroundColor)) 106 { 107 static struct NextState 108 { 109 typeof(State.fg) fg; 110 BackgroundColor bg; 111 } 112 return putImpl(NextState(state.fg, args[0]), args[1..$]); 113 } 114 else 115 { 116 flush(state); 117 static if (is(A : const(char)[])) 118 putText(args[0]); 119 else 120 { 121 import std.format : formattedWrite; 122 formattedWrite!"%s"(&putText, args[0]); 123 } 124 return putImpl(initState, args[1..$]); 125 } 126 } 127 } 128 129 alias NoColor = void[0]; 130 static struct InitState 131 { 132 NoColor fg, bg; 133 } 134 enum initState = InitState.init; 135 136 void flush(State)(State state) 137 { 138 static if (!is(typeof(state.bg) == NoColor)) 139 static if (!is(typeof(state.fg) == NoColor)) 140 setColor(state.bg.c, state.fg.c); 141 else 142 setBackgroundColor(state.bg.c); 143 else 144 static if (!is(typeof(state.fg) == NoColor)) 145 setTextColor(state.fg.c); 146 } 147 148 protected: 149 void putText(in char[] s); 150 void setTextColor(Color c); 151 void setBackgroundColor(Color c); 152 void setColor(Color fg, Color bg); 153 } 154 155 /// No output (including text). 156 /// (See DumbTerm for text-only output). 157 class NullTerm : Term 158 { 159 protected: 160 override void putText(in char[] s) {} 161 override void setTextColor(Color c) {} 162 override void setBackgroundColor(Color c) {} 163 override void setColor(Color fg, Color bg) {} 164 } 165 166 /// Base class for File-based terminal implementations. 167 abstract class FileTerm : Term 168 { 169 File f; /// 170 171 protected: 172 override void putText(in char[] s) { f.rawWrite(s); } 173 } 174 175 /// No color, only text output. 176 class DumbTerm : FileTerm 177 { 178 this(File f) 179 { 180 this.f = f; 181 } /// 182 183 protected: 184 override void setTextColor(Color c) {} 185 override void setBackgroundColor(Color c) {} 186 override void setColor(Color fg, Color bg) {} 187 } 188 189 /// ANSI escape sequence based terminal. 190 class ANSITerm : FileTerm 191 { 192 /// Returns true if `f` is a TTY and `TERM` 193 /// is set to anything other than "dumb". 194 static bool isTerm(File f) 195 { 196 version (Posix) 197 { 198 import core.sys.posix.unistd : isatty; 199 if (!isatty(f.fileno)) 200 return false; 201 } 202 203 import std.process : environment; 204 auto term = environment.get("TERM"); 205 if (!term || term == "dumb") 206 return false; 207 return true; 208 } 209 210 /// Convert a `Color` to an ANSI color number. 211 static int ansiColor(Color c, bool background) 212 { 213 if (c < -1 || c > Color.white) 214 assert(false); 215 216 int result; 217 if (c == Color.none) 218 result = 39; 219 else 220 if (c & Color.bright) 221 result = 90 + (c & ~uint(Color.bright)); 222 else 223 result = 30 + c; 224 if (background) 225 result += 10; 226 return result; 227 } 228 229 this(File f) 230 { 231 this.f = f; 232 } /// 233 234 protected: 235 override void setTextColor(Color c) 236 { 237 f.writef("\x1b[%dm", ansiColor(c, false)); 238 } 239 240 override void setBackgroundColor(Color c) 241 { 242 f.writef("\x1b[%dm", ansiColor(c, true)); 243 } 244 245 override void setColor(Color fg, Color bg) 246 { 247 f.writef("\x1b[%d;%dm", ansiColor(fg, false), ansiColor(bg, true)); 248 } 249 } 250 251 /// Windows API based terminal. 252 version (Windows) 253 class WindowsTerm : FileTerm 254 { 255 import core.sys.windows.basetsd : HANDLE; 256 import core.sys.windows.windef : WORD; 257 import core.sys.windows.wincon : CONSOLE_SCREEN_BUFFER_INFO, GetConsoleScreenBufferInfo, SetConsoleTextAttribute; 258 import ae.sys.windows.exception : wenforce; 259 260 /// Returns true if `f` is a Windows console. 261 static bool isTerm(File f) 262 { 263 CONSOLE_SCREEN_BUFFER_INFO info; 264 return !!GetConsoleScreenBufferInfo(f.windowsHandle, &info); 265 } 266 267 this(File f) 268 { 269 this.f = f; 270 handle = f.windowsHandle; 271 272 CONSOLE_SCREEN_BUFFER_INFO info; 273 GetConsoleScreenBufferInfo(handle, &info) 274 .wenforce("GetConsoleScreenBufferInfo"); 275 attributes = origAttributes = info.wAttributes; 276 } /// 277 278 protected: 279 HANDLE handle; 280 WORD attributes, origAttributes; 281 282 final void setAttribute(bool background)(Color c) 283 { 284 enum shift = background ? 4 : 0; 285 enum mask = 0xF << shift; 286 if (c == Color.none) 287 attributes = (attributes & ~mask) | (origAttributes & mask); 288 else 289 { 290 WORD value = c; 291 value <<= shift; 292 attributes = (attributes & ~mask) | value; 293 } 294 } 295 296 final void applyAttribute() 297 { 298 f.flush(); 299 SetConsoleTextAttribute(handle, attributes) 300 .wenforce("SetConsoleTextAttribute"); 301 } 302 303 override void setTextColor(Color c) 304 { 305 setAttribute!false(c); 306 applyAttribute(); 307 } 308 309 override void setBackgroundColor(Color c) 310 { 311 setAttribute!true(c); 312 applyAttribute(); 313 } 314 315 override void setColor(Color fg, Color bg) 316 { 317 setAttribute!false(fg); 318 setAttribute!true(bg); 319 applyAttribute(); 320 } 321 } 322 323 /// Returns whether the given file is likely to be attached to a terminal. 324 bool isTerm(File f) 325 { 326 version (Windows) 327 { 328 if (WindowsTerm.isTerm(f)) 329 return true; 330 // fall through - we might be on a POSIX environment on Windows 331 } 332 return ANSITerm.isTerm(f); 333 } 334 335 /// Choose a suitable terminal implementation for the given file, create and return it. 336 Term makeTerm(File f = stderr) 337 { 338 if (isTerm(f)) 339 version (Windows) 340 return new WindowsTerm(f); 341 else 342 return new ANSITerm(f); 343 else 344 return new DumbTerm(f); 345 } 346 347 private Term globalTerm; 348 /// Get or set a `Term` implementation for the current thread (creating one if necessary). 349 @property Term term() 350 { 351 if (!globalTerm) 352 globalTerm = makeTerm(); 353 return globalTerm; 354 } /// ditto 355 @property Term term(Term newTerm) 356 { 357 return globalTerm = newTerm; 358 } /// ditto 359 360 version (ae_sys_term_demo) 361 private void main() 362 { 363 auto t = term; 364 foreach (bg; -1 .. 16) 365 { 366 t.put(t.bg(cast(Term.Color)bg)); 367 foreach (fg; -1 .. 16) 368 t.put(t.fg(cast(Term.Color)fg), "XX"); 369 t.put(t.none, t.bg.none, "\n"); 370 } 371 readln(); 372 }