521 lines
16 KiB
C++
521 lines
16 KiB
C++
#include "terminal_view.h"
|
|
|
|
#include <QApplication>
|
|
#include <QClipboard>
|
|
#include <QColor>
|
|
#include <QFocusEvent>
|
|
#include <QFontMetrics>
|
|
#include <QKeyEvent>
|
|
#include <QResizeEvent>
|
|
#include <QTimer>
|
|
#include <QTextCursor>
|
|
|
|
#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(false);
|
|
setUndoRedoEnabled(false);
|
|
setAcceptRichText(false);
|
|
setLineWrapMode(QTextEdit::NoWrap);
|
|
setContextMenuPolicy(Qt::NoContextMenu);
|
|
setCursorWidth(2);
|
|
document()->setMaximumBlockCount(4000);
|
|
|
|
applyThemePalette(paletteByName(QStringLiteral("Dark")));
|
|
resetSgrState();
|
|
|
|
QTimer::singleShot(0, this, [this]() {
|
|
moveCursor(QTextCursor::End);
|
|
emitTerminalSize();
|
|
});
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (event == nullptr) {
|
|
return;
|
|
}
|
|
|
|
moveCursor(QTextCursor::End);
|
|
|
|
const Qt::KeyboardModifiers modifiers = event->modifiers();
|
|
|
|
if (modifiers == (Qt::ControlModifier | Qt::ShiftModifier)
|
|
&& event->key() == Qt::Key_C) {
|
|
const QString selected = textCursor().selectedText();
|
|
if (!selected.isEmpty()) {
|
|
QApplication::clipboard()->setText(selected);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (modifiers == Qt::ControlModifier) {
|
|
switch (event->key()) {
|
|
case Qt::Key_C:
|
|
emit inputGenerated(QStringLiteral("\x03"));
|
|
return;
|
|
case Qt::Key_D:
|
|
emit inputGenerated(QStringLiteral("\x04"));
|
|
return;
|
|
case Qt::Key_L:
|
|
emit inputGenerated(QStringLiteral("\x0c"));
|
|
return;
|
|
case Qt::Key_V: {
|
|
const QString clipboardText = QApplication::clipboard()->text();
|
|
if (!clipboardText.isEmpty()) {
|
|
emit inputGenerated(clipboardText);
|
|
}
|
|
return;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (event->key()) {
|
|
case Qt::Key_Return:
|
|
case Qt::Key_Enter:
|
|
emit inputGenerated(QStringLiteral("\n"));
|
|
return;
|
|
case Qt::Key_Backspace:
|
|
emit inputGenerated(QStringLiteral("\x7f"));
|
|
return;
|
|
case Qt::Key_Tab:
|
|
emit inputGenerated(QStringLiteral("\t"));
|
|
return;
|
|
case Qt::Key_Left:
|
|
emit inputGenerated(QStringLiteral("\x1b[D"));
|
|
return;
|
|
case Qt::Key_Right:
|
|
emit inputGenerated(QStringLiteral("\x1b[C"));
|
|
return;
|
|
case Qt::Key_Up:
|
|
emit inputGenerated(QStringLiteral("\x1b[A"));
|
|
return;
|
|
case Qt::Key_Down:
|
|
emit inputGenerated(QStringLiteral("\x1b[B"));
|
|
return;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
const QString text = event->text();
|
|
if (!text.isEmpty()) {
|
|
emit inputGenerated(text);
|
|
return;
|
|
}
|
|
}
|
|
|
|
void TerminalView::focusInEvent(QFocusEvent* event)
|
|
{
|
|
QTextEdit::focusInEvent(event);
|
|
moveCursor(QTextCursor::End);
|
|
}
|
|
|
|
void TerminalView::resizeEvent(QResizeEvent* event)
|
|
{
|
|
QTextEdit::resizeEvent(event);
|
|
emitTerminalSize();
|
|
}
|
|
|
|
TerminalView::ThemePalette TerminalView::paletteByName(const QString& themeName)
|
|
{
|
|
const QString theme = normalizedThemeName(themeName);
|
|
|
|
if (theme == QStringLiteral("light")) {
|
|
return ThemePalette{QStringLiteral("Light"),
|
|
QColor(QStringLiteral("#ececec")),
|
|
QColor(QStringLiteral("#000000")),
|
|
{QColor(QStringLiteral("#000000")),
|
|
QColor(QStringLiteral("#aa0000")),
|
|
QColor(QStringLiteral("#008000")),
|
|
QColor(QStringLiteral("#7a5f00")),
|
|
QColor(QStringLiteral("#0033cc")),
|
|
QColor(QStringLiteral("#8a00a8")),
|
|
QColor(QStringLiteral("#005f87")),
|
|
QColor(QStringLiteral("#333333"))},
|
|
{QColor(QStringLiteral("#5c5c5c")),
|
|
QColor(QStringLiteral("#d30000")),
|
|
QColor(QStringLiteral("#00a000")),
|
|
QColor(QStringLiteral("#9a7700")),
|
|
QColor(QStringLiteral("#0055ff")),
|
|
QColor(QStringLiteral("#b300db")),
|
|
QColor(QStringLiteral("#007ea7")),
|
|
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;
|
|
const 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) {
|
|
const 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));
|
|
}
|
|
|
|
int TerminalView::terminalColumns() const
|
|
{
|
|
const QFontMetrics metrics(font());
|
|
const int cellWidth = std::max(1, metrics.horizontalAdvance(QChar::fromLatin1('M')));
|
|
return std::max(1, viewport()->width() / cellWidth);
|
|
}
|
|
|
|
int TerminalView::terminalRows() const
|
|
{
|
|
const QFontMetrics metrics(font());
|
|
const int cellHeight = std::max(1, metrics.lineSpacing());
|
|
return std::max(1, viewport()->height() / cellHeight);
|
|
}
|
|
|
|
void TerminalView::emitTerminalSize()
|
|
{
|
|
emit terminalSizeChanged(terminalColumns(), terminalRows());
|
|
}
|