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