1 /**
2  * Control the various text mode attributes, such as color, when writing text
3  * to the console.
4  *
5  * Copyright:   Copyright (C) 1999-2023 by The D Language Foundation, All Rights Reserved
6  * Authors:     $(LINK2 https://www.digitalmars.com, Walter Bright)
7  * License:     $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
8  * Source:      $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/console.d, _console.d)
9  * Documentation:  https://dlang.org/phobos/dmd_console.html
10  * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/src/dmd/console.d
11  */
12 
13 module dmd.console;
14 
15 import core.stdc.stdio;
16 
17 version (Windows)
18 {
19     import core.sys.windows.winbase;
20     import core.sys.windows.wincon;
21     import core.sys.windows.windef;
22 
23     private extern (C) int isatty(int) @trusted @nogc nothrow;
24 }
25 else version (Posix)
26 {
27     import core.sys.posix.unistd;
28 }
29 else
30 {
31     static assert(0);
32 }
33 
34 enum Color : int
35 {
36     black         = 0,
37     red           = 1,
38     green         = 2,
39     blue          = 4,
40     yellow        = red | green,
41     magenta       = red | blue,
42     cyan          = green | blue,
43     lightGray     = red | green | blue,
44     bright        = 8,
45     darkGray      = bright | black,
46     brightRed     = bright | red,
47     brightGreen   = bright | green,
48     brightBlue    = bright | blue,
49     brightYellow  = bright | yellow,
50     brightMagenta = bright | magenta,
51     brightCyan    = bright | cyan,
52     white         = bright | lightGray,
53 }
54 
55 interface Console
56 {
57   nothrow:
58     @property FILE* fp();
59 
60     /**
61      * Turn on/off intensity.
62      * Params:
63      *      bright = turn it on
64      */
65     void setColorBright(bool bright);
66 
67     /**
68      * Set color and intensity.
69      * Params:
70      *      color = the color
71      */
72     void setColor(Color color);
73 
74     /**
75      * Reset console attributes to what they were
76      * when create() was called.
77      */
78     void resetColor();
79 }
80 
81 version (Windows)
82 private final class WindowsConsole : Console
83 {
84   nothrow:
85 
86   private:
87     CONSOLE_SCREEN_BUFFER_INFO sbi;
88     HANDLE handle;
89     FILE* _fp;
90 
91     static HANDLE getStdHandle(FILE *fp)
92     {
93         /* Determine if stream fp is a console
94          */
95         version (CRuntime_DigitalMars)
96         {
97             if (!isatty(fp._file))
98                 return null;
99         }
100         else version (CRuntime_Microsoft)
101         {
102             if (!isatty(fileno(fp)))
103                 return null;
104         }
105         else
106         {
107             static assert(0, "Unsupported Windows runtime.");
108         }
109 
110         if (fp == stdout)
111             return GetStdHandle(STD_OUTPUT_HANDLE);
112         else if (fp == stderr)
113             return GetStdHandle(STD_ERROR_HANDLE);
114         else
115             return null;
116     }
117 
118   public:
119 
120     @property FILE* fp() { return _fp; }
121 
122     static WindowsConsole create(FILE* fp)
123     {
124         auto h = getStdHandle(fp);
125         if (h is null)
126             return null;
127 
128         CONSOLE_SCREEN_BUFFER_INFO sbi;
129         if (GetConsoleScreenBufferInfo(h, &sbi) == 0) // get initial state of console
130             return null;
131 
132         auto c = new WindowsConsole();
133         c._fp = fp;
134         c.handle = h;
135         c.sbi = sbi;
136         return c;
137     }
138 
139     void setColorBright(bool bright)
140     {
141         SetConsoleTextAttribute(handle, sbi.wAttributes | (bright ? FOREGROUND_INTENSITY : 0));
142     }
143 
144     void setColor(Color color)
145     {
146         enum FOREGROUND_WHITE = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE;
147         WORD attr = sbi.wAttributes;
148         attr = (attr & ~(FOREGROUND_WHITE | FOREGROUND_INTENSITY)) |
149                 ((color & Color.red)    ? FOREGROUND_RED   : 0) |
150                 ((color & Color.green)  ? FOREGROUND_GREEN : 0) |
151                 ((color & Color.blue)   ? FOREGROUND_BLUE  : 0) |
152                 ((color & Color.bright) ? FOREGROUND_INTENSITY : 0);
153         SetConsoleTextAttribute(handle, attr);
154     }
155 
156     void resetColor()
157     {
158         SetConsoleTextAttribute(handle, sbi.wAttributes);
159     }
160 }
161 
162 /* Uses the ANSI escape codes.
163  * https://en.wikipedia.org/wiki/ANSI_escape_code
164  * Foreground colors: 30..37
165  * Background colors: 40..47
166  * Attributes:
167  *  0: reset all attributes
168  *  1: high intensity
169  *  2: low intensity
170  *  3: italic
171  *  4: single line underscore
172  *  5: slow blink
173  *  6: fast blink
174  *  7: reverse video
175  *  8: hidden
176  */
177 private final class ANSIConsole : Console
178 {
179   nothrow:
180 
181   private:
182     FILE* _fp;
183 
184   public:
185 
186     this(FILE* fp) @safe { _fp = fp; }
187 
188     @property FILE* fp() { return _fp; }
189 
190     void setColorBright(bool bright)
191     {
192         fprintf(_fp, "\033[%dm", bright);
193     }
194 
195     void setColor(Color color)
196     {
197         fprintf(_fp, "\033[%d;%dm", color & Color.bright ? 1 : 0, 30 + (color & ~Color.bright));
198     }
199 
200     void resetColor()
201     {
202         fputs("\033[m", _fp);
203     }
204 }
205 
206 /**
207  Tries to detect whether DMD has been invoked from a terminal.
208  Returns: `true` if a terminal has been detected, `false` otherwise
209  */
210 bool detectTerminal() nothrow
211 {
212     version (Posix)
213     {
214         import core.stdc.stdlib : getenv;
215         const(char)* term = getenv("TERM");
216         import core.stdc.string : strcmp;
217         return isatty(STDERR_FILENO) && term && term[0] && strcmp(term, "dumb") != 0;
218     }
219     else version (Windows)
220     {
221         auto h = WindowsConsole.getStdHandle(stderr);
222         if (h is null)
223             return false;
224 
225         CONSOLE_SCREEN_BUFFER_INFO sbi;
226         return GetConsoleScreenBufferInfo(h, &sbi) != 0;
227     }
228 }
229 
230 /**
231  * Tries to detect the preference for colorized console output
232  * based on the `NO_COLOR` environment variable: https://no-color.org/
233  *
234  * Returns: `true` if colorized console output is preferred
235 */
236 bool detectColorPreference() nothrow @trusted
237 {
238     import core.stdc.stdlib : getenv;
239     const noColor = getenv("NO_COLOR");
240         return noColor == null || noColor[0] == '\0';
241 }
242 
243 /**
244  * Creates an instance of Console connected to stream fp.
245  * Params:
246  *      fp = io stream
247  * Returns:
248  *      reference to created Console
249  */
250 Console createConsole(FILE* fp) nothrow
251 {
252     version (Windows)
253     {
254         if (auto c = WindowsConsole.create(fp))
255             return c;
256     }
257 
258     return new ANSIConsole(fp);
259 }