Files
seer/src/QAnsiTextEdit.cpp
T
2025-08-26 17:26:05 -05:00

397 lines
14 KiB
C++

// SPDX-FileCopyrightText: 2021 Ernie Pasveer <epasveer@att.net>
//
// SPDX-License-Identifier: MIT
#include "QAnsiTextEdit.h"
#include <QtCore/QDebug>
//
//
//
QAnsiTextEditFormattedText::QAnsiTextEditFormattedText(const QString& txt, const QTextCharFormat& fmt) {
text = txt;
format = fmt;
}
//
//
//
QList<QAnsiTextEditFormattedText> QAnsiTextEditEscapeCodeHandler::parseText (const QAnsiTextEditFormattedText& input) {
enum AnsiEscapeCodes {
ResetFormat = 0,
BoldText = 1,
UnderLinedText = 4,
TextColorStart = 30,
TextColorEnd = 37,
RgbTextColor = 38,
DefaultTextColor = 39,
BackgroundColorStart = 40,
BackgroundColorEnd = 47,
RgbBackgroundColor = 48,
DefaultBackgroundColor = 49
};
const QString escape = "\x1b[";
const QChar semicolon = ';';
const QChar colorTerminator = 'm';
const QChar eraseToEol = 'K';
QList<QAnsiTextEditFormattedText> outputData;
QTextCharFormat charFormat = _previousFormatClosed ? input.format : _previousFormat;
QString strippedText;
if (_pendingText.isEmpty()) {
strippedText = input.text;
}else{
strippedText = _pendingText.append(input.text);
_pendingText.clear();
}
while (!strippedText.isEmpty()) {
if (_waitingForTerminator) {
// We ignore all escape codes taking string arguments.
QString terminator = "\x1b\\";
int terminatorPos = strippedText.indexOf(terminator);
if (terminatorPos == -1 && !_alternateTerminator.isEmpty()) {
terminator = _alternateTerminator;
terminatorPos = strippedText.indexOf(terminator);
}
if (terminatorPos == -1) {
_pendingText = strippedText;
break;
}
_waitingForTerminator = false;
_alternateTerminator.clear();
strippedText.remove(0, terminatorPos + terminator.length());
if (strippedText.isEmpty()) {
break;
}
}
const int escapePos = strippedText.indexOf(escape.at(0));
if (escapePos < 0) {
outputData << QAnsiTextEditFormattedText(strippedText, charFormat);
break;
}else if (escapePos != 0) {
outputData << QAnsiTextEditFormattedText(strippedText.left(escapePos), charFormat);
strippedText.remove(0, escapePos);
}
while (!strippedText.isEmpty() && escape.at(0) == strippedText.at(0)) {
if (escape.startsWith(strippedText)) {
// control secquence is not complete
_pendingText += strippedText;
strippedText.clear();
break;
}
if (!strippedText.startsWith(escape)) {
switch (strippedText.at(1).toLatin1()) {
case '\\': // Unexpected terminator sequence.
Q_FALLTHROUGH();
case 'N': case 'O': // Ignore unsupported single-character sequences.
strippedText.remove(0, 2);
break;
case ']':
_alternateTerminator = QChar(7);
Q_FALLTHROUGH();
case 'P': case 'X': case '^': case '_':
strippedText.remove(0, 2);
_waitingForTerminator = true;
break;
default:
// not a control sequence
_pendingText.clear();
outputData << QAnsiTextEditFormattedText(strippedText.left(1), charFormat);
strippedText.remove(0, 1);
continue;
}
break;
}
_pendingText += strippedText.mid(0, escape.length());
strippedText.remove(0, escape.length());
// \e[K is not supported. Just strip it.
if (strippedText.startsWith(eraseToEol)) {
_pendingText.clear();
strippedText.remove(0, 1);
continue;
}
// get the number
QString strNumber;
QStringList numbers;
while (!strippedText.isEmpty()) {
if (strippedText.at(0).isDigit()) {
strNumber += strippedText.at(0);
}else{
if (!strNumber.isEmpty()) {
numbers << strNumber;
}
if (strNumber.isEmpty() || strippedText.at(0) != semicolon) {
break;
}
strNumber.clear();
}
_pendingText += strippedText.mid(0, 1);
strippedText.remove(0, 1);
}
if (strippedText.isEmpty()) {
break;
}
// remove terminating char
if (!strippedText.startsWith(colorTerminator)) {
_pendingText.clear();
strippedText.remove(0, 1);
break;
}
// got consistent control sequence, ok to clear pending text
_pendingText.clear();
strippedText.remove(0, 1);
if (numbers.isEmpty()) {
charFormat = input.format;
endFormatScope();
}
for (int i = 0; i < numbers.size(); ++i) {
const uint code = numbers.at(i).toUInt();
if (code >= TextColorStart && code <= TextColorEnd) {
//qDebug() << "TextColorStart/TextColorEnd called";
charFormat.setForeground(ansiColor(code - TextColorStart));
setFormatScope(charFormat);
}else if (code >= BackgroundColorStart && code <= BackgroundColorEnd) {
//qDebug() << "BackgroundColorStart/BackgroundColorEnd called";
charFormat.setBackground(ansiColor(code - BackgroundColorStart));
setFormatScope(charFormat);
}else{
switch (code) {
case ResetFormat:
//qDebug() << "ResetFormat called";
charFormat.setFontWeight(QFont::Normal);
charFormat.setFontUnderline(false);
charFormat.setForeground(input.format.foreground());
charFormat.setBackground(input.format.background());
setFormatScope(charFormat);
endFormatScope();
break;
case BoldText:
//qDebug() << "BoldText called";
charFormat.setFontWeight(QFont::ExtraBold);
setFormatScope(charFormat);
break;
case UnderLinedText:
//qDebug() << "UnderLinedText called";
charFormat.setFontUnderline(true);
setFormatScope(charFormat);
break;
case DefaultTextColor:
//qDebug() << "DefaultTextColor called";
charFormat.setForeground(input.format.foreground());
setFormatScope(charFormat);
break;
case DefaultBackgroundColor:
//qDebug() << "DefaultBackgroundColor called";
charFormat.setBackground(input.format.background());
setFormatScope(charFormat);
break;
case RgbTextColor:
case RgbBackgroundColor:
//qDebug() << "RgbTextColor/RgbBackgroundColor called";
// See http://en.wikipedia.org/wiki/ANSI_escape_code#Colors
if (++i >= numbers.size()) {
break;
}
switch (numbers.at(i).toInt()) {
case 2:
// 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;
break;
case 5:
// 256 color mode with format: 38;5;<i>
uint index = numbers.at(i + 1).toUInt();
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.
int grey = int((index - 232) * 11);
color = QColor(grey, grey, grey);
}
if (code == RgbTextColor) {
charFormat.setForeground(color);
}else{
charFormat.setBackground(color);
}
setFormatScope(charFormat);
++i;
break;
}
break;
default:
qDebug() << "Unkown code:" << code;
break;
}
}
}
}
}
return outputData;
}
void QAnsiTextEditEscapeCodeHandler::endFormatScope () {
_previousFormatClosed = true;
}
void QAnsiTextEditEscapeCodeHandler::setFormatScope (const QTextCharFormat& charFormat) {
_previousFormat = charFormat;
_previousFormatClosed = false;
}
QTextCharFormat QAnsiTextEditEscapeCodeHandler::formatScope () const {
return _previousFormat;
}
QColor QAnsiTextEditEscapeCodeHandler::ansiColor(uint code) {
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);
}
//
//
//
QAnsiTextEdit::QAnsiTextEdit (QWidget* parent) : QPlainTextEdit(parent) {
}
QAnsiTextEdit::QAnsiTextEdit (const QString& text, QWidget* parent) : QPlainTextEdit(parent) {
setAnsiText(text);
}
QAnsiTextEdit::~QAnsiTextEdit () {
}
void QAnsiTextEdit::dumpCharFormat (QString string, QTextCharFormat format) {
qDebug() << "dumpCharFormat" << string << "Font" << format.font().toString();
qDebug() << "dumpCharFormat" << string << "Color" << format.foreground().color();
qDebug() << "dumpCharFormat" << string << "FontWeight" << format.fontWeight();
qDebug() << "dumpCharFormat" << string << "UnderLined" << format.fontUnderline();
qDebug() << "";
}
void QAnsiTextEdit::setAnsiText (const QString& text) {
// Reset the text edit
clear();
// Use append method.
appendAnsiText(text);
}
void QAnsiTextEdit::appendAnsiText (const QString& text) {
//
// Use the 'append' method for the first sub text, then
// the 'insert' method for every sub text after that.
//
// Save current TextCharFormat
QTextCharFormat saveFormat = currentCharFormat();
// Create the default text object;
QAnsiTextEditFormattedText ftext;
ftext.text = text;
ftext.format = currentCharFormat();
// Parse it. A list of sub text objects is created.
QList<QAnsiTextEditFormattedText> ftexts = _escapeCodeHandler.parseText(ftext);
// Print each sub text object. Each one has its own text format.
bool first = true;
for (const QAnsiTextEditFormattedText& ftext : ftexts) {
QTextCharFormat format = ftext.format;
setCurrentCharFormat(format);
if (first) {
appendPlainText(ftext.text);
first = false;
}else{
insertPlainText(ftext.text);
}
}
// Restore current TextCharFormat
setCurrentCharFormat(saveFormat);
}
void QAnsiTextEdit::insertAnsiText (const QString& text) {
//
// Use the 'insert' method for every sub text.
//
// Save current TextCharFormat
QTextCharFormat saveFormat = currentCharFormat();
// Create the default text object;
QAnsiTextEditFormattedText ftext;
ftext.text = text;
ftext.format = currentCharFormat();
// Parse it. A list of sub text objects is created.
QList<QAnsiTextEditFormattedText> ftexts = _escapeCodeHandler.parseText(ftext);
// Print each sub text object. Each one has its own text format.
for (const QAnsiTextEditFormattedText& ftext : ftexts) {
QTextCharFormat format = ftext.format;
setCurrentCharFormat(format);
insertPlainText(ftext.text);
}
// Restore current TextCharFormat
setCurrentCharFormat(saveFormat);
}