1 | // Copyright (C) 2019 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
3 | |
4 | #include "qcoloroutput_p.h" |
5 | |
6 | #include <QtCore/qfile.h> |
7 | #include <QtCore/qhash.h> |
8 | |
9 | #ifndef Q_OS_WIN |
10 | #include <unistd.h> |
11 | #endif |
12 | |
13 | QT_BEGIN_NAMESPACE |
14 | |
15 | class QColorOutputPrivate |
16 | { |
17 | public: |
18 | QColorOutputPrivate() |
19 | { |
20 | /* - QIODevice::Unbuffered because we want it to appear when the user actually calls, |
21 | * performance is considered of lower priority. |
22 | */ |
23 | m_out.open(stderr, ioFlags: QIODevice::WriteOnly | QIODevice::Unbuffered); |
24 | m_coloringEnabled = isColoringPossible(); |
25 | } |
26 | |
27 | static const char *const foregrounds[]; |
28 | static const char *const backgrounds[]; |
29 | |
30 | inline void write(const QString &msg) { m_out.write(data: msg.toLocal8Bit()); } |
31 | |
32 | static QString escapeCode(const QString &in) |
33 | { |
34 | const ushort escapeChar = 0x1B; |
35 | QString result; |
36 | result.append(c: QChar(escapeChar)); |
37 | result.append(c: QLatin1Char('[')); |
38 | result.append(s: in); |
39 | result.append(c: QLatin1Char('m')); |
40 | return result; |
41 | } |
42 | |
43 | void insertColor(int id, QColorOutput::ColorCode code) { m_colorMapping.insert(key: id, value: code); } |
44 | QColorOutput::ColorCode color(int id) const { return m_colorMapping.value(key: id); } |
45 | bool containsColor(int id) const { return m_colorMapping.contains(key: id); } |
46 | |
47 | void setSilent(bool silent) { m_silent = silent; } |
48 | bool isSilent() const { return m_silent; } |
49 | |
50 | void setCurrentColorID(int colorId) { m_currentColorID = colorId; } |
51 | |
52 | bool coloringEnabled() const { return m_coloringEnabled; } |
53 | |
54 | private: |
55 | QFile m_out; |
56 | QColorOutput::ColorMapping m_colorMapping; |
57 | int m_currentColorID = -1; |
58 | bool m_coloringEnabled = false; |
59 | bool m_silent = false; |
60 | |
61 | /* |
62 | Returns true if it's suitable to send colored output to \c stderr. |
63 | */ |
64 | inline bool isColoringPossible() const |
65 | { |
66 | #if defined(Q_OS_WIN) |
67 | /* Windows doesn't at all support ANSI escape codes, unless |
68 | * the user install a "device driver". See the Wikipedia links in the |
69 | * class documentation for details. */ |
70 | return false; |
71 | #else |
72 | /* We use QFile::handle() to get the file descriptor. It's a bit unsure |
73 | * whether it's 2 on all platforms and in all cases, so hopefully this layer |
74 | * of abstraction helps handle such cases. */ |
75 | return isatty(fd: m_out.handle()); |
76 | #endif |
77 | } |
78 | }; |
79 | |
80 | const char *const QColorOutputPrivate::foregrounds[] = |
81 | { |
82 | "0;30" , |
83 | "0;34" , |
84 | "0;32" , |
85 | "0;36" , |
86 | "0;31" , |
87 | "0;35" , |
88 | "0;33" , |
89 | "0;37" , |
90 | "1;30" , |
91 | "1;34" , |
92 | "1;32" , |
93 | "1;36" , |
94 | "1;31" , |
95 | "1;35" , |
96 | "1;33" , |
97 | "1;37" |
98 | }; |
99 | |
100 | const char *const QColorOutputPrivate::backgrounds[] = |
101 | { |
102 | "0;40" , |
103 | "0;44" , |
104 | "0;42" , |
105 | "0;46" , |
106 | "0;41" , |
107 | "0;45" , |
108 | "0;43" |
109 | }; |
110 | |
111 | /*! |
112 | \class QColorOutput |
113 | \nonreentrant |
114 | \brief Outputs colored messages to \c stderr. |
115 | \internal |
116 | |
117 | QColorOutput is a convenience class for outputting messages to \c |
118 | stderr using color escape codes, as mandated in ECMA-48. QColorOutput |
119 | will only color output when it is detected to be suitable. For |
120 | instance, if \c stderr is detected to be attached to a file instead |
121 | of a TTY, no coloring will be done. |
122 | |
123 | QColorOutput does its best attempt. but it is generally undefined |
124 | what coloring or effect the various coloring flags has. It depends |
125 | strongly on what terminal software that is being used. |
126 | |
127 | When using `echo -e 'my escape sequence'`, \c{\033} works as an |
128 | initiator but not when printing from a C++ program, despite having |
129 | escaped the backslash. That's why we below use characters with |
130 | value 0x1B. |
131 | |
132 | It can be convenient to subclass QColorOutput with a private scope, |
133 | such that the functions are directly available in the class using |
134 | it. |
135 | |
136 | \section1 Usage |
137 | |
138 | To output messages, call write() or writeUncolored(). write() takes |
139 | as second argument an integer, which QColorOutput uses as a lookup |
140 | key to find the color it should color the text in. The mapping from |
141 | keys to colors is done using insertMapping(). Typically this is used |
142 | by having enums for the various kinds of messages, which |
143 | subsequently are registered. |
144 | |
145 | \code |
146 | enum MyMessage |
147 | { |
148 | Error, |
149 | Important |
150 | }; |
151 | |
152 | QColorOutput output; |
153 | output.insertMapping(Error, QColorOutput::RedForeground); |
154 | output.insertMapping(Import, QColorOutput::BlueForeground); |
155 | |
156 | output.write("This is important", Important); |
157 | output.write("Jack, I'm only the selected official!", Error); |
158 | \endcode |
159 | |
160 | \sa {http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x329.html}{Bash Prompt HOWTO, 6.1. Colors}, |
161 | {http://linuxgazette.net/issue51/livingston-blade.html}{Linux Gazette, Tweaking Eterm, Edward Livingston-Blade}, |
162 | {http://www.ecma-international.org/publications/standards/Ecma-048.htm}{Standard ECMA-48, Control Functions for Coded Character Sets, ECMA International}, |
163 | {http://en.wikipedia.org/wiki/ANSI_escape_code}{Wikipedia, ANSI escape code}, |
164 | {http://linuxgazette.net/issue65/padala.html}{Linux Gazette, So You Like Color!, Pradeep Padala} |
165 | */ |
166 | |
167 | /*! |
168 | \internal |
169 | \enum QColorOutput::ColorCodeComponent |
170 | \value BlackForeground |
171 | \value BlueForeground |
172 | \value GreenForeground |
173 | \value CyanForeground |
174 | \value RedForeground |
175 | \value PurpleForeground |
176 | \value BrownForeground |
177 | \value LightGrayForeground |
178 | \value DarkGrayForeground |
179 | \value LightBlueForeground |
180 | \value LightGreenForeground |
181 | \value LightCyanForeground |
182 | \value LightRedForeground |
183 | \value LightPurpleForeground |
184 | \value YellowForeground |
185 | \value WhiteForeground |
186 | \value BlackBackground |
187 | \value BlueBackground |
188 | \value GreenBackground |
189 | \value CyanBackground |
190 | \value RedBackground |
191 | \value PurpleBackground |
192 | \value BrownBackground |
193 | |
194 | \value DefaultColor QColorOutput performs no coloring. This typically |
195 | means black on white or white on black, depending |
196 | on the settings of the user's terminal. |
197 | */ |
198 | |
199 | /*! |
200 | \internal |
201 | Constructs a QColorOutput instance, ready for use. |
202 | */ |
203 | QColorOutput::QColorOutput() : d(new QColorOutputPrivate) {} |
204 | |
205 | // must be here so that QScopedPointer has access to the complete type |
206 | QColorOutput::~QColorOutput() = default; |
207 | |
208 | bool QColorOutput::isSilent() const { return d->isSilent(); } |
209 | void QColorOutput::setSilent(bool silent) { d->setSilent(silent); } |
210 | |
211 | /*! |
212 | \internal |
213 | Sends \a message to \c stderr, using the color looked up in the color mapping using \a colorID. |
214 | |
215 | If \a color isn't available in the color mapping, result and behavior is undefined. |
216 | |
217 | If \a colorID is 0, which is the default value, the previously used coloring is used. QColorOutput |
218 | is initialized to not color at all. |
219 | |
220 | If \a message is empty, effects are undefined. |
221 | |
222 | \a message will be printed as is. For instance, no line endings will be inserted. |
223 | */ |
224 | void QColorOutput::write(QStringView message, int colorID) |
225 | { |
226 | if (!d->isSilent()) |
227 | d->write(msg: colorify(message, color: colorID)); |
228 | } |
229 | |
230 | void QColorOutput::writePrefixedMessage(const QString &message, QtMsgType type, |
231 | const QString &prefix) |
232 | { |
233 | static const QHash<QtMsgType, QString> prefixes = { |
234 | {QtMsgType::QtCriticalMsg, QStringLiteral("Error" )}, |
235 | {QtMsgType::QtWarningMsg, QStringLiteral("Warning" )}, |
236 | {QtMsgType::QtInfoMsg, QStringLiteral("Info" )}, |
237 | {QtMsgType::QtDebugMsg, QStringLiteral("Hint" )} |
238 | }; |
239 | |
240 | Q_ASSERT(prefixes.contains(type)); |
241 | Q_ASSERT(prefix.isEmpty() || prefix.front().isUpper()); |
242 | write(message: (prefix.isEmpty() ? prefixes[type] : prefix) + QStringLiteral(": " ), color: type); |
243 | writeUncolored(message); |
244 | } |
245 | |
246 | /*! |
247 | \internal |
248 | Writes \a message to \c stderr as if for instance |
249 | QTextStream would have been used, and adds a line ending at the end. |
250 | |
251 | This function can be practical to use such that one can use QColorOutput for all forms of writing. |
252 | */ |
253 | void QColorOutput::writeUncolored(const QString &message) |
254 | { |
255 | if (!d->isSilent()) |
256 | d->write(msg: message + QLatin1Char('\n')); |
257 | } |
258 | |
259 | /*! |
260 | \internal |
261 | Treats \a message and \a colorID identically to write(), but instead of writing |
262 | \a message to \c stderr, it is prepared for being written to \c stderr, but is then |
263 | returned. |
264 | |
265 | This is useful when the colored string is inserted into a translated string(dividing |
266 | the string into several small strings prevents proper translation). |
267 | */ |
268 | QString QColorOutput::colorify(const QStringView message, int colorID) const |
269 | { |
270 | Q_ASSERT_X(colorID == -1 || d->containsColor(colorID), Q_FUNC_INFO, |
271 | qPrintable(QString::fromLatin1("There is no color registered by id %1" ) |
272 | .arg(colorID))); |
273 | Q_ASSERT_X(!message.isEmpty(), Q_FUNC_INFO, |
274 | "It makes no sense to attempt to print an empty string." ); |
275 | |
276 | if (colorID != -1) |
277 | d->setCurrentColorID(colorID); |
278 | |
279 | if (d->coloringEnabled() && colorID != -1) { |
280 | const int color = d->color(id: colorID); |
281 | |
282 | /* If DefaultColor is set, we don't want to color it. */ |
283 | if (color & DefaultColor) |
284 | return message.toString(); |
285 | |
286 | const int foregroundCode = (color & ForegroundMask) >> ForegroundShift; |
287 | const int backgroundCode = (color & BackgroundMask) >> BackgroundShift; |
288 | QString finalMessage; |
289 | bool closureNeeded = false; |
290 | |
291 | if (foregroundCode > 0) { |
292 | finalMessage.append( |
293 | s: QColorOutputPrivate::escapeCode( |
294 | in: QLatin1String(QColorOutputPrivate::foregrounds[foregroundCode - 1]))); |
295 | closureNeeded = true; |
296 | } |
297 | |
298 | if (backgroundCode > 0) { |
299 | finalMessage.append( |
300 | s: QColorOutputPrivate::escapeCode( |
301 | in: QLatin1String(QColorOutputPrivate::backgrounds[backgroundCode - 1]))); |
302 | closureNeeded = true; |
303 | } |
304 | |
305 | finalMessage.append(v: message); |
306 | |
307 | if (closureNeeded) |
308 | finalMessage.append(s: QColorOutputPrivate::escapeCode(in: QLatin1String("0" ))); |
309 | |
310 | return finalMessage; |
311 | } |
312 | |
313 | return message.toString(); |
314 | } |
315 | |
316 | /*! |
317 | \internal |
318 | Adds a color mapping from \a colorID to \a colorCode, for this QColorOutput instance. |
319 | */ |
320 | void QColorOutput::insertMapping(int colorID, const ColorCode colorCode) |
321 | { |
322 | d->insertColor(id: colorID, code: colorCode); |
323 | } |
324 | |
325 | QT_END_NAMESPACE |
326 | |