Add ANSI color rendering and terminal themes
This commit is contained in:
@@ -2,12 +2,119 @@
|
||||
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QColor>
|
||||
#include <QKeyEvent>
|
||||
#include <QTextCursor>
|
||||
|
||||
TerminalView::TerminalView(QWidget* parent) : QPlainTextEdit(parent)
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
QString normalizedThemeName(const QString& value)
|
||||
{
|
||||
return value.trimmed().toLower();
|
||||
}
|
||||
}
|
||||
|
||||
TerminalView::TerminalView(QWidget* parent)
|
||||
: QTextEdit(parent),
|
||||
m_bold(false),
|
||||
m_hasFgColor(false),
|
||||
m_hasBgColor(false)
|
||||
{
|
||||
setReadOnly(true);
|
||||
setUndoRedoEnabled(false);
|
||||
document()->setMaximumBlockCount(4000);
|
||||
setAcceptRichText(false);
|
||||
|
||||
applyThemePalette(paletteByName(QStringLiteral("Dark")));
|
||||
resetSgrState();
|
||||
}
|
||||
|
||||
QStringList TerminalView::themeNames()
|
||||
{
|
||||
return {QStringLiteral("Dark"), QStringLiteral("Light"), QStringLiteral("Solarized Dark")};
|
||||
}
|
||||
|
||||
void TerminalView::setThemeName(const QString& themeName)
|
||||
{
|
||||
applyThemePalette(paletteByName(themeName));
|
||||
}
|
||||
|
||||
void TerminalView::appendTerminalData(const QString& data)
|
||||
{
|
||||
if (data.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString merged = m_pendingEscape + data;
|
||||
m_pendingEscape.clear();
|
||||
|
||||
QString plainBuffer;
|
||||
|
||||
for (int i = 0; i < merged.size();) {
|
||||
const QChar ch = merged.at(i);
|
||||
|
||||
if (ch == QChar::fromLatin1('\x1b')) {
|
||||
if (!plainBuffer.isEmpty()) {
|
||||
appendTextChunk(plainBuffer);
|
||||
plainBuffer.clear();
|
||||
}
|
||||
|
||||
if (i + 1 >= merged.size()) {
|
||||
m_pendingEscape = merged.mid(i);
|
||||
break;
|
||||
}
|
||||
|
||||
if (merged.at(i + 1) != QChar::fromLatin1('[')) {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
int end = i + 2;
|
||||
while (end < merged.size()) {
|
||||
const ushort c = merged.at(end).unicode();
|
||||
if (c >= 0x40 && c <= 0x7e) {
|
||||
break;
|
||||
}
|
||||
++end;
|
||||
}
|
||||
|
||||
if (end >= merged.size()) {
|
||||
m_pendingEscape = merged.mid(i);
|
||||
break;
|
||||
}
|
||||
|
||||
const QChar finalByte = merged.at(end);
|
||||
const QString params = merged.mid(i + 2, end - (i + 2));
|
||||
|
||||
if (finalByte == QChar::fromLatin1('m')) {
|
||||
handleSgrSequence(params);
|
||||
} else if (finalByte == QChar::fromLatin1('J')) {
|
||||
if (params.isEmpty() || params == QStringLiteral("2")) {
|
||||
clear();
|
||||
}
|
||||
}
|
||||
|
||||
i = end + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == QChar::fromLatin1('\r')) {
|
||||
const bool hasLfAfter = (i + 1 < merged.size() && merged.at(i + 1) == QChar::fromLatin1('\n'));
|
||||
if (!hasLfAfter) {
|
||||
plainBuffer.append(QChar::fromLatin1('\n'));
|
||||
}
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
plainBuffer.append(ch);
|
||||
++i;
|
||||
}
|
||||
|
||||
if (!plainBuffer.isEmpty()) {
|
||||
appendTextChunk(plainBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
void TerminalView::keyPressEvent(QKeyEvent* event)
|
||||
@@ -74,5 +181,288 @@ void TerminalView::keyPressEvent(QKeyEvent* event)
|
||||
return;
|
||||
}
|
||||
|
||||
QPlainTextEdit::keyPressEvent(event);
|
||||
QTextEdit::keyPressEvent(event);
|
||||
}
|
||||
|
||||
TerminalView::ThemePalette TerminalView::paletteByName(const QString& themeName)
|
||||
{
|
||||
const QString theme = normalizedThemeName(themeName);
|
||||
|
||||
if (theme == QStringLiteral("light")) {
|
||||
return ThemePalette{QStringLiteral("Light"),
|
||||
QColor(QStringLiteral("#fafafa")),
|
||||
QColor(QStringLiteral("#202124")),
|
||||
{QColor(QStringLiteral("#000000")),
|
||||
QColor(QStringLiteral("#a31515")),
|
||||
QColor(QStringLiteral("#008000")),
|
||||
QColor(QStringLiteral("#795e26")),
|
||||
QColor(QStringLiteral("#0000ff")),
|
||||
QColor(QStringLiteral("#af00db")),
|
||||
QColor(QStringLiteral("#0451a5")),
|
||||
QColor(QStringLiteral("#666666"))},
|
||||
{QColor(QStringLiteral("#7f7f7f")),
|
||||
QColor(QStringLiteral("#cd3131")),
|
||||
QColor(QStringLiteral("#14a10e")),
|
||||
QColor(QStringLiteral("#b5ba00")),
|
||||
QColor(QStringLiteral("#0451a5")),
|
||||
QColor(QStringLiteral("#bc05bc")),
|
||||
QColor(QStringLiteral("#0598bc")),
|
||||
QColor(QStringLiteral("#111111"))}};
|
||||
}
|
||||
|
||||
if (theme == QStringLiteral("solarized dark")) {
|
||||
return ThemePalette{QStringLiteral("Solarized Dark"),
|
||||
QColor(QStringLiteral("#002b36")),
|
||||
QColor(QStringLiteral("#839496")),
|
||||
{QColor(QStringLiteral("#073642")),
|
||||
QColor(QStringLiteral("#dc322f")),
|
||||
QColor(QStringLiteral("#859900")),
|
||||
QColor(QStringLiteral("#b58900")),
|
||||
QColor(QStringLiteral("#268bd2")),
|
||||
QColor(QStringLiteral("#d33682")),
|
||||
QColor(QStringLiteral("#2aa198")),
|
||||
QColor(QStringLiteral("#eee8d5"))},
|
||||
{QColor(QStringLiteral("#586e75")),
|
||||
QColor(QStringLiteral("#cb4b16")),
|
||||
QColor(QStringLiteral("#586e75")),
|
||||
QColor(QStringLiteral("#657b83")),
|
||||
QColor(QStringLiteral("#839496")),
|
||||
QColor(QStringLiteral("#6c71c4")),
|
||||
QColor(QStringLiteral("#93a1a1")),
|
||||
QColor(QStringLiteral("#fdf6e3"))}};
|
||||
}
|
||||
|
||||
return ThemePalette{QStringLiteral("Dark"),
|
||||
QColor(QStringLiteral("#1e1e1e")),
|
||||
QColor(QStringLiteral("#d4d4d4")),
|
||||
{QColor(QStringLiteral("#000000")),
|
||||
QColor(QStringLiteral("#cd3131")),
|
||||
QColor(QStringLiteral("#0dbc79")),
|
||||
QColor(QStringLiteral("#e5e510")),
|
||||
QColor(QStringLiteral("#2472c8")),
|
||||
QColor(QStringLiteral("#bc3fbc")),
|
||||
QColor(QStringLiteral("#11a8cd")),
|
||||
QColor(QStringLiteral("#e5e5e5"))},
|
||||
{QColor(QStringLiteral("#666666")),
|
||||
QColor(QStringLiteral("#f14c4c")),
|
||||
QColor(QStringLiteral("#23d18b")),
|
||||
QColor(QStringLiteral("#f5f543")),
|
||||
QColor(QStringLiteral("#3b8eea")),
|
||||
QColor(QStringLiteral("#d670d6")),
|
||||
QColor(QStringLiteral("#29b8db")),
|
||||
QColor(QStringLiteral("#ffffff"))}};
|
||||
}
|
||||
|
||||
QColor TerminalView::colorFrom256Index(int index)
|
||||
{
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
if (index > 255) {
|
||||
index = 255;
|
||||
}
|
||||
|
||||
if (index < 16) {
|
||||
static const std::array<QColor, 16> base = {
|
||||
QColor(QStringLiteral("#000000")), QColor(QStringLiteral("#800000")),
|
||||
QColor(QStringLiteral("#008000")), QColor(QStringLiteral("#808000")),
|
||||
QColor(QStringLiteral("#000080")), QColor(QStringLiteral("#800080")),
|
||||
QColor(QStringLiteral("#008080")), QColor(QStringLiteral("#c0c0c0")),
|
||||
QColor(QStringLiteral("#808080")), QColor(QStringLiteral("#ff0000")),
|
||||
QColor(QStringLiteral("#00ff00")), QColor(QStringLiteral("#ffff00")),
|
||||
QColor(QStringLiteral("#0000ff")), QColor(QStringLiteral("#ff00ff")),
|
||||
QColor(QStringLiteral("#00ffff")), QColor(QStringLiteral("#ffffff"))};
|
||||
return base.at(static_cast<size_t>(index));
|
||||
}
|
||||
|
||||
if (index >= 16 && index <= 231) {
|
||||
const int c = index - 16;
|
||||
const int r = c / 36;
|
||||
const int g = (c / 6) % 6;
|
||||
const int b = c % 6;
|
||||
|
||||
const auto channel = [](int v) { return v == 0 ? 0 : 55 + v * 40; };
|
||||
return QColor(channel(r), channel(g), channel(b));
|
||||
}
|
||||
|
||||
const int gray = 8 + (index - 232) * 10;
|
||||
return QColor(gray, gray, gray);
|
||||
}
|
||||
|
||||
void TerminalView::applyThemePalette(const ThemePalette& palette)
|
||||
{
|
||||
m_palette = palette;
|
||||
|
||||
const QString stylesheet = QStringLiteral("QTextEdit { background: %1; color: %2; }")
|
||||
.arg(m_palette.background.name(), m_palette.foreground.name());
|
||||
setStyleSheet(stylesheet);
|
||||
|
||||
if (!m_hasFgColor) {
|
||||
m_fgColor = m_palette.foreground;
|
||||
}
|
||||
if (!m_hasBgColor) {
|
||||
m_bgColor = m_palette.background;
|
||||
}
|
||||
applyCurrentFormat();
|
||||
}
|
||||
|
||||
void TerminalView::applyCurrentFormat()
|
||||
{
|
||||
m_currentFormat = QTextCharFormat();
|
||||
m_currentFormat.setForeground(m_hasFgColor ? m_fgColor : m_palette.foreground);
|
||||
if (m_hasBgColor) {
|
||||
m_currentFormat.setBackground(m_bgColor);
|
||||
}
|
||||
QFont font = currentFont();
|
||||
font.setBold(m_bold);
|
||||
m_currentFormat.setFont(font);
|
||||
}
|
||||
|
||||
void TerminalView::resetSgrState()
|
||||
{
|
||||
m_bold = false;
|
||||
m_hasFgColor = false;
|
||||
m_hasBgColor = false;
|
||||
m_fgColor = m_palette.foreground;
|
||||
m_bgColor = m_palette.background;
|
||||
applyCurrentFormat();
|
||||
}
|
||||
|
||||
void TerminalView::handleSgrSequence(const QString& params)
|
||||
{
|
||||
QStringList parts = params.split(QChar::fromLatin1(';'), Qt::KeepEmptyParts);
|
||||
if (parts.isEmpty()) {
|
||||
parts.push_back(QStringLiteral("0"));
|
||||
}
|
||||
|
||||
for (int i = 0; i < parts.size(); ++i) {
|
||||
const QString part = parts.at(i).trimmed();
|
||||
bool ok = false;
|
||||
int code = part.isEmpty() ? 0 : part.toInt(&ok);
|
||||
if (!ok && !part.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code == 0) {
|
||||
resetSgrState();
|
||||
continue;
|
||||
}
|
||||
if (code == 1) {
|
||||
m_bold = true;
|
||||
continue;
|
||||
}
|
||||
if (code == 22) {
|
||||
m_bold = false;
|
||||
continue;
|
||||
}
|
||||
if (code == 39) {
|
||||
m_hasFgColor = false;
|
||||
continue;
|
||||
}
|
||||
if (code == 49) {
|
||||
m_hasBgColor = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code >= 30 && code <= 37) {
|
||||
m_fgColor = paletteColor(false, code - 30, false);
|
||||
m_hasFgColor = true;
|
||||
continue;
|
||||
}
|
||||
if (code >= 90 && code <= 97) {
|
||||
m_fgColor = paletteColor(false, code - 90, true);
|
||||
m_hasFgColor = true;
|
||||
continue;
|
||||
}
|
||||
if (code >= 40 && code <= 47) {
|
||||
m_bgColor = paletteColor(true, code - 40, false);
|
||||
m_hasBgColor = true;
|
||||
continue;
|
||||
}
|
||||
if (code >= 100 && code <= 107) {
|
||||
m_bgColor = paletteColor(true, code - 100, true);
|
||||
m_hasBgColor = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code == 38 || code == 48) {
|
||||
const bool background = (code == 48);
|
||||
if (i + 1 >= parts.size()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int mode = parts.at(i + 1).toInt(&ok);
|
||||
if (!ok) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mode == 5 && i + 2 < parts.size()) {
|
||||
const int index = parts.at(i + 2).toInt(&ok);
|
||||
if (ok) {
|
||||
QColor color = colorFrom256Index(index);
|
||||
if (background) {
|
||||
m_bgColor = color;
|
||||
m_hasBgColor = true;
|
||||
} else {
|
||||
m_fgColor = color;
|
||||
m_hasFgColor = true;
|
||||
}
|
||||
}
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mode == 2 && i + 4 < parts.size()) {
|
||||
const int r = parts.at(i + 2).toInt(&ok);
|
||||
if (!ok) {
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
const int g = parts.at(i + 3).toInt(&ok);
|
||||
if (!ok) {
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
const int b = parts.at(i + 4).toInt(&ok);
|
||||
if (!ok) {
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
|
||||
const QColor color(r, g, b);
|
||||
if (background) {
|
||||
m_bgColor = color;
|
||||
m_hasBgColor = true;
|
||||
} else {
|
||||
m_fgColor = color;
|
||||
m_hasFgColor = true;
|
||||
}
|
||||
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyCurrentFormat();
|
||||
}
|
||||
|
||||
void TerminalView::appendTextChunk(const QString& text)
|
||||
{
|
||||
if (text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QTextCursor cursor = textCursor();
|
||||
cursor.movePosition(QTextCursor::End);
|
||||
cursor.insertText(text, m_currentFormat);
|
||||
setTextCursor(cursor);
|
||||
ensureCursorVisible();
|
||||
}
|
||||
|
||||
QColor TerminalView::paletteColor(bool, int index, bool bright) const
|
||||
{
|
||||
const int safeIndex = std::clamp(index, 0, 7);
|
||||
return bright ? m_palette.bright.at(static_cast<size_t>(safeIndex))
|
||||
: m_palette.normal.at(static_cast<size_t>(safeIndex));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user