UI: colored ANSI logs

This commit is contained in:
zeph 2026-01-09 02:02:51 +01:00 committed by Megamouse
parent 9faf9a6145
commit b2768bbd61
5 changed files with 128 additions and 2 deletions

View file

@ -3,6 +3,7 @@
#include "gui_settings.h"
#include "hex_validator.h"
#include "memory_viewer_panel.h"
#include "syntax_highlighter.h"
#include "Utilities/lockless.h"
#include "util/asm.hpp"
@ -131,6 +132,8 @@ log_frame::log_frame(std::shared_ptr<gui_settings> _gui_settings, QWidget* paren
m_tty->setContextMenuPolicy(Qt::CustomContextMenu);
m_tty->document()->setMaximumBlockCount(max_block_count_tty);
m_tty->installEventFilter(this);
m_tty_ansi_highlighter = new AnsiHighlighter(m_tty->document());
m_tty_input = new QLineEdit();
if (m_tty_channel >= 0)
@ -599,8 +602,15 @@ void log_frame::UpdateUI()
buf_line.assign(std::string_view(m_tty_buf).substr(str_index, m_tty_buf.find_first_of('\n', str_index) - str_index));
str_index += buf_line.size() + 1;
// Ignore control characters and greater/equal to 0x80
buf_line.erase(std::remove_if(buf_line.begin(), buf_line.end(), [](s8 c) { return c <= 0x8 || c == 0x7F || (c >= 0xE && c <= 0x1F); }), buf_line.end());
// Ignore control characters and greater/equal to 0x80, but preserve ESC (0x1B) if ANSI mode is enabled
buf_line.erase(std::remove_if(buf_line.begin(), buf_line.end(), [this](s8 c) {
if (m_ansi_tty)
{
// Keep ESC (0x1B) so ANSI sequences remain intact
return c <= 0x8 || c == 0x7F || (c >= 0xE && c <= 0x1F && c != 0x1B);
}
return c <= 0x8 || c == 0x7F || (c >= 0xE && c <= 0x1F);
}), buf_line.end());
// save old scroll bar state
QScrollBar* sb = m_tty->verticalScrollBar();

View file

@ -5,6 +5,7 @@
#include "custom_dock_widget.h"
#include "find_dialog.h"
#include "syntax_highlighter.h"
#include <memory>
@ -69,6 +70,7 @@ private:
QPlainTextEdit* m_tty = nullptr;
QLineEdit* m_tty_input = nullptr;
int m_tty_channel = -1;
AnsiHighlighter* m_tty_ansi_highlighter = nullptr;
QAction* m_clear_act = nullptr;
QAction* m_clear_tty_act = nullptr;

View file

@ -9,6 +9,7 @@
#include <memory>
class LogHighlighter;
class AnsiHighlighter;
class gui_settings;
class log_viewer : public QWidget
@ -33,6 +34,7 @@ private:
QString m_full_log;
QPlainTextEdit* m_log_text;
LogHighlighter* m_log_highlighter;
AnsiHighlighter* m_ansi_highlighter;
std::unique_ptr<find_dialog> m_find_dialog;
std::bitset<32> m_log_levels = std::bitset<32>(0b11111111u);
bool m_show_timestamps = true;

View file

@ -183,3 +183,104 @@ GlslHighlighter::GlslHighlighter(QTextDocument* parent) : Highlighter(parent)
commentStartExpression = QRegularExpression("/\\*");
commentEndExpression = QRegularExpression("\\*/");
}
AnsiHighlighter::AnsiHighlighter(QTextDocument* parent) : Highlighter(parent)
{
}
void AnsiHighlighter::highlightBlock(const QString &text)
{
// Match ANSI SGR sequences, e.g. "\x1b[31m" or "\x1b[1;32m"
const QRegularExpression ansi_re("\x1b\\[[0-9;]*m");
const QRegularExpression param_re("\x1b\\[([0-9;]*)m");
QTextCharFormat escapeFormat;
escapeFormat.setForeground(Qt::darkGray);
escapeFormat.setFontItalic(true);
QTextCharFormat currentFormat;
currentFormat.setForeground(gui::utils::get_foreground_color());
int pos = 0;
auto it = ansi_re.globalMatch(text);
while (it.hasNext())
{
auto match = it.next();
int start = match.capturedStart();
int length = match.capturedLength();
// Apply current format to the chunk before this escape sequence
if (start > pos)
{
setFormat(pos, start - pos, currentFormat);
}
// Highlight the escape sequence itself
setFormat(start, length, escapeFormat);
// Parse SGR parameters and update currentFormat
QRegularExpressionMatch pm = param_re.match(match.captured());
if (pm.hasMatch())
{
QString params = pm.captured(1);
if (params.isEmpty())
{
// empty or just \x1b[m = reset
currentFormat = QTextCharFormat();
currentFormat.setForeground(gui::utils::get_foreground_color());
}
else
{
const QStringList codes = params.split(';', Qt::SkipEmptyParts);
for (const QString& c : codes)
{
bool ok = false;
const int code = c.toInt(&ok);
if (!ok) continue;
switch (code)
{
case 0:
currentFormat = QTextCharFormat();
currentFormat.setForeground(gui::utils::get_foreground_color());
break;
case 1:
currentFormat.setFontWeight(QFont::Bold);
break;
case 3:
currentFormat.setFontItalic(true);
break;
case 4:
currentFormat.setFontUnderline(true);
break;
case 30: currentFormat.setForeground(Qt::black); break;
case 31: currentFormat.setForeground(Qt::red); break;
case 32: currentFormat.setForeground(Qt::darkGreen); break;
case 33: currentFormat.setForeground(Qt::darkYellow); break;
case 34: currentFormat.setForeground(Qt::darkBlue); break;
case 35: currentFormat.setForeground(Qt::darkMagenta); break;
case 36: currentFormat.setForeground(Qt::darkCyan); break;
case 37: currentFormat.setForeground(Qt::lightGray); break;
case 39: currentFormat.setForeground(gui::utils::get_foreground_color()); break;
case 90: currentFormat.setForeground(Qt::darkGray); break;
case 91: currentFormat.setForeground(Qt::red); break;
case 92: currentFormat.setForeground(Qt::green); break;
case 93: currentFormat.setForeground(Qt::yellow); break;
case 94: currentFormat.setForeground(Qt::blue); break;
case 95: currentFormat.setForeground(Qt::magenta); break;
case 96: currentFormat.setForeground(Qt::cyan); break;
case 97: currentFormat.setForeground(Qt::white); break;
// Background and extended colors not yet handled
default:
break;
}
}
}
}
pos = start + length;
}
// Apply remaining format
if (pos < text.length())
setFormat(pos, text.length() - pos, currentFormat);
}

View file

@ -52,3 +52,14 @@ class GlslHighlighter : public Highlighter
public:
explicit GlslHighlighter(QTextDocument* parent = nullptr);
};
class AnsiHighlighter : public Highlighter
{
Q_OBJECT
public:
explicit AnsiHighlighter(QTextDocument* parent = nullptr);
protected:
void highlightBlock(const QString &text) override;
};