// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 #include "terminalsurface.h" #include "surfaceintegration.h" #include "keys.h" #include "scrollback.h" #include #include #include namespace TerminalSolution { Q_LOGGING_CATEGORY(log, "qtc.terminal.surface", QtWarningMsg); QColor toQColor(const VTermColor &c) { return QColor(qRgb(c.rgb.red, c.rgb.green, c.rgb.blue)); }; constexpr int batchFlushSize = 256; struct TerminalSurfacePrivate { TerminalSurfacePrivate(TerminalSurface *surface, const QSize &initialGridSize) : m_vterm(vterm_new(initialGridSize.height(), initialGridSize.width()), vterm_free) , m_vtermScreen(vterm_obtain_screen(m_vterm.get())) , m_scrollback(std::make_unique(100'000'000)) , q(surface) {} void flush() { if (m_writeBuffer.isEmpty()) return; QByteArray data = m_writeBuffer.left(batchFlushSize); qint64 result = m_writeToPty(data); if (result != data.size()) { // Not all data was written, remove the unwritten data from the array data.resize(qMax(0, result)); } // Remove the written data from the buffer if (data.size() > 0) m_writeBuffer = m_writeBuffer.mid(data.size()); if (!m_writeBuffer.isEmpty()) m_delayWriteTimer.start(); } void init() { m_delayWriteTimer.setInterval(1); m_delayWriteTimer.setSingleShot(true); QObject::connect(&m_delayWriteTimer, &QTimer::timeout, &m_delayWriteTimer, [this] { flush(); }); vterm_set_utf8(m_vterm.get(), true); static auto writeToPty = [](const char *s, size_t len, void *user) { auto p = static_cast(user); QByteArray d(s, len); // If its just a couple of chars, or we already have data in the writeBuffer, // add the new data to the write buffer and start the delay timer if (d.size() < batchFlushSize || !p->m_writeBuffer.isEmpty()) { p->m_writeBuffer.append(d); p->m_delayWriteTimer.start(); return; } // Try to write the data ... qint64 result = p->m_writeToPty(d); if (result != d.size()) { // if writing failed, append the data to the writeBuffer and start the delay timer // Check if partial data may have already been written ... if (result <= 0) p->m_writeBuffer.append(d); else p->m_writeBuffer.append(d.mid(result)); p->m_delayWriteTimer.start(); } }; vterm_output_set_callback(m_vterm.get(), writeToPty, this); memset(&m_vtermScreenCallbacks, 0, sizeof(m_vtermScreenCallbacks)); m_vtermScreenCallbacks.damage = [](VTermRect rect, void *user) { auto p = static_cast(user); p->invalidate(rect); return 1; }; m_vtermScreenCallbacks.sb_pushline = [](int cols, const VTermScreenCell *cells, void *user) { auto p = static_cast(user); return p->sb_pushline(cols, cells); }; m_vtermScreenCallbacks.sb_popline = [](int cols, VTermScreenCell *cells, void *user) { auto p = static_cast(user); return p->sb_popline(cols, cells); }; m_vtermScreenCallbacks.settermprop = [](VTermProp prop, VTermValue *val, void *user) { auto p = static_cast(user); return p->setTerminalProperties(prop, val); }; m_vtermScreenCallbacks.movecursor = [](VTermPos pos, VTermPos oldpos, int visible, void *user) { auto p = static_cast(user); return p->movecursor(pos, oldpos, visible); }; m_vtermScreenCallbacks.sb_clear = [](void *user) { auto p = static_cast(user); emit p->q->cleared(); return p->sb_clear(); }; m_vtermScreenCallbacks.bell = [](void *user) { auto p = static_cast(user); if (p->m_surfaceIntegration) p->m_surfaceIntegration->onBell(); return 1; }; vterm_screen_set_callbacks(m_vtermScreen, &m_vtermScreenCallbacks, this); vterm_screen_set_damage_merge(m_vtermScreen, VTERM_DAMAGE_SCROLL); vterm_screen_enable_altscreen(m_vtermScreen, true); memset(&m_vtermStateFallbacks, 0, sizeof(m_vtermStateFallbacks)); m_vtermStateFallbacks.osc = [](int cmd, VTermStringFragment fragment, void *user) { auto p = static_cast(user); return p->osc(cmd, fragment); }; VTermState *vts = vterm_obtain_state(m_vterm.get()); vterm_state_set_unrecognised_fallbacks(vts, &m_vtermStateFallbacks, this); memset(&m_vtermSelectionCallbacks, 0, sizeof(m_vtermSelectionCallbacks)); m_vtermSelectionCallbacks.query = [](VTermSelectionMask mask, void *user) { if (!(mask & 0xF)) return 0; auto p = static_cast(user); if (p->m_surfaceIntegration) p->m_surfaceIntegration->onGetClipboard(); return 0; }; m_vtermSelectionCallbacks.set = [](VTermSelectionMask mask, VTermStringFragment frag, void *user) { if (!(mask & 0xF)) return 0; auto p = static_cast(user); if (frag.initial) p->m_selectionBuffer.clear(); p->m_selectionBuffer.append(frag.str, frag.len); if (!frag.final) return 1; if (p->m_surfaceIntegration) p->m_surfaceIntegration->onSetClipboard(p->m_selectionBuffer); return 1; }; vterm_state_set_selection_callbacks( vts, &m_vtermSelectionCallbacks, this, buffer, sizeof buffer); vterm_state_set_bold_highbright(vts, true); VTermColor fg; VTermColor bg; vterm_color_indexed(&fg, ColorIndex::Foreground); vterm_color_indexed(&bg, ColorIndex::Background); vterm_state_set_default_colors(vts, &fg, &bg); for (int i = 0; i < 16; ++i) { VTermColor col; vterm_color_indexed(&col, i); vterm_state_set_palette_color(vts, i, &col); } vterm_screen_reset(m_vtermScreen, 1); } QSize liveSize() const { int rows; int cols; vterm_get_size(m_vterm.get(), &rows, &cols); return QSize(cols, rows); } std::variant toVariantColor(const VTermColor &color) { if (color.type & VTERM_COLOR_DEFAULT_BG) return ColorIndex::Background; else if (color.type & VTERM_COLOR_DEFAULT_FG) return ColorIndex::Foreground; else if (color.type & VTERM_COLOR_INDEXED) { if (color.indexed.idx >= 16) { VTermColor c = color; vterm_state_convert_color_to_rgb(vterm_obtain_state(m_vterm.get()), &c); return toQColor(c); } return color.indexed.idx; } else if (color.type == VTERM_COLOR_RGB) return toQColor(color); else return -1; } TerminalCell toCell(const VTermScreenCell &cell) { TerminalCell result; result.width = cell.width; result.text = QString::fromUcs4(reinterpret_cast(cell.chars)); const VTermColor *bg = &cell.bg; const VTermColor *fg = &cell.fg; if (static_cast(cell.attrs.reverse)) std::swap(fg, bg); result.backgroundColor = toVariantColor(*bg); result.foregroundColor = toVariantColor(*fg); result.bold = cell.attrs.bold; result.strikeOut = cell.attrs.strike; if (cell.attrs.underline > 0) { result.underlineStyle = QTextCharFormat::NoUnderline; switch (cell.attrs.underline) { case VTERM_UNDERLINE_SINGLE: result.underlineStyle = QTextCharFormat::SingleUnderline; break; case VTERM_UNDERLINE_DOUBLE: // TODO: Double underline result.underlineStyle = QTextCharFormat::SingleUnderline; break; case VTERM_UNDERLINE_CURLY: result.underlineStyle = QTextCharFormat::WaveUnderline; break; case VTERM_UNDERLINE_DASHED: result.underlineStyle = QTextCharFormat::DashUnderline; break; case VTERM_UNDERLINE_DOTTED: result.underlineStyle = QTextCharFormat::DotLine; break; } } result.strikeOut = cell.attrs.strike; return result; } // Callbacks from vterm void invalidate(VTermRect rect) { if (!m_altscreen) { rect.start_row += m_scrollback->size(); rect.end_row += m_scrollback->size(); } emit q->invalidated( QRect{QPoint{rect.start_col, rect.start_row}, QPoint{rect.end_col, rect.end_row - 1}}); } int sb_pushline(int cols, const VTermScreenCell *cells) { auto oldSize = m_scrollback->size(); m_scrollback->emplace(cols, cells); if (m_scrollback->size() != oldSize) emit q->fullSizeChanged(q->fullSize()); return 1; } int sb_popline(int cols, VTermScreenCell *cells) { if (m_scrollback->size() == 0) return 0; m_scrollback->popto(cols, cells); emit q->fullSizeChanged(q->fullSize()); return 1; } int sb_clear() { m_scrollback->clear(); emit q->fullSizeChanged(q->fullSize()); return 1; } int osc(int cmd, const VTermStringFragment &fragment) { if (m_surfaceIntegration) { m_surfaceIntegration->onOsc(cmd, {fragment.str, fragment.len}, fragment.initial, fragment.final); } return 1; } int setTerminalProperties(VTermProp prop, VTermValue *val) { switch (prop) { case VTERM_PROP_CURSORVISIBLE: { Cursor old = q->cursor(); m_cursor.visible = val->boolean; q->cursorChanged(old, q->cursor()); break; } case VTERM_PROP_CURSORBLINK: { Cursor old = q->cursor(); m_cursor.blink = val->boolean; emit q->cursorChanged(old, q->cursor()); break; } case VTERM_PROP_CURSORSHAPE: { Cursor old = q->cursor(); m_cursor.shape = (Cursor::Shape) val->number; emit q->cursorChanged(old, q->cursor()); break; } case VTERM_PROP_ICONNAME: break; case VTERM_PROP_TITLE: if (m_surfaceIntegration) m_surfaceIntegration->onTitle(QString::fromUtf8(val->string.str, val->string.len)); break; case VTERM_PROP_ALTSCREEN: m_altscreen = val->boolean; emit q->altscreenChanged(m_altscreen); break; case VTERM_PROP_MOUSE: qCDebug(log) << "Ignoring VTERM_PROP_MOUSE" << val->number; break; case VTERM_PROP_REVERSE: qCDebug(log) << "Ignoring VTERM_PROP_REVERSE" << val->boolean; break; case VTERM_N_PROPS: break; case VTERM_PROP_FOCUSREPORT: break; } return 1; } int movecursor(VTermPos pos, VTermPos oldpos, int visible) { Q_UNUSED(oldpos) Cursor oldCursor = q->cursor(); m_cursor.position = {pos.col, pos.row}; m_cursor.visible = visible > 0; q->cursorChanged(oldCursor, q->cursor()); return 1; } const VTermScreenCell *cellAt(int x, int y) { if (y < 0 || x < 0 || y >= q->fullSize().height() || x >= liveSize().width()) { qCWarning(log) << "Invalid Parameter for cellAt:" << x << y << "liveSize:" << liveSize() << "fullSize:" << q->fullSize(); return nullptr; } if (!m_altscreen && y < m_scrollback->size()) { const auto &sbl = m_scrollback->line((m_scrollback->size() - 1) - y); if (x < sbl.cols()) { return sbl.cell(x); } return nullptr; } if (!m_altscreen) y -= m_scrollback->size(); static VTermScreenCell refCell{}; VTermPos vtp{y, x}; vterm_screen_get_cell(m_vtermScreen, vtp, &refCell); return &refCell; } std::unique_ptr m_vterm; char buffer[256]; VTermScreen *m_vtermScreen; VTermScreenCallbacks m_vtermScreenCallbacks; VTermStateFallbacks m_vtermStateFallbacks; VTermSelectionCallbacks m_vtermSelectionCallbacks; Cursor m_cursor; QString m_currentCommand; bool m_altscreen{false}; std::unique_ptr m_scrollback; SurfaceIntegration *m_surfaceIntegration{nullptr}; TerminalSurface *q; QTimer m_delayWriteTimer; QByteArray m_writeBuffer; QByteArray m_selectionBuffer; TerminalSurface::WriteToPty m_writeToPty; }; TerminalSurface::TerminalSurface(QSize initialGridSize) : d(std::make_unique(this, initialGridSize)) { d->init(); } TerminalSurface::~TerminalSurface() = default; int TerminalSurface::cellWidthAt(int x, int y) const { const VTermScreenCell *cell = d->cellAt(x, y); if (!cell) return 0; return cell->width; } QSize TerminalSurface::liveSize() const { return d->liveSize(); } QSize TerminalSurface::fullSize() const { if (d->m_altscreen) return liveSize(); return QSize{d->liveSize().width(), d->liveSize().height() + d->m_scrollback->size()}; } std::u32string::value_type TerminalSurface::fetchCharAt(int x, int y) const { const VTermScreenCell *cell = d->cellAt(x, y); if (!cell) return 0; if (cell->width == 0) return 0; if (cell->chars[0] == 0xffffffff) return 0; QString s = QString::fromUcs4(reinterpret_cast(cell->chars), 6) .normalized(QString::NormalizationForm_C); const QList ucs4 = s.toUcs4(); return std::u32string(ucs4.begin(), ucs4.end()).front(); } TerminalCell TerminalSurface::fetchCell(int x, int y) const { static TerminalCell emptyCell{1, {}, {}, false, ColorIndex::Foreground, ColorIndex::Background, QTextCharFormat::NoUnderline, false}; if (y < 0 || y >= fullSize().height() || x >= fullSize().width()) { qCWarning(log) << "Invalid Parameter for fetchCell:" << x << y << "fullSize:" << fullSize(); return emptyCell; } const VTermScreenCell *refCell = d->cellAt(x, y); if (!refCell) return emptyCell; return d->toCell(*refCell); } void TerminalSurface::clearAll() { // Fake a scrollback clearing QByteArray data{"\x1b[3J"}; vterm_input_write(d->m_vterm.get(), data.constData(), data.size()); // Send Ctrl+L which will clear the screen d->m_writeToPty(QByteArray("\f")); } void TerminalSurface::resize(QSize newSize) { vterm_set_size(d->m_vterm.get(), newSize.height(), newSize.width()); } QPoint TerminalSurface::posToGrid(int pos) const { return {pos % d->liveSize().width(), pos / d->liveSize().width()}; } int TerminalSurface::gridToPos(QPoint gridPos) const { return gridPos.y() * d->liveSize().width() + gridPos.x(); } void TerminalSurface::dataFromPty(const QByteArray &data) { vterm_input_write(d->m_vterm.get(), data.constData(), data.size()); vterm_screen_flush_damage(d->m_vtermScreen); } void TerminalSurface::flush() { vterm_screen_flush_damage(d->m_vtermScreen); } void TerminalSurface::pasteFromClipboard(const QString &clipboardText) { if (clipboardText.isEmpty()) return; vterm_keyboard_start_paste(d->m_vterm.get()); for (unsigned int ch : clipboardText.toUcs4()) { // Workaround for weird nano behavior to correctly paste newlines // see: http://savannah.gnu.org/bugs/?49176 // and: https://github.com/kovidgoyal/kitty/issues/994 if (ch == '\n') ch = '\r'; vterm_keyboard_unichar(d->m_vterm.get(), ch, VTERM_MOD_NONE); } vterm_keyboard_end_paste(d->m_vterm.get()); if (!d->m_altscreen) { emit unscroll(); } } void TerminalSurface::sendKey(Qt::Key key) { if (key == Qt::Key_Escape) vterm_keyboard_key(d->m_vterm.get(), VTERM_KEY_ESCAPE, VTERM_MOD_NONE); } void TerminalSurface::sendKey(const QString &text) { for (const unsigned int ch : text.toUcs4()) vterm_keyboard_unichar(d->m_vterm.get(), ch, VTERM_MOD_NONE); } void TerminalSurface::sendKey(QKeyEvent *event) { bool keypad = event->modifiers() & Qt::KeypadModifier; VTermModifier mod = qtModifierToVTerm(event->modifiers()); VTermKey key = qtKeyToVTerm(Qt::Key(event->key()), keypad); if (key != VTERM_KEY_NONE) { if (mod == VTERM_MOD_SHIFT && (key == VTERM_KEY_ESCAPE || key == VTERM_KEY_BACKSPACE)) mod = VTERM_MOD_NONE; vterm_keyboard_key(d->m_vterm.get(), key, mod); } else if (event->text().size() == 1) { // event->text() already contains the correct unicode character based on the modifiers // used, so we can simply convert it to Ucs4 and send it to the terminal. vterm_keyboard_unichar(d->m_vterm.get(), event->text().toUcs4()[0], VTERM_MOD_NONE); } else if (mod == VTERM_MOD_CTRL && event->key() >= Qt::Key_A && event->key() < Qt::Key_Z) { vterm_keyboard_unichar(d->m_vterm.get(), 'a' + (event->key() - Qt::Key_A), mod); } } Cursor TerminalSurface::cursor() const { Cursor cursor = d->m_cursor; if (!d->m_altscreen) cursor.position.setY(cursor.position.y() + d->m_scrollback->size()); return cursor; } SurfaceIntegration *TerminalSurface::surfaceIntegration() const { return d->m_surfaceIntegration; } void TerminalSurface::setSurfaceIntegration(SurfaceIntegration *surfaceIntegration) { d->m_surfaceIntegration = surfaceIntegration; } void TerminalSurface::mouseMove(QPoint pos, Qt::KeyboardModifiers modifiers) { vterm_mouse_move(d->m_vterm.get(), pos.y(), pos.x(), qtModifierToVTerm(modifiers)); } void TerminalSurface::mouseButton(Qt::MouseButton button, bool pressed, Qt::KeyboardModifiers modifiers) { int btnIdx = 0; switch (button) { case Qt::LeftButton: btnIdx = 1; break; case Qt::RightButton: btnIdx = 3; break; case Qt::MiddleButton: btnIdx = 2; break; case Qt::ExtraButton1: btnIdx = 4; break; case Qt::ExtraButton2: btnIdx = 5; break; case Qt::ExtraButton3: btnIdx = 6; break; case Qt::ExtraButton4: btnIdx = 7; break; default: return; } vterm_mouse_button(d->m_vterm.get(), btnIdx, pressed, qtModifierToVTerm(modifiers)); } void TerminalSurface::sendFocus(bool hasFocus) { VTermState *vts = vterm_obtain_state(d->m_vterm.get()); if (hasFocus) vterm_state_focus_in(vts); else vterm_state_focus_out(vts); } bool TerminalSurface::isInAltScreen() { return d->m_altscreen; } void TerminalSurface::setWriteToPty(WriteToPty writeToPty) { d->m_writeToPty = writeToPty; } CellIterator TerminalSurface::begin() const { auto res = CellIterator(this, {0, 0}); res.m_state = CellIterator::State::BEGIN; return res; } CellIterator TerminalSurface::end() const { return CellIterator(this); } std::reverse_iterator TerminalSurface::rbegin() const { return std::make_reverse_iterator(end()); } std::reverse_iterator TerminalSurface::rend() const { return std::make_reverse_iterator(begin()); } CellIterator TerminalSurface::iteratorAt(QPoint pos) const { return CellIterator(this, pos); } CellIterator TerminalSurface::iteratorAt(int pos) const { return CellIterator(this, pos); } std::reverse_iterator TerminalSurface::rIteratorAt(QPoint pos) const { return std::make_reverse_iterator(iteratorAt(pos)); } std::reverse_iterator TerminalSurface::rIteratorAt(int pos) const { return std::make_reverse_iterator(iteratorAt(pos)); } void TerminalSurface::enableLiveReflow(bool enable) { vterm_screen_enable_reflow(d->m_vtermScreen, enable); } } // namespace TerminalSolution