Initializes ANSI escape code supports (#131)

This commit is contained in:
Putta Khunchalee 2023-02-06 23:36:05 +07:00 committed by GitHub
parent b612ca53ec
commit 7abe3b4187
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 573 additions and 19 deletions

View file

@ -140,8 +140,7 @@ If you don't have a PS4 application for testing you can download PS Scene Quiz f
### Rules for Rust sources
- Don't be afraid to use `unsafe` when it is necessary. We are writing an application that requires very high-performance code and we use Rust to assist us on this task, not to use Rust to compromise performance that C/C++ can provide.
- Any functions that operate on pointers don't need to mark as `unsafe`. The reason is that it will require the caller to wrap it in `unsafe`. We already embrace `unsafe` code so no point to make it harder to use.
- Use unsafe code only when you know what you are doing. When you do try to wrap it in a safe function so other people who are not familiar with unsafe code can have a safe life.
- Don't chain method calls without an intermediate variable if the result code is hard to follow. We encourage code readability as a pleasure when writing so try to make it easy to read and understand for other people.
- Do not blindly cast an integer. Make sure the value can fit in a destination type. We don't have any plans to support non-64-bit systems so the pointer size and its related types like `usize` are always 64-bits.
@ -166,6 +165,7 @@ We use icons from https://materialdesignicons.com for action icons (e.g. on the
## License
- All source code except `src/pfs` and `src/pkg` is licensed under MIT license.
- `src/ansi_escape.hpp`, `src/ansi_escape.cpp`, `src/log_formatter.hpp` and `src/log_formatter.cpp` are licensed under GPL-3.0 only.
- `src/pfs` and `src/pkg` are licensed under LGPL-3.0 license.
- All other source code is licensed under MIT license.
- All release binaries are under GPL-3.0 license.

View file

@ -28,11 +28,13 @@ ExternalProject_Add(core
# Setup application target.
add_executable(obliteration WIN32
ansi_escape.cpp
app_data.cpp
game_models.cpp
game_settings.cpp
game_settings_dialog.cpp
initialize_dialog.cpp
log_formatter.cpp
main.cpp
main_window.cpp
path.cpp

257
src/ansi_escape.cpp Normal file
View file

@ -0,0 +1,257 @@
// This file is a derived works from Qt Creator.
// Licensed under GPL-3.0-only.
#include "ansi_escape.hpp"
static QColor ansiColor(uint code)
{
// QTC_ASSERT(code < 8, return QColor());
if (!(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);
}
QList<FormattedText> AnsiEscape::parseText(const FormattedText &input)
{
enum AnsiEscapeCodes {
ResetFormat = 0,
BoldText = 1,
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<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();
}
while (!strippedText.isEmpty()) {
// QTC_ASSERT(m_pendingText.isEmpty(), break);
if (!m_pendingText.isEmpty()) {
break;
}
if (m_waitingForTerminator) {
// We ignore all escape codes taking string arguments.
QString terminator = "\x1b\\";
int terminatorPos = strippedText.indexOf(terminator);
if (terminatorPos == -1 && !m_alternateTerminator.isEmpty()) {
terminator = m_alternateTerminator;
terminatorPos = strippedText.indexOf(terminator);
}
if (terminatorPos == -1) {
m_pendingText = strippedText;
break;
}
m_waitingForTerminator = false;
m_alternateTerminator.clear();
strippedText.remove(0, terminatorPos + terminator.length());
if (strippedText.isEmpty())
break;
}
const int escapePos = strippedText.indexOf(escape.at(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.at(0) == escape.at(0), break);
if (!(strippedText.at(0) == escape.at(0))) {
break;
}
while (!strippedText.isEmpty() && escape.at(0) == strippedText.at(0)) {
if (escape.startsWith(strippedText)) {
// control secquence is not complete
m_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 ']':
m_alternateTerminator = QChar(7);
Q_FALLTHROUGH();
case 'P': case 'X': case '^': case '_':
strippedText.remove(0, 2);
m_waitingForTerminator = true;
break;
default:
// not a control sequence
m_pendingText.clear();
outputData << FormattedText(strippedText.left(1), charFormat);
strippedText.remove(0, 1);
continue;
}
break;
}
m_pendingText += strippedText.mid(0, escape.length());
strippedText.remove(0, escape.length());
// \e[K is not supported. Just strip it.
if (strippedText.startsWith(eraseToEol)) {
m_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();
}
m_pendingText += strippedText.mid(0, 1);
strippedText.remove(0, 1);
}
if (strippedText.isEmpty())
break;
// remove terminating char
if (!strippedText.startsWith(colorTerminator)) {
m_pendingText.clear();
strippedText.remove(0, 1);
break;
}
// got consistent control sequence, ok to clear pending text
m_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) {
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:
charFormat = input.format;
endFormatScope();
break;
case BoldText:
charFormat.setFontWeight(QFont::Bold);
setFormatScope(charFormat);
break;
case DefaultTextColor:
charFormat.setForeground(input.format.foreground());
setFormatScope(charFormat);
break;
case DefaultBackgroundColor:
charFormat.setBackground(input.format.background());
setFormatScope(charFormat);
break;
case RgbTextColor:
case RgbBackgroundColor:
// 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:
break;
}
}
}
}
}
return outputData;
}
void AnsiEscape::endFormatScope()
{
m_previousFormatClosed = true;
}
void AnsiEscape::setFormatScope(const QTextCharFormat &charFormat)
{
m_previousFormat = charFormat;
m_previousFormatClosed = false;
}

36
src/ansi_escape.hpp Normal file
View file

@ -0,0 +1,36 @@
// This file is a derived works from Qt Creator.
// Licensed under GPL-3.0-only.
#pragma once
#include <QList>
#include <QString>
#include <QTextCharFormat>
class FormattedText {
public:
FormattedText() = default;
FormattedText(const QString &txt, const QTextCharFormat &fmt = QTextCharFormat()) :
text(txt),
format(fmt)
{
}
QString text;
QTextCharFormat format;
};
class AnsiEscape {
public:
QList<FormattedText> parseText(const FormattedText &input);
void endFormatScope();
private:
void setFormatScope(const QTextCharFormat &charFormat);
private:
bool m_previousFormatClosed = true;
bool m_waitingForTerminator = false;
QString m_alternateTerminator;
QTextCharFormat m_previousFormat;
QString m_pendingText;
};

203
src/log_formatter.cpp Normal file
View file

@ -0,0 +1,203 @@
// This file is a derived works from Qt Creator.
// Licensed under GPL-3.0-only.
#include "log_formatter.hpp"
#include <QBrush>
#include <QPlainTextEdit>
#include <QScrollBar>
static QString normalizeNewlines(const QString &text)
{
QString res = text;
const auto newEnd = std::unique(res.begin(), res.end(), [](const QChar c1, const QChar c2) {
return c1 == '\r' && c2 == '\r'; // QTCREATORBUG-24556
});
res.chop(std::distance(newEnd, res.end()));
res.replace("\r\n", "\n");
return res;
}
LogFormatter::LogFormatter(QPlainTextEdit *output, QObject *parent) :
QObject(parent),
m_output(output),
m_cursor(output->textCursor()),
m_prependLineFeed(false),
m_prependCarriageReturn(false)
{
m_cursor.movePosition(QTextCursor::End);
initFormats();
}
LogFormatter::~LogFormatter()
{
}
void LogFormatter::appendMessage(const QString &text, LogFormat format)
{
if (text.isEmpty()) {
return;
}
// If we have an existing incomplete line and its format is different from this one,
// then we consider the two messages unrelated. We re-insert the previous incomplete line,
// possibly formatted now, and start from scratch with the new input.
if (!m_incompleteLine.first.isEmpty() && m_incompleteLine.second != format) {
flushIncompleteLine();
}
QString out = text;
if (m_prependCarriageReturn) {
m_prependCarriageReturn = false;
out.prepend('\r');
}
out = normalizeNewlines(out);
if (out.endsWith('\r')) {
m_prependCarriageReturn = true;
out.chop(1);
}
// If the input is a single incomplete line, we do not forward it to the specialized
// formatting code, but simply dump it as-is. Once it becomes complete or it needs to
// be flushed for other reasons, we remove the unformatted part and re-insert it, this
// time with proper formatting.
if (!out.contains('\n')) {
dumpIncompleteLine(out, format);
return;
}
// We have at least one complete line, so let's remove the previously dumped
// incomplete line and prepend it to the first line of our new input.
if (!m_incompleteLine.first.isEmpty()) {
clearLastLine();
out.prepend(m_incompleteLine.first);
m_incompleteLine.first.clear();
}
// Forward all complete lines to the specialized formatting code, and handle a
// potential trailing incomplete line the same way as above.
for (int startPos = 0; ;) {
const int eolPos = out.indexOf('\n', startPos);
if (eolPos == -1) {
dumpIncompleteLine(out.mid(startPos), format);
break;
}
doAppendMessage(out.mid(startPos, eolPos - startPos), format);
scroll();
m_prependLineFeed = true;
startPos = eolPos + 1;
}
}
void LogFormatter::reset()
{
m_output->clear();
m_prependLineFeed = false;
m_prependCarriageReturn = false;
m_incompleteLine.first.clear();
m_escapeCodeHandler = AnsiEscape();
}
void LogFormatter::initFormats()
{
m_formats[InfoMessageFormat].setForeground(QBrush(Qt::darkGreen));
m_formats[ErrorMessageFormat].setForeground(QBrush(Qt::darkRed));
m_formats[ErrorMessageFormat].setFontWeight(QFont::Bold);
m_formats[WarnMessageFormat].setForeground(QBrush(Qt::darkYellow));
m_formats[WarnMessageFormat].setFontWeight(QFont::Bold);
}
void LogFormatter::doAppendMessage(const QString &text, LogFormat format)
{
QTextCharFormat charFmt = charFormat(format);
QList<FormattedText> formattedText = parseAnsi(text, charFmt);
const QString cleanLine = std::accumulate(formattedText.begin(), formattedText.end(), QString(),
[](const FormattedText &t1, const FormattedText &t2) -> QString
{ return t1.text + t2.text; });
for (FormattedText output : formattedText) {
append(output.text, output.format);
}
if (formattedText.isEmpty()) {
append({}, charFmt); // This might cause insertion of a newline character.
}
}
void LogFormatter::append(const QString &text, const QTextCharFormat &format)
{
flushTrailingNewline();
int startPos = 0;
int crPos = -1;
while ((crPos = text.indexOf('\r', startPos)) >= 0) {
m_cursor.insertText(text.mid(startPos, crPos - startPos), format);
m_cursor.clearSelection();
m_cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
startPos = crPos + 1;
}
if (startPos < text.size()) {
m_cursor.insertText(text.mid(startPos), format);
}
}
void LogFormatter::flushTrailingNewline()
{
if (m_prependLineFeed) {
m_cursor.insertText("\n");
m_prependLineFeed = false;
}
}
QTextCharFormat LogFormatter::charFormat(LogFormat format) const
{
return m_formats[format];
}
QList<FormattedText> LogFormatter::parseAnsi(const QString &text, const QTextCharFormat &format)
{
return m_escapeCodeHandler.parseText(FormattedText(text, format));
}
void LogFormatter::dumpIncompleteLine(const QString &line, LogFormat format)
{
if (line.isEmpty())
return;
append(line, charFormat(format));
m_incompleteLine.first.append(line);
m_incompleteLine.second = format;
}
void LogFormatter::flushIncompleteLine()
{
clearLastLine();
doAppendMessage(m_incompleteLine.first, m_incompleteLine.second);
m_incompleteLine.first.clear();
}
void LogFormatter::clearLastLine()
{
// Note that this approach will fail if the text edit is not read-only and users
// have messed with the last line between programmatic inputs.
// We live with this risk, as all the alternatives are worse.
if (!m_cursor.atEnd()) {
m_cursor.movePosition(QTextCursor::End);
}
m_cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
m_cursor.removeSelectedText();
}
void LogFormatter::scroll()
{
auto bar = m_output->verticalScrollBar();
auto max = bar->maximum();
auto bottom = (bar->value() >= (max - 4)); // 4 is an error threshold.
if (bottom) {
bar->setValue(max);
}
}

51
src/log_formatter.hpp Normal file
View file

@ -0,0 +1,51 @@
// This file is a derived works from Qt Creator.
// Licensed under GPL-3.0-only.
#pragma once
#include "ansi_escape.hpp"
#include <QList>
#include <QObject>
#include <QTextCharFormat>
#include <QTextCursor>
class QPlainTextEdit;
enum LogFormat {
InfoMessageFormat,
ErrorMessageFormat,
WarnMessageFormat,
NumberOfFormats // Keep this entry last.
};
class LogFormatter : public QObject {
Q_OBJECT
public:
LogFormatter(QPlainTextEdit *output, QObject *parent = nullptr);
~LogFormatter() override;
public:
void appendMessage(const QString &text, LogFormat format);
void reset();
private:
void initFormats();
void doAppendMessage(const QString &text, LogFormat format);
void append(const QString &text, const QTextCharFormat &format);
void flushTrailingNewline();
QTextCharFormat charFormat(LogFormat format) const;
QList<FormattedText> parseAnsi(const QString &text, const QTextCharFormat &format);
void dumpIncompleteLine(const QString &line, LogFormat format);
void flushIncompleteLine();
void clearLastLine();
void scroll();
private:
AnsiEscape m_escapeCodeHandler;
QPlainTextEdit *m_output;
QTextCursor m_cursor;
QTextCharFormat m_formats[NumberOfFormats];
QPair<QString, LogFormat> m_incompleteLine;
bool m_prependLineFeed;
bool m_prependCarriageReturn;
};

View file

@ -2,6 +2,7 @@
#include "app_data.hpp"
#include "game_models.hpp"
#include "game_settings_dialog.hpp"
#include "log_formatter.hpp"
#include "pkg.hpp"
#include "progress_dialog.hpp"
#include "settings.hpp"
@ -92,18 +93,21 @@ MainWindow::MainWindow() :
m_tab->addTab(m_games, QIcon(":/resources/view-comfy.svg"), "Games");
// Setup log view.
m_log = new QPlainTextEdit();
m_log->setReadOnly(true);
m_log->setLineWrapMode(QPlainTextEdit::NoWrap);
m_log->setMaximumBlockCount(10000);
auto log = new QPlainTextEdit();
log->setReadOnly(true);
log->setLineWrapMode(QPlainTextEdit::NoWrap);
log->setMaximumBlockCount(10000);
#ifdef _WIN32
m_log->document()->setDefaultFont(QFont("Courier New", 10));
log->document()->setDefaultFont(QFont("Courier New", 10));
#else
m_log->document()->setDefaultFont(QFont("monospace", 10));
log->document()->setDefaultFont(QFont("monospace", 10));
#endif
m_tab->addTab(m_log, QIcon(":/resources/card-text-outline.svg"), "Log");
m_log = new LogFormatter(log, this);
m_tab->addTab(log, QIcon(":/resources/card-text-outline.svg"), "Log");
// Setup status bar.
statusBar();
@ -333,7 +337,7 @@ void MainWindow::startGame(const QModelIndex &index)
auto game = model->get(index.row()); // Qt already guaranteed the index is valid.
// Clear previous log and switch to log view.
m_log->clear();
m_log->reset();
m_tab->setCurrentIndex(1);
// Get full path to kernel binary.
@ -423,18 +427,19 @@ void MainWindow::kernelOutput()
// It is possible for Qt to signal this slot after QProcess::errorOccurred or QProcess::finished
// so we need to check if the those signals has been received.
while (m_kernel && m_kernel->canReadLine()) {
auto line = m_kernel->readLine();
auto line = QString::fromUtf8(m_kernel->readLine());
if (line.endsWith('\n')) {
line.chop(1);
}
m_log->appendPlainText(QString::fromUtf8(line));
m_log->appendMessage(line, InfoMessageFormat);
}
}
void MainWindow::kernelTerminated(int, QProcess::ExitStatus)
{
// Do nothing if we got QProcess::errorOccurred before this signal.
if (!m_kernel) {
return;
}
kernelOutput();
QMessageBox::critical(this, "Error", "The emulator kernel has been stopped unexpectedly. Please take a look on the log and report the issue.");

View file

@ -3,8 +3,8 @@
#include <QMainWindow>
#include <QProcess>
class LogFormatter;
class QListView;
class QPlainTextEdit;
class MainWindow final : public QMainWindow {
public:
@ -36,6 +36,6 @@ private:
private:
QTabWidget *m_tab;
QListView *m_games;
QPlainTextEdit *m_log;
LogFormatter *m_log;
QProcess *m_kernel;
};