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 <vladimir@thecybershadow.net> 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, 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 static bool isTerm(File f) 193 { 194 version (Posix) 195 { 196 import core.sys.posix.unistd : isatty; 197 if (!isatty(f.fileno)) 198 return false; 199 } 200 201 import std.process : environment; 202 auto term = environment.get("TERM"); 203 if (!term || term == "dumb") 204 return false; 205 return true; 206 } 207 208 static int ansiColor(Color c, bool background) 209 { 210 if (c < -1 || c > Color.white) 211 assert(false); 212 213 int result; 214 if (c == Color.none) 215 result = 39; 216 else 217 if (c & Color.bright) 218 result = 90 + (c & ~uint(Color.bright)); 219 else 220 result = 30 + c; 221 if (background) 222 result += 10; 223 return result; 224 } 225 226 this(File f) 227 { 228 this.f = f; 229 } 230 231 protected: 232 override void setTextColor(Color c) 233 { 234 f.writef("\x1b[%dm", ansiColor(c, false)); 235 } 236 237 override void setBackgroundColor(Color c) 238 { 239 f.writef("\x1b[%dm", ansiColor(c, true)); 240 } 241 242 override void setColor(Color fg, Color bg) 243 { 244 f.writef("\x1b[%d;%dm", ansiColor(fg, false), ansiColor(bg, true)); 245 } 246 } 247 248 /// Windows API based terminal. 249 version (Windows) 250 class WindowsTerm : FileTerm 251 { 252 import core.sys.windows.basetsd : HANDLE; 253 import core.sys.windows.windef : WORD; 254 import core.sys.windows.wincon : CONSOLE_SCREEN_BUFFER_INFO, GetConsoleScreenBufferInfo, SetConsoleTextAttribute; 255 import ae.sys.windows.exception : wenforce; 256 257 static bool isTerm(File f) 258 { 259 CONSOLE_SCREEN_BUFFER_INFO info; 260 return !!GetConsoleScreenBufferInfo(f.windowsHandle, &info); 261 } 262 263 HANDLE handle; 264 WORD attributes, origAttributes; 265 266 this(File f) 267 { 268 this.f = f; 269 handle = f.windowsHandle; 270 271 CONSOLE_SCREEN_BUFFER_INFO info; 272 GetConsoleScreenBufferInfo(handle, &info) 273 .wenforce("GetConsoleScreenBufferInfo"); 274 attributes = origAttributes = info.wAttributes; 275 } 276 277 protected: 278 final void setAttribute(bool background)(Color c) 279 { 280 enum shift = background ? 4 : 0; 281 enum mask = 0xF << shift; 282 if (c == Color.none) 283 attributes = (attributes & ~mask) | (origAttributes & mask); 284 else 285 { 286 WORD value = c; 287 value <<= shift; 288 attributes = (attributes & ~mask) | value; 289 } 290 } 291 292 final void applyAttribute() 293 { 294 f.flush(); 295 SetConsoleTextAttribute(handle, attributes) 296 .wenforce("SetConsoleTextAttribute"); 297 } 298 299 override void setTextColor(Color c) 300 { 301 setAttribute!false(c); 302 applyAttribute(); 303 } 304 305 override void setBackgroundColor(Color c) 306 { 307 setAttribute!true(c); 308 applyAttribute(); 309 } 310 311 override void setColor(Color fg, Color bg) 312 { 313 setAttribute!false(fg); 314 setAttribute!true(bg); 315 applyAttribute(); 316 } 317 } 318 319 /// Returns whether the given file is likely to be attached to a terminal. 320 bool isTerm(File f) 321 { 322 version (Windows) 323 { 324 if (WindowsTerm.isTerm(f)) 325 return true; 326 // fall through - we might be on a POSIX environment on Windows 327 } 328 return ANSITerm.isTerm(f); 329 } 330 331 /// Choose a suitable terminal implementation for the given file, create and return it. 332 Term makeTerm(File f = stderr) 333 { 334 if (isTerm(f)) 335 version (Windows) 336 return new WindowsTerm(f); 337 else 338 return new ANSITerm(f); 339 else 340 return new DumbTerm(f); 341 } 342 343 /// Get or set a Term implementation for the current thread (creating one if necessary). 344 private Term globalTerm; 345 @property Term term() 346 { 347 if (!globalTerm) 348 globalTerm = makeTerm(); 349 return globalTerm; 350 } 351 @property Term term(Term newTerm) /// ditto 352 { 353 return globalTerm = newTerm; 354 } 355 356 version (ae_sys_term_demo) 357 void main() 358 { 359 auto t = term; 360 foreach (bg; -1 .. 16) 361 { 362 t.put(t.bg(cast(Term.Color)bg)); 363 foreach (fg; -1 .. 16) 364 t.put(t.fg(cast(Term.Color)fg), "XX"); 365 t.put(t.none, t.bg.none, "\n"); 366 } 367 readln(); 368 }