| 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: | 
| 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, ioFlags: QIODevice::WriteOnly | QIODevice::Unbuffered); | 
| 48 |         m_coloringEnabled = isColoringPossible(); | 
| 49 |     } | 
| 50 |  | 
| 51 |     static const char *const foregrounds[]; | 
| 52 |     static const char *const backgrounds[]; | 
| 53 |  | 
| 54 |     inline void write(const QString &msg) { m_out.write(data: msg.toLocal8Bit()); } | 
| 55 |  | 
| 56 |     static QString escapeCode(const QString &in) | 
| 57 |     { | 
| 58 |         const ushort escapeChar = 0x1B; | 
| 59 |         QString result; | 
| 60 |         result.append(c: QChar(escapeChar)); | 
| 61 |         result.append(c: QLatin1Char('[')); | 
| 62 |         result.append(s: in); | 
| 63 |         result.append(c: QLatin1Char('m')); | 
| 64 |         return result; | 
| 65 |     } | 
| 66 |  | 
| 67 |     void insertColor(int id, ColorOutput::ColorCode code) { m_colorMapping.insert(akey: id, avalue: code); } | 
| 68 |     ColorOutput::ColorCode color(int id) const { return m_colorMapping.value(akey: id); } | 
| 69 |     bool containsColor(int id) const { return m_colorMapping.contains(akey: id); } | 
| 70 |  | 
| 71 |     bool isSilent() const { return m_silent; } | 
| 72 |     void setCurrentColorID(int colorId) { m_currentColorID = colorId; } | 
| 73 |  | 
| 74 |     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 |      */ | 
| 86 |     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(fd: 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 |  */ | 
| 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 |  */ | 
| 240 | void ColorOutput::write(const QString &message, int colorID) | 
| 241 | { | 
| 242 |     if (!d->isSilent()) | 
| 243 |         d->write(msg: colorify(message, color: 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 |  */ | 
| 252 | void ColorOutput::writeUncolored(const QString &message) | 
| 253 | { | 
| 254 |     if (!d->isSilent()) | 
| 255 |         d->write(msg: 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 |  */ | 
| 266 | 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(id: 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 |                         s: ColorOutputPrivate::escapeCode( | 
| 292 |                             in: QLatin1String(ColorOutputPrivate::foregrounds[foregroundCode - 1]))); | 
| 293 |             closureNeeded = true; | 
| 294 |         } | 
| 295 |  | 
| 296 |         if (backgroundCode > 0) { | 
| 297 |             finalMessage.append( | 
| 298 |                         s: ColorOutputPrivate::escapeCode( | 
| 299 |                             in: QLatin1String(ColorOutputPrivate::backgrounds[backgroundCode - 1]))); | 
| 300 |             closureNeeded = true; | 
| 301 |         } | 
| 302 |  | 
| 303 |         finalMessage.append(s: message); | 
| 304 |  | 
| 305 |         if (closureNeeded) | 
| 306 |             finalMessage.append(s: ColorOutputPrivate::escapeCode(in: 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 |  */ | 
| 317 | void ColorOutput::insertMapping(int colorID, const ColorCode colorCode) | 
| 318 | { | 
| 319 |     d->insertColor(id: colorID, code: colorCode); | 
| 320 | } | 
| 321 |  |