diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4f0d36a..27bc06c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -141,6 +141,7 @@ set(HEADER_FILES QEditDelegate.h QHContainerWidget.h QImageViewer.h + QAnsiTextEdit.h ) set(SOURCE_FILES @@ -234,6 +235,7 @@ set(SOURCE_FILES QEditDelegate.cpp QHContainerWidget.cpp QImageViewer.cpp + QAnsiTextEdit.cpp ) if(${QTVERSION} STREQUAL "QT6") diff --git a/src/QAnsiTextEdit.cpp b/src/QAnsiTextEdit.cpp new file mode 100644 index 0000000..7d1079a --- /dev/null +++ b/src/QAnsiTextEdit.cpp @@ -0,0 +1,375 @@ +#include "QAnsiTextEdit.h" +#include + +// +// +// +QAnsiTextEditFormattedText::QAnsiTextEditFormattedText(const QString& txt, const QTextCharFormat& fmt) { + text = txt; + format = fmt; +} + +// +// +// +QList 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 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 = QTextCharFormat(); + 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;;; + 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; + 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 << "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(); + + // Create the default text object; + QAnsiTextEditFormattedText ftext; + ftext.text = text; + ftext.format = currentCharFormat(); // Use cleared format. + + // Parse it. A list of sub text objects is created. + QList ftexts = _escapeCodeHandler.parseText(ftext); + + // Print each sub text object. Each one has its own text format. + for (const QAnsiTextEditFormattedText& ftext : ftexts) { + + //dumpCharFormat(ftext.text, ftext.format); + + QTextCursor cursor = textCursor(); + cursor.setCharFormat(ftext.format); + cursor.insertText(ftext.text); + setTextCursor(cursor); + } +} + +void QAnsiTextEdit::appendAnsiText (const QString& text) { + + // Create the default text object; + QAnsiTextEditFormattedText ftext; + ftext.text = text; + // XXX ftext.format = currentCharFormat(); + ftext.format = _escapeCodeHandler.formatScope(); + + // Parse it. A list of sub text objects is created. + QList ftexts = _escapeCodeHandler.parseText(ftext); + + // Print each sub text object. Each one has its own text format. + for (const QAnsiTextEditFormattedText& ftext : ftexts) { + QTextCursor cursor = textCursor(); + cursor.setCharFormat(ftext.format); + cursor.insertText(ftext.text); + setTextCursor(cursor); + } +} + +void QAnsiTextEdit::insertAnsiText (const QString& text) { + + // Create the default text object; + QAnsiTextEditFormattedText ftext; + ftext.text = text; + // XXX ftext.format = currentCharFormat(); + ftext.format = _escapeCodeHandler.formatScope(); + + // Parse it. A list of sub text objects is created. + QList ftexts = _escapeCodeHandler.parseText(ftext); + + // Print each sub text object. Each one has its own text format. + for (const QAnsiTextEditFormattedText& ftext : ftexts) { + QTextCursor cursor = textCursor(); + cursor.setCharFormat(ftext.format); + cursor.insertText(ftext.text); + setTextCursor(cursor); + } +} + diff --git a/src/QAnsiTextEdit.h b/src/QAnsiTextEdit.h new file mode 100644 index 0000000..58d48e6 --- /dev/null +++ b/src/QAnsiTextEdit.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class QAnsiTextEditFormattedText { + public: + QAnsiTextEditFormattedText() = default; + QAnsiTextEditFormattedText(const QString& txt, const QTextCharFormat& fmt = QTextCharFormat()); + + QString text; + QTextCharFormat format; +}; + +class QAnsiTextEditEscapeCodeHandler { + public: + QList parseText (const QAnsiTextEditFormattedText& input); + void endFormatScope (); + void setFormatScope (const QTextCharFormat& charFormat); + QTextCharFormat formatScope () const; + + private: + QColor ansiColor (uint code); + + bool _previousFormatClosed = true; + bool _waitingForTerminator = false; + QString _alternateTerminator; + QTextCharFormat _previousFormat; + QString _pendingText; +}; + +class QAnsiTextEdit : public QPlainTextEdit { + + Q_OBJECT + + public: + explicit QAnsiTextEdit(QWidget* parent = 0); + explicit QAnsiTextEdit(const QString& text, QWidget* parent = 0); + ~QAnsiTextEdit(); + + private: + void dumpCharFormat (QString string, QTextCharFormat format); + + public slots: + void setAnsiText (const QString& text); + void appendAnsiText (const QString& text); + void insertAnsiText (const QString& text); + + private: + QAnsiTextEditEscapeCodeHandler _escapeCodeHandler; + +}; + diff --git a/src/SeerConsoleWidget.cpp b/src/SeerConsoleWidget.cpp index eba10e8..804d88e 100644 --- a/src/SeerConsoleWidget.cpp +++ b/src/SeerConsoleWidget.cpp @@ -45,8 +45,6 @@ SeerConsoleWidget::SeerConsoleWidget (QWidget* parent) : QWidget(parent) { textEdit->setLineWrapMode(QPlainTextEdit::NoWrap); // No wrap wrapTextCheckBox->setCheckState(Qt::Unchecked); // No wrap - _cursor = QTextCursor(textEdit->document()); - // Create psuedo terminal for console. createConsole(); connectConsole(); @@ -76,44 +74,15 @@ const QString& SeerConsoleWidget::ttyDeviceName () const { void SeerConsoleWidget::handleText (const char* buffer, int count) { - // parse off lines - const char* start = buffer; - - while (count > 0) { - - int len = 0; - - while (count > 0 && start[len] != '\n' && start[len] != '\r') { - --count; - ++len; - } - - if (len > 0) { - QString str = QString::fromLatin1(start, len); - // replace text in the last line - // this selection is non-empty only after a '\r' that was not - // followed by a '\n' - _cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, len); - _cursor.insertText(str); - start += len; - len = 0; - } - - if (count > 0 && *start == '\r') { - ++start; - --count; - _cursor.movePosition(QTextCursor::StartOfLine); - } - - if (count > 0 && *start == '\n') { - ++start; - --count; - _cursor.movePosition(QTextCursor::End); - _cursor.insertText(QString('\n')); - } - - textEdit->verticalScrollBar()->setValue(textEdit->verticalScrollBar()->maximum()); + if (count < 1) { + return; } + + QString str = QString::fromLatin1(buffer, count); + + textEdit->insertAnsiText(str); + + return; } void SeerConsoleWidget::handleChangeWindowTitle (QString title) { @@ -127,7 +96,6 @@ void SeerConsoleWidget::handleChangeWindowTitle (QString title) { void SeerConsoleWidget::handleClearButton () { textEdit->clear(); - _cursor.movePosition(QTextCursor::End); } void SeerConsoleWidget::handlePrintButton () { diff --git a/src/SeerConsoleWidget.h b/src/SeerConsoleWidget.h index bda4d74..3a0c501 100644 --- a/src/SeerConsoleWidget.h +++ b/src/SeerConsoleWidget.h @@ -47,7 +47,6 @@ class SeerConsoleWidget : public QWidget, protected Ui::SeerConsoleWidgetForm { private: QString _mode; - QTextCursor _cursor; QString _ttyDeviceName; int _ptsFD; QSocketNotifier* _ptsListener; diff --git a/src/SeerConsoleWidget.ui b/src/SeerConsoleWidget.ui index cf797a0..94ab58b 100644 --- a/src/SeerConsoleWidget.ui +++ b/src/SeerConsoleWidget.ui @@ -62,7 +62,7 @@ - + QPlainTextEdit::NoWrap @@ -129,6 +129,11 @@ QLineEdit
QHistoryLineEdit.h
+ + QAnsiTextEdit + QPlainTextEdit +
QAnsiTextEdit.h
+
diff --git a/src/SeerCppSourceHighlighter.cpp b/src/SeerCppSourceHighlighter.cpp index 87b4d5c..2dfbe8e 100644 --- a/src/SeerCppSourceHighlighter.cpp +++ b/src/SeerCppSourceHighlighter.cpp @@ -110,7 +110,7 @@ void SeerCppSourceHighlighter::setHighlighterSettings (const SeerHighlighterSett void SeerCppSourceHighlighter::highlightBlock (const QString& text) { - for (const HighlightingRule& rule : qAsConst(_highlightingRules)) { + for (const HighlightingRule& rule : std::as_const(_highlightingRules)) { QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text); diff --git a/tests/helloansicodes/.gitignore b/tests/helloansicodes/.gitignore new file mode 100644 index 0000000..8b889b5 --- /dev/null +++ b/tests/helloansicodes/.gitignore @@ -0,0 +1 @@ +helloansicodes diff --git a/tests/helloansicodes/ANSI-color-codes.h b/tests/helloansicodes/ANSI-color-codes.h new file mode 100644 index 0000000..6bc656b --- /dev/null +++ b/tests/helloansicodes/ANSI-color-codes.h @@ -0,0 +1,80 @@ +/* + * This is free and unencumbered software released into the public domain. + * + * For more information, please refer to + */ + +//Regular text +#define BLK "\033[0;30m" +#define RED "\033[0;31m" +#define GRN "\033[0;32m" +#define YEL "\033[0;33m" +#define BLU "\033[0;34m" +#define MAG "\033[0;35m" +#define CYN "\033[0;36m" +#define WHT "\033[0;37m" + +//Regular bold text +#define BBLK "\033[1;30m" +#define BRED "\033[1;31m" +#define BGRN "\033[1;32m" +#define BYEL "\033[1;33m" +#define BBLU "\033[1;34m" +#define BMAG "\033[1;35m" +#define BCYN "\033[1;36m" +#define BWHT "\033[1;37m" + +//Regular underline text +#define UBLK "\033[4;30m" +#define URED "\033[4;31m" +#define UGRN "\033[4;32m" +#define UYEL "\033[4;33m" +#define UBLU "\033[4;34m" +#define UMAG "\033[4;35m" +#define UCYN "\033[4;36m" +#define UWHT "\033[4;37m" + +//Regular background +#define BLKB "\033[40m" +#define REDB "\033[41m" +#define GRNB "\033[42m" +#define YELB "\033[43m" +#define BLUB "\033[44m" +#define MAGB "\033[45m" +#define CYNB "\033[46m" +#define WHTB "\033[47m" + +//High intensty background +#define BLKHB "\033[0;100m" +#define REDHB "\033[0;101m" +#define GRNHB "\033[0;102m" +#define YELHB "\033[0;103m" +#define BLUHB "\033[0;104m" +#define MAGHB "\033[0;105m" +#define CYNHB "\033[0;106m" +#define WHTHB "\033[0;107m" + +//High intensty text +#define HBLK "\033[0;90m" +#define HRED "\033[0;91m" +#define HGRN "\033[0;92m" +#define HYEL "\033[0;93m" +#define HBLU "\033[0;94m" +#define HMAG "\033[0;95m" +#define HCYN "\033[0;96m" +#define HWHT "\033[0;97m" + +//Bold high intensity text +#define BHBLK "\033[1;90m" +#define BHRED "\033[1;91m" +#define BHGRN "\033[1;92m" +#define BHYEL "\033[1;93m" +#define BHBLU "\033[1;94m" +#define BHMAG "\033[1;95m" +#define BHCYN "\033[1;96m" +#define BHWHT "\033[1;97m" + +//Reset +//#define reset "\033[0m" +#define CRESET "\033[0m" +#define COLOR_RESET "\033[0m" diff --git a/tests/helloansicodes/Makefile b/tests/helloansicodes/Makefile new file mode 100644 index 0000000..4fb4db1 --- /dev/null +++ b/tests/helloansicodes/Makefile @@ -0,0 +1,10 @@ +.PHONY: all +all: helloansicodes + +helloansicodes: helloansicodes.c ANSI-color-codes.h + g++ -g -o helloansicodes helloansicodes.c + +.PHONY: clean +clean: + rm -f helloansicodes helloansicodes.o + diff --git a/tests/helloansicodes/helloansicodes.c b/tests/helloansicodes/helloansicodes.c new file mode 100644 index 0000000..32586ea --- /dev/null +++ b/tests/helloansicodes/helloansicodes.c @@ -0,0 +1,26 @@ +#include "ANSI-color-codes.h" +#include + +int main() { + + printf("STARTING ANSI CODE TESTS.\n\n"); + + printf(BRED "Hey this is the color red, and it's bold! \n" CRESET); + printf(RED "If " BLU "you " YEL "are " GRN "bored " CYN "do " MAG "this! \n" CRESET); + printf(BRED "If " BBLU "you " BYEL "are " BGRN "bored " BCYN "do " BMAG "this! \n" CRESET); + printf(URED "If " UBLU "you " UYEL "are " UGRN "bored " UCYN "do " UMAG "this! \n" CRESET); + + printf("\n"); + printf(" \033[4;31mLight Colors\033[0m \t\t\t \033[1;4;31mDark Colors\033[0m\n"); + printf(" \033[0;30;47m Black \033[0m 0;30m \t\t \033[1;30;40m Dark Gray \033[0m 1;30m\n"); + printf(" \033[0;31;47m Red \033[0m 0;31m \t\t \033[1;31;40m Dark Red \033[0m 1;31m\n"); + printf(" \033[0;32;47m Green \033[0m 0;32m \t\t \033[1;32;40m Dark Green \033[0m 1;32m\n"); + printf(" \033[0;33;47m Brown \033[0m 0;33m \t\t \033[1;33;40m Yellow \033[0m 1;33m\n"); + printf(" \033[0;34;47m Blue \033[0m 0;34m \t\t \033[1;34;40m Dark Blue \033[0m 1;34m\n"); + printf(" \033[0;35;47m Magenta \033[0m 0;35m \t\t \033[1;35;40m Dark Magenta\033[0m 1;35m\n"); + printf(" \033[0;36;47m Cyan \033[0m 0;36m \t\t \033[1;36;40m Dark Cyan \033[0m 1;36m\n"); + printf(" \033[0;37;47m Light Gray\033[0m 0;37m \t\t \033[1;37;40m White \033[0m 1;37m\n"); + + return 0; +} +