2013-09-03 06:47:17 +03:00
|
|
|
/****************************************************************************
|
|
|
|
|
**
|
2016-01-15 14:58:39 +01:00
|
|
|
** Copyright (C) 2016 Petar Perisin <petar.perisin@gmail.com>
|
|
|
|
|
** Contact: https://www.qt.io/licensing/
|
2013-09-03 06:47:17 +03:00
|
|
|
**
|
|
|
|
|
** This file is part of Qt Creator.
|
|
|
|
|
**
|
|
|
|
|
** Commercial License Usage
|
|
|
|
|
** Licensees holding valid commercial Qt licenses may use this file in
|
|
|
|
|
** accordance with the commercial license agreement provided with the
|
|
|
|
|
** Software or, alternatively, in accordance with the terms contained in
|
2016-01-15 14:58:39 +01:00
|
|
|
** a written agreement between you and The Qt Company. For licensing terms
|
|
|
|
|
** and conditions see https://www.qt.io/terms-conditions. For further
|
|
|
|
|
** information use the contact form at https://www.qt.io/contact-us.
|
2013-09-03 06:47:17 +03:00
|
|
|
**
|
2016-01-15 14:58:39 +01:00
|
|
|
** GNU General Public License Usage
|
|
|
|
|
** Alternatively, this file may be used under the terms of the GNU
|
|
|
|
|
** General Public License version 3 as published by the Free Software
|
|
|
|
|
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
|
|
|
|
** included in the packaging of this file. Please review the following
|
|
|
|
|
** information to ensure the GNU General Public License requirements will
|
|
|
|
|
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
2013-09-03 06:47:17 +03:00
|
|
|
**
|
|
|
|
|
****************************************************************************/
|
|
|
|
|
|
|
|
|
|
#include "ansiescapecodehandler.h"
|
|
|
|
|
#include <utils/qtcassert.h>
|
|
|
|
|
|
|
|
|
|
namespace Utils {
|
|
|
|
|
|
|
|
|
|
/*!
|
|
|
|
|
\class Utils::AnsiEscapeCodeHandler
|
|
|
|
|
|
|
|
|
|
\brief The AnsiEscapeCodeHandler class parses text and extracts ANSI escape codes from it.
|
|
|
|
|
|
|
|
|
|
In order to preserve color information across text segments, an instance of this class
|
|
|
|
|
must be stored for the lifetime of a stream.
|
|
|
|
|
Also, one instance of this class should not handle multiple streams (at least not
|
|
|
|
|
at the same time).
|
|
|
|
|
|
2013-10-07 13:34:40 +02:00
|
|
|
Its main function is parseText(), which accepts text and default QTextCharFormat.
|
|
|
|
|
This function is designed to parse text and split colored text to smaller strings,
|
2013-09-03 06:47:17 +03:00
|
|
|
with their appropriate formatting information set inside QTextCharFormat.
|
|
|
|
|
|
|
|
|
|
Usage:
|
|
|
|
|
\list
|
|
|
|
|
\li Create new instance of AnsiEscapeCodeHandler for a stream.
|
|
|
|
|
\li To add new text, call parseText() with the text and a default QTextCharFormat.
|
2013-10-07 13:34:40 +02:00
|
|
|
The result of this function is a list of strings with formats set in appropriate
|
2013-09-03 06:47:17 +03:00
|
|
|
QTextCharFormat.
|
|
|
|
|
\endlist
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
AnsiEscapeCodeHandler::AnsiEscapeCodeHandler() :
|
|
|
|
|
m_previousFormatClosed(true)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static QColor ansiColor(uint code)
|
|
|
|
|
{
|
|
|
|
|
QTC_ASSERT(code < 8, return QColor());
|
|
|
|
|
|
|
|
|
|
const int red = code & 1 ? 170 : 0;
|
|
|
|
|
const int green = code & 2 ? 170 : 0;
|
|
|
|
|
const int blue = code & 4 ? 170 : 0;
|
|
|
|
|
return QColor(red, green, blue);
|
|
|
|
|
}
|
|
|
|
|
|
2014-01-17 15:55:27 +01:00
|
|
|
QList<FormattedText> AnsiEscapeCodeHandler::parseText(const FormattedText &input)
|
2013-09-03 06:47:17 +03:00
|
|
|
{
|
2014-01-17 15:37:13 +01:00
|
|
|
enum AnsiEscapeCodes {
|
|
|
|
|
ResetFormat = 0,
|
|
|
|
|
BoldText = 1,
|
|
|
|
|
TextColorStart = 30,
|
|
|
|
|
TextColorEnd = 37,
|
|
|
|
|
RgbTextColor = 38,
|
|
|
|
|
DefaultTextColor = 39,
|
|
|
|
|
BackgroundColorStart = 40,
|
|
|
|
|
BackgroundColorEnd = 47,
|
|
|
|
|
RgbBackgroundColor = 48,
|
|
|
|
|
DefaultBackgroundColor = 49
|
|
|
|
|
};
|
|
|
|
|
|
2013-09-11 10:16:03 +03:00
|
|
|
const QString escape = QLatin1String("\x1b[");
|
2013-09-03 06:47:17 +03:00
|
|
|
const QChar semicolon = QLatin1Char(';');
|
|
|
|
|
const QChar colorTerminator = QLatin1Char('m');
|
2014-03-18 20:23:08 +02:00
|
|
|
const QChar eraseToEol = QLatin1Char('K');
|
2015-07-11 23:35:49 -07:00
|
|
|
|
|
|
|
|
QList<FormattedText> outputData;
|
|
|
|
|
QTextCharFormat charFormat = m_previousFormatClosed ? input.format : m_previousFormat;
|
|
|
|
|
QString strippedText;
|
|
|
|
|
if (m_pendingText.isEmpty()) {
|
|
|
|
|
strippedText = input.text;
|
|
|
|
|
} else {
|
|
|
|
|
strippedText = m_pendingText.append(input.text);
|
|
|
|
|
m_pendingText.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2013-09-03 06:47:17 +03:00
|
|
|
while (!strippedText.isEmpty()) {
|
2015-07-11 23:35:49 -07:00
|
|
|
QTC_ASSERT(m_pendingText.isEmpty(), break);
|
|
|
|
|
const int escapePos = strippedText.indexOf(escape[0]);
|
|
|
|
|
if (escapePos < 0) {
|
|
|
|
|
outputData << FormattedText(strippedText, charFormat);
|
|
|
|
|
break;
|
|
|
|
|
} else if (escapePos != 0) {
|
|
|
|
|
outputData << FormattedText(strippedText.left(escapePos), charFormat);
|
|
|
|
|
strippedText.remove(0, escapePos);
|
|
|
|
|
}
|
|
|
|
|
QTC_ASSERT(strippedText[0] == escape[0], break);
|
|
|
|
|
|
|
|
|
|
while (!strippedText.isEmpty() && escape[0] == strippedText[0]) {
|
|
|
|
|
if (escape.startsWith(strippedText)) {
|
|
|
|
|
// control secquence is not complete
|
|
|
|
|
m_pendingText += strippedText;
|
|
|
|
|
strippedText.clear();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (!strippedText.startsWith(escape)) {
|
|
|
|
|
// not a control sequence
|
|
|
|
|
m_pendingText.clear();
|
|
|
|
|
outputData << FormattedText(strippedText.left(1), charFormat);
|
|
|
|
|
strippedText.remove(0, 1);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2019-01-17 01:38:54 +01:00
|
|
|
m_pendingText += strippedText.midRef(0, escape.length());
|
2015-07-11 23:35:49 -07:00
|
|
|
strippedText.remove(0, escape.length());
|
2013-09-03 06:47:17 +03:00
|
|
|
|
2014-03-18 20:23:08 +02:00
|
|
|
// \e[K is not supported. Just strip it.
|
|
|
|
|
if (strippedText.startsWith(eraseToEol)) {
|
2015-07-11 23:35:49 -07:00
|
|
|
m_pendingText.clear();
|
2014-03-18 20:23:08 +02:00
|
|
|
strippedText.remove(0, 1);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2013-09-03 06:47:17 +03:00
|
|
|
// get the number
|
|
|
|
|
QString strNumber;
|
|
|
|
|
QStringList numbers;
|
2015-07-11 23:35:49 -07:00
|
|
|
while (!strippedText.isEmpty()) {
|
2013-09-03 06:47:17 +03:00
|
|
|
if (strippedText.at(0).isDigit()) {
|
|
|
|
|
strNumber += strippedText.at(0);
|
|
|
|
|
} else {
|
2015-07-11 23:35:49 -07:00
|
|
|
if (!strNumber.isEmpty())
|
|
|
|
|
numbers << strNumber;
|
|
|
|
|
if (strNumber.isEmpty() || strippedText.at(0) != semicolon)
|
|
|
|
|
break;
|
2013-09-03 06:47:17 +03:00
|
|
|
strNumber.clear();
|
|
|
|
|
}
|
2019-01-17 01:38:54 +01:00
|
|
|
m_pendingText += strippedText.midRef(0, 1);
|
2013-09-03 06:47:17 +03:00
|
|
|
strippedText.remove(0, 1);
|
|
|
|
|
}
|
2015-07-11 23:35:49 -07:00
|
|
|
if (strippedText.isEmpty())
|
|
|
|
|
break;
|
2013-09-03 06:47:17 +03:00
|
|
|
|
|
|
|
|
// remove terminating char
|
|
|
|
|
if (!strippedText.startsWith(colorTerminator)) {
|
2015-07-11 23:35:49 -07:00
|
|
|
m_pendingText.clear();
|
2013-09-03 06:47:17 +03:00
|
|
|
strippedText.remove(0, 1);
|
2015-07-11 23:35:49 -07:00
|
|
|
break;
|
2013-09-03 06:47:17 +03:00
|
|
|
}
|
2015-07-11 23:35:49 -07:00
|
|
|
// got consistent control sequence, ok to clear pending text
|
|
|
|
|
m_pendingText.clear();
|
2013-09-03 06:47:17 +03:00
|
|
|
strippedText.remove(0, 1);
|
|
|
|
|
|
2013-09-09 22:49:36 +03:00
|
|
|
if (numbers.isEmpty()) {
|
2014-01-17 15:55:27 +01:00
|
|
|
charFormat = input.format;
|
2013-09-09 22:49:36 +03:00
|
|
|
endFormatScope();
|
|
|
|
|
}
|
|
|
|
|
|
2013-09-03 06:47:17 +03:00
|
|
|
for (int i = 0; i < numbers.size(); ++i) {
|
|
|
|
|
const int code = numbers.at(i).toInt();
|
|
|
|
|
|
|
|
|
|
if (code >= TextColorStart && code <= TextColorEnd) {
|
|
|
|
|
charFormat.setForeground(ansiColor(code - TextColorStart));
|
|
|
|
|
setFormatScope(charFormat);
|
|
|
|
|
} else if (code >= BackgroundColorStart && code <= BackgroundColorEnd) {
|
|
|
|
|
charFormat.setBackground(ansiColor(code - BackgroundColorStart));
|
|
|
|
|
setFormatScope(charFormat);
|
|
|
|
|
} else {
|
|
|
|
|
switch (code) {
|
|
|
|
|
case ResetFormat:
|
2014-01-17 15:55:27 +01:00
|
|
|
charFormat = input.format;
|
2013-09-03 06:47:17 +03:00
|
|
|
endFormatScope();
|
|
|
|
|
break;
|
|
|
|
|
case BoldText:
|
|
|
|
|
charFormat.setFontWeight(QFont::Bold);
|
|
|
|
|
setFormatScope(charFormat);
|
|
|
|
|
break;
|
|
|
|
|
case DefaultTextColor:
|
2014-01-17 15:55:27 +01:00
|
|
|
charFormat.setForeground(input.format.foreground());
|
2013-09-03 06:47:17 +03:00
|
|
|
setFormatScope(charFormat);
|
|
|
|
|
break;
|
|
|
|
|
case DefaultBackgroundColor:
|
2014-01-17 15:55:27 +01:00
|
|
|
charFormat.setBackground(input.format.background());
|
2013-09-03 06:47:17 +03:00
|
|
|
setFormatScope(charFormat);
|
|
|
|
|
break;
|
|
|
|
|
case RgbTextColor:
|
|
|
|
|
case RgbBackgroundColor:
|
2014-07-04 15:40:16 -04:00
|
|
|
// See http://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
2013-09-03 06:47:17 +03:00
|
|
|
if (++i >= numbers.size())
|
|
|
|
|
break;
|
2014-07-04 15:40:16 -04:00
|
|
|
switch (numbers.at(i).toInt()) {
|
|
|
|
|
case 2:
|
2013-09-03 06:47:17 +03:00
|
|
|
// RGB set with format: 38;2;<r>;<g>;<b>
|
|
|
|
|
if ((i + 3) < numbers.size()) {
|
|
|
|
|
(code == RgbTextColor) ?
|
|
|
|
|
charFormat.setForeground(QColor(numbers.at(i + 1).toInt(),
|
|
|
|
|
numbers.at(i + 2).toInt(),
|
|
|
|
|
numbers.at(i + 3).toInt())) :
|
|
|
|
|
charFormat.setBackground(QColor(numbers.at(i + 1).toInt(),
|
|
|
|
|
numbers.at(i + 2).toInt(),
|
|
|
|
|
numbers.at(i + 3).toInt()));
|
|
|
|
|
setFormatScope(charFormat);
|
|
|
|
|
}
|
|
|
|
|
i += 3;
|
2014-07-04 15:40:16 -04:00
|
|
|
break;
|
|
|
|
|
case 5:
|
|
|
|
|
// 256 color mode with format: 38;5;<i>
|
|
|
|
|
uint index = numbers.at(i + 1).toInt();
|
|
|
|
|
|
|
|
|
|
QColor color;
|
|
|
|
|
if (index < 8) {
|
|
|
|
|
// The first 8 colors are standard low-intensity ANSI colors.
|
|
|
|
|
color = ansiColor(index);
|
|
|
|
|
} else if (index < 16) {
|
|
|
|
|
// The next 8 colors are standard high-intensity ANSI colors.
|
|
|
|
|
color = ansiColor(index - 8).lighter(150);
|
|
|
|
|
} else if (index < 232) {
|
|
|
|
|
// The next 216 colors are a 6x6x6 RGB cube.
|
|
|
|
|
uint o = index - 16;
|
|
|
|
|
color = QColor((o / 36) * 51, ((o / 6) % 6) * 51, (o % 6) * 51);
|
|
|
|
|
} else {
|
|
|
|
|
// The last 24 colors are a greyscale gradient.
|
|
|
|
|
uint grey = (index - 232) * 11;
|
|
|
|
|
color = QColor(grey, grey, grey);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (code == RgbTextColor)
|
|
|
|
|
charFormat.setForeground(color);
|
|
|
|
|
else
|
|
|
|
|
charFormat.setBackground(color);
|
|
|
|
|
|
|
|
|
|
setFormatScope(charFormat);
|
2013-09-03 06:47:17 +03:00
|
|
|
++i;
|
2014-07-04 15:40:16 -04:00
|
|
|
break;
|
2013-09-03 06:47:17 +03:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return outputData;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AnsiEscapeCodeHandler::endFormatScope()
|
|
|
|
|
{
|
|
|
|
|
m_previousFormatClosed = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AnsiEscapeCodeHandler::setFormatScope(const QTextCharFormat &charFormat)
|
|
|
|
|
{
|
|
|
|
|
m_previousFormat = charFormat;
|
|
|
|
|
m_previousFormatClosed = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace Utils
|