#include "terminal_view.h" #include #include #include #include #include #include #include #include #include #include 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 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(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(safeIndex)) : m_palette.normal.at(static_cast(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()); }