// SPDX-License-Identifier: MIT // Author: Diego Iastrubni #include "KodoTerm/KodoTerm.hpp" #include "PtyProcess.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static void vterm_output_callback(const char *s, size_t len, void *user) { auto *pty = static_cast(user); if (pty) { pty->write(QByteArray(s, (int)len)); } } static VTermColor toVTermColor(const QColor &c) { VTermColor vc; vc.type = VTERM_COLOR_RGB; vc.rgb.red = c.red(); vc.rgb.green = c.green(); vc.rgb.blue = c.blue(); return vc; } void KodoTerm::setConfig(const KodoTermConfig &config) { m_config = config; setFont(m_config.font); setTheme(m_config.theme); // Force a full redraw by resetting cell size and calling updateTerminalSize m_cellSize = QSize(0, 0); updateTerminalSize(); } void KodoTerm::setTheme(const TerminalTheme &theme) { m_config.theme = theme; VTermState *state = vterm_obtain_state(m_vterm); VTermColor fg = toVTermColor(theme.foreground), bg = toVTermColor(theme.background); vterm_state_set_default_colors(state, &fg, &bg); for (int i = 0; i < 16; ++i) { VTermColor c = toVTermColor(theme.palette[i]); vterm_state_set_palette_color(state, i, &c); } for (int i = 0; i < 256; ++i) { m_paletteCacheValid[i] = false; } for (auto &cell : m_cellCache) { cell.chars[0] = (uint32_t)-1; } damageAll(); } KodoTerm::KodoTerm(QWidget *parent) : QWidget(parent) { m_restoring = false; for (int i = 0; i < 256; ++i) { m_paletteCacheValid[i] = false; } setAttribute(Qt::WA_OpaquePaintEvent); setAttribute(Qt::WA_NoSystemBackground); memset(&m_lastVTermFg, 0, sizeof(VTermColor)); memset(&m_lastVTermBg, 0, sizeof(VTermColor)); m_config.font.setStyleHint(QFont::Monospace); setFocusPolicy(Qt::StrongFocus); m_scrollBar = new QScrollBar(Qt::Vertical, this); m_scrollBar->setRange(0, 0); connect(m_scrollBar, &QScrollBar::valueChanged, this, &KodoTerm::onScrollValueChanged); setMouseTracking(true); updateTerminalSize(); m_cursorBlinkTimer = new QTimer(this); m_cursorBlinkTimer->setInterval(500); m_restorationBannerTimer = new QTimer(this); m_restorationBannerTimer->setSingleShot(true); m_restorationBannerTimer->setInterval(3000); connect(m_restorationBannerTimer, &QTimer::timeout, this, [this]() { m_restorationBannerActive = false; update(); }); connect(m_cursorBlinkTimer, &QTimer::timeout, this, [this]() { if (m_cursorBlink) { m_cursorBlinkState = !m_cursorBlinkState; QRect r(m_cursorCol * m_cellSize.width(), m_cursorRow * m_cellSize.height(), m_cellSize.width(), m_cellSize.height()); update(r); } }); m_vterm = vterm_new(25, 80); if (!m_vterm) { return; } vterm_set_utf8(m_vterm, 1); m_vtermScreen = vterm_obtain_screen(m_vterm); if (!m_vtermScreen) { return; } vterm_screen_enable_altscreen(m_vtermScreen, 1); static VTermScreenCallbacks callbacks = {.damage = &KodoTerm::onDamage, .moverect = &KodoTerm::onMoveRect, .movecursor = &KodoTerm::onMoveCursor, .settermprop = &KodoTerm::onSetTermProp, .bell = &KodoTerm::onBell, .resize = nullptr, .sb_pushline = &KodoTerm::onSbPushLine, .sb_popline = &KodoTerm::onSbPopLine, .sb_clear = nullptr, .sb_pushline4 = nullptr}; vterm_screen_set_callbacks(m_vtermScreen, &callbacks, this); vterm_screen_reset(m_vtermScreen, 1); if (!m_environment.contains("TERM")) { m_environment.insert("TERM", "xterm-256color"); } if (!m_environment.contains("COLORTERM")) { m_environment.insert("COLORTERM", "truecolor"); } setTheme(m_config.theme); resetDirtyRect(); VTermState *state = vterm_obtain_state(m_vterm); static VTermStateFallbacks fallbacks = {.control = nullptr, .csi = nullptr, .osc = &KodoTerm::onOsc, .dcs = nullptr, .apc = nullptr, .pm = nullptr, .sos = nullptr}; vterm_state_set_unrecognised_fallbacks(state, &fallbacks, this); } KodoTerm::~KodoTerm() { if (m_replayFile) { m_replayFile->close(); delete m_replayFile; } if (m_pty) { m_pty->kill(); } if (m_vterm) { vterm_free(m_vterm); } } bool KodoTerm::start(bool reset) { if (m_pty) { m_pty->kill(); delete m_pty; m_pty = nullptr; } if (reset) { resetTerminal(); } setupPty(); if (m_program.isEmpty()) { return false; } m_pty->setProgram(m_program); m_pty->setArguments(m_arguments); m_pty->setWorkingDirectory(m_workingDirectory); m_pty->setProcessEnvironment(m_environment); if (m_config.enableLogging) { QDir logDir(m_config.logDirectory); if (!logDir.exists()) { logDir.mkpath("."); } QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss_zzz"); m_logFile.setFileName(logDir.filePath(QString("kodoterm_%1.log").arg(timestamp))); if (m_logFile.open(QIODevice::WriteOnly)) { QString h = QString("-- KodoTerm Session Log ---\nProgram: %1\nArguments: %2\nCWD: " "%3\nLOG_START_MARKER\n") .arg(m_program) .arg(m_arguments.join(" ")) .arg(m_workingDirectory); m_logFile.write(h.toUtf8()); m_logFile.flush(); } } updateTerminalSize(); int r, c; vterm_get_size(m_vterm, &r, &c); return m_pty->start(QSize(c, r)); } void KodoTerm::setupPty() { if (m_pty) { return; } m_pty = PtyProcess::create(this); if (!m_pty) { return; } connect(m_pty, &PtyProcess::readyRead, this, &KodoTerm::onPtyReadyRead); connect(m_pty, &PtyProcess::finished, this, &KodoTerm::finished); vterm_output_set_callback(m_vterm, vterm_output_callback, m_pty); } void KodoTerm::flushTerminal() { if (m_vtermScreen) { vterm_screen_flush_damage(m_vtermScreen); } } void KodoTerm::onPtyReadyRead(const QByteArray &data) { if (!data.isEmpty()) { if (m_logFile.isOpen()) { m_logFile.write(data); m_logFile.flush(); } vterm_input_write(m_vterm, data.constData(), data.size()); flushTerminal(); } } void KodoTerm::onScrollValueChanged(int value) { if (m_vterm && !m_backBuffer.isNull()) { VTermState *state = vterm_obtain_state(m_vterm); VTermColor dfg, dbg; vterm_state_get_default_colors(state, &dfg, &dbg); m_backBuffer.fill(mapColor(dbg, state)); for (auto &cell : m_cellCache) { cell.chars[0] = (uint32_t)-1; } } damageAll(); } void KodoTerm::scrollUp(int lines) { m_scrollBar->setValue(m_scrollBar->value() - lines); } void KodoTerm::scrollDown(int lines) { m_scrollBar->setValue(m_scrollBar->value() + lines); } void KodoTerm::pageUp() { scrollUp(m_scrollBar->pageStep()); } void KodoTerm::pageDown() { scrollDown(m_scrollBar->pageStep()); } int KodoTerm::onSbPushLine(int cols, const VTermScreenCell *cells, void *user) { return static_cast(user)->pushScrollback(cols, cells); } int KodoTerm::onSbPopLine(int cols, VTermScreenCell *cells, void *user) { return static_cast(user)->popScrollback(cols, cells); } int KodoTerm::onOsc(int command, VTermStringFragment frag, void *user) { auto *w = static_cast(user); if (frag.initial) { w->m_oscBuffer.clear(); } w->m_oscBuffer.append(frag.str, frag.len); if (frag.final && command == 7) { QString s = QString::fromUtf8(w->m_oscBuffer); while (!s.isEmpty() && (s.endsWith(';') || s.endsWith('') || s.endsWith('\n') || s.endsWith(' '))) { s.chop(1); } if (s.startsWith("file://")) { QUrl u(s); QString p = u.toLocalFile(); if (p.isEmpty() || (p.startsWith("//") && !u.host().isEmpty())) { p = u.path(); } if (!p.isEmpty() && w->m_cwd != p) { w->m_cwd = p; emit w->cwdChanged(p); } } else if (!s.isEmpty() && w->m_cwd != s) { w->m_cwd = s; emit w->cwdChanged(s); } } return 1; } int KodoTerm::pushScrollback(int cols, const VTermScreenCell *cells) { if (m_altScreen) { return 0; } SavedLine line; line.reserve(cols); for (int i = 0; i < cols; ++i) { SavedCell sc; memcpy(sc.chars, cells[i].chars, sizeof(sc.chars)); sc.attrs = cells[i].attrs; sc.fg = cells[i].fg; sc.bg = cells[i].bg; sc.width = cells[i].width; line.push_back(sc); } m_scrollback.push_back(std::move(line)); if ((int)m_scrollback.size() > m_config.maxScrollback) { m_scrollback.pop_front(); } bool bottom = m_scrollBar->value() == m_scrollBar->maximum(); m_scrollBar->setRange(0, (int)m_scrollback.size()); if (bottom) { m_scrollBar->setValue(m_scrollBar->maximum()); } return 1; } int KodoTerm::popScrollback(int cols, VTermScreenCell *cells) { if (m_scrollback.empty()) { return 0; } const SavedLine &line = m_scrollback.back(); int n = std::min(cols, (int)line.size()); for (int i = 0; i < n; ++i) { memcpy(cells[i].chars, line[i].chars, sizeof(cells[i].chars)); cells[i].attrs = line[i].attrs; cells[i].fg = line[i].fg; cells[i].bg = line[i].bg; cells[i].width = line[i].width; } for (int i = n; i < cols; ++i) { memset(&cells[i], 0, sizeof(VTermScreenCell)); cells[i].width = 1; } m_scrollback.pop_back(); m_scrollBar->setRange(0, (int)m_scrollback.size()); return 1; } void KodoTerm::updateTerminalSize() { QFontMetrics fm(m_config.font); QSize oldCellSize = m_cellSize; m_cellSize = QSize(fm.horizontalAdvance('W'), fm.height()); if (m_cellSize.width() <= 0 || m_cellSize.height() <= 0) { m_cellSize = QSize(10, 20); } int rows = height() / m_cellSize.height(), sb = m_scrollBar->isVisible() ? m_scrollBar->sizeHint().width() : 0, cols = (width() - sb) / m_cellSize.width(); if (rows <= 0) { rows = 1; } if (cols <= 0) { cols = 1; } int orows, ocols; if (m_vterm) { vterm_get_size(m_vterm, &orows, &ocols); if (rows == orows && cols == ocols && m_cellSize == oldCellSize && m_pendingLogReplay.isEmpty()) { return; } vterm_set_size(m_vterm, rows, cols); vterm_screen_flush_damage(m_vtermScreen); qreal dpr = devicePixelRatioF(); m_backBuffer = QImage(cols * m_cellSize.width() * dpr, rows * m_cellSize.height() * dpr, QImage::Format_RGB32); m_backBuffer.setDevicePixelRatio(dpr); VTermState *state = vterm_obtain_state(m_vterm); VTermColor dfg, dbg; vterm_state_get_default_colors(state, &dfg, &dbg); m_backBuffer.fill(mapColor(dbg, state)); m_cellCache.assign(rows * cols, VTermScreenCell{}); for (auto &c : m_cellCache) { c.chars[0] = (uint32_t)-1; } m_selectedCache.assign(rows * cols, false); m_scrollBar->setPageStep(rows); if (m_scrollBar->value() == m_scrollBar->maximum()) { m_scrollBar->setValue(m_scrollBar->maximum()); } if (!m_pendingLogReplay.isEmpty() && cols > 40) { QTimer::singleShot(100, this, &KodoTerm::processLogReplay); } } if (m_pty) { m_pty->resize(QSize(cols, rows)); } damageAll(); } void KodoTerm::processLogReplay() { if (m_pendingLogReplay.isEmpty() && !m_replayFile) { return; } if (!m_replayFile) { m_restoring = true; m_restorationBannerActive = true; m_restorationBannerText = tr("Restoring session..."); // Thorough reset vterm_screen_reset(m_vtermScreen, 1); m_scrollback.clear(); m_scrollBar->setRange(0, 0); m_scrollBar->setValue(0); VTermState *state = vterm_obtain_state(m_vterm); VTermColor dfg, dbg; vterm_state_get_default_colors(state, &dfg, &dbg); if (!m_backBuffer.isNull()) { m_backBuffer.fill(mapColor(dbg, state)); } for (auto &c : m_cellCache) { c.chars[0] = (uint32_t)-1; } m_selectedCache.assign(m_selectedCache.size(), false); update(); m_replayFile = new QFile(m_pendingLogReplay); m_pendingLogReplay.clear(); if (!m_replayFile->open(QIODevice::ReadOnly)) { delete m_replayFile; m_replayFile = nullptr; m_restoring = false; m_restorationBannerActive = false; return; } // Read header QByteArray header; while (!m_replayFile->atEnd()) { char c; if (m_replayFile->getChar(&c)) { header.append(c); if (c == '\n' && header.endsWith("LOG_START_MARKER\n")) { break; } if (header.size() > 1024) { // Header too big or invalid break; } } else { break; } } } if (m_replayFile) { QByteArray chunk = m_replayFile->read(65536); // 64KB if (!chunk.isEmpty()) { onPtyReadyRead(chunk); QTimer::singleShot(0, this, &KodoTerm::processLogReplay); } else { m_replayFile->close(); delete m_replayFile; m_replayFile = nullptr; onPtyReadyRead("\r\n"); scrollToBottom(); m_restoring = false; m_restorationBannerTimer->start(); damageAll(); update(); } } } void KodoTerm::resetDirtyRect() { m_dirtyRect.start_row = 10000; m_dirtyRect.start_col = 10000; m_dirtyRect.end_row = -1; m_dirtyRect.end_col = -1; } void KodoTerm::damageAll() { int r, c; if (!m_vterm) { return; } vterm_get_size(m_vterm, &r, &c); m_dirtyRect.start_row = 0; m_dirtyRect.start_col = 0; m_dirtyRect.end_row = r; m_dirtyRect.end_col = c; m_dirty = true; if (!m_restoring) { update(); } } void KodoTerm::drawRestorationBanner(QPainter &painter) { if (m_restorationBannerText.isEmpty()) { return; } QFont f = font(); f.setBold(true); f.setPointSize(f.pointSize() * 1.5); painter.setFont(f); QFontMetrics fm(f); int padding = 20; QRect textRect = fm.boundingRect(m_restorationBannerText); QRect bannerRect(0, 0, textRect.width() + padding * 2, textRect.height() + padding); bannerRect.moveCenter(rect().center()); painter.setRenderHint(QPainter::Antialiasing, m_config.textAntialiasing); painter.setRenderHint(QPainter::TextAntialiasing, m_config.textAntialiasing); painter.setBrush(QColor(0, 0, 0, 180)); painter.setPen(Qt::NoPen); painter.drawRoundedRect(bannerRect, 10, 10); painter.setPen(Qt::white); painter.drawText(bannerRect, Qt::AlignCenter, m_restorationBannerText); } static bool colorsEqual(const VTermColor &a, const VTermColor &b) { if (a.type != b.type) { return false; } if (a.type == VTERM_COLOR_RGB) { return a.rgb.red == b.rgb.red && a.rgb.green == b.rgb.green && a.rgb.blue == b.rgb.blue; } if (a.type == VTERM_COLOR_INDEXED) { return a.indexed.idx == b.indexed.idx; } return true; } static bool cellsEqual(const VTermScreenCell &a, const VTermScreenCell &b) { if (a.width != b.width) { return false; } if (memcmp(&a.attrs, &b.attrs, sizeof(VTermScreenCellAttrs)) != 0) { return false; } if (!colorsEqual(a.fg, b.fg) || !colorsEqual(a.bg, b.bg)) { return false; } for (int i = 0; i < VTERM_MAX_CHARS_PER_CELL; ++i) { if (a.chars[i] != b.chars[i]) { return false; } if (a.chars[i] == 0) { break; } } return true; } static bool isBoxChar(uint32_t c) { return (c >= 0x2500 && c <= 0x257F); } static void drawBoxChar(QPainter &p, const QRect &r, uint32_t c, const QColor &fg) { enum { L = 1, R = 2, U = 4, D = 8, H = 16, Db = 32 }; int f = 0; switch (c) { case 0x2500: f = L | R; break; case 0x2501: f = L | R | H; break; case 0x2502: f = U | D; break; case 0x2503: f = U | D | H; break; case 0x250C: f = D | R; break; case 0x250F: f = D | R | H; break; case 0x2510: f = D | L; break; case 0x2513: f = D | L | H; break; case 0x2514: f = U | R; break; case 0x2517: f = U | R | H; break; case 0x2518: f = U | L; break; case 0x251B: f = U | L | H; break; case 0x251C: f = U | D | R; break; case 0x2523: f = U | D | R | H; break; case 0x2524: f = U | D | L; break; case 0x252B: f = U | D | L | H; break; case 0x252C: f = D | L | R; break; case 0x2533: f = D | L | R | H; break; case 0x2534: f = U | L | R; break; case 0x253B: f = U | L | R | H; break; case 0x253C: f = U | D | L | R; break; case 0x254B: f = U | D | L | R | H; break; case 0x2550: f = L | R | Db; break; case 0x2551: f = U | D | Db; break; case 0x2554: f = D | R | Db; break; case 0x2557: f = D | L | Db; break; case 0x255A: f = U | R | Db; break; case 0x255D: f = U | L | Db; break; case 0x2560: f = U | D | R | Db; break; case 0x2563: f = U | D | L | Db; break; case 0x2566: f = D | L | R | Db; break; case 0x2569: f = U | L | R | Db; break; case 0x256C: f = U | D | L | R | Db; break; } if (f == 0) { p.setPen(fg); p.drawText(r, Qt::AlignCenter, QString::fromUcs4((const char32_t *)&c, 1)); return; } p.setPen(Qt::NoPen); p.setBrush(fg); int cx = r.center().x(); int cy = r.center().y(); int t = (f & H) ? 2 : 1; if (f & Db) { int g = 1; // gap if (f & U) { p.drawRect(cx - 1, r.top(), 1, cy - r.top() + g); p.drawRect(cx + 1, r.top(), 1, cy - r.top() + g); } if (f & D) { p.drawRect(cx - 1, cy - g, 1, r.bottom() - cy + 1 + g); p.drawRect(cx + 1, cy - g, 1, r.bottom() - cy + 1 + g); } if (f & L) { p.drawRect(r.left(), cy - 1, cx - r.left() + g, 1); p.drawRect(r.left(), cy + 1, cx - r.left() + g, 1); } if (f & R) { p.drawRect(cx - g, cy - 1, r.right() - cx + 1 + g, 1); p.drawRect(cx - g, cy + 1, r.right() - cx + 1 + g, 1); } } else { if (f & U) { p.drawRect(cx, r.top(), t, cy - r.top() + t); } if (f & D) { p.drawRect(cx, cy, t, r.bottom() - cy + 1); } if (f & L) { p.drawRect(r.left(), cy, cx - r.left() + t, t); } if (f & R) { p.drawRect(cx, cy, r.right() - cx + 1, t); } } } void KodoTerm::renderToBackbuffer() { if (m_backBuffer.isNull()) { return; } int rows, cols; vterm_get_size(m_vterm, &rows, &cols); int cur = m_scrollBar->value(), sb = (int)m_scrollback.size(); bool useCache = (cur == sb); if (useCache && (m_dirtyRect.start_row > m_dirtyRect.end_row)) { m_dirty = false; return; } QPainter painter(&m_backBuffer); QFont f = font(); f.setKerning(false); f.setStyleStrategy(m_config.textAntialiasing ? QFont::PreferAntialias : QFont::NoAntialias); painter.setFont(f); painter.setRenderHint(QPainter::TextAntialiasing, m_config.textAntialiasing); painter.setRenderHint(QPainter::Antialiasing, false); // Always false for cell backgrounds to prevent gaps VTermState *state = vterm_obtain_state(m_vterm); VTermColor dfg, dbg; vterm_state_get_default_colors(state, &dfg, &dbg); QColor defBg = mapColor(dbg, state), defFg = mapColor(dfg, state); VTermPos sS = m_selectionStart, sE = m_selectionEnd; bool hasS = (sS.row != -1); if (hasS && (sS.row > sE.row || (sS.row == sE.row && sS.col > sE.col))) { std::swap(sS, sE); } int sR = 0, eR = rows, sC = 0, eC = cols; if (useCache) { sR = std::max(0, m_dirtyRect.start_row); eR = std::min(rows, m_dirtyRect.end_row); sC = std::max(0, m_dirtyRect.start_col); eC = std::min(cols, m_dirtyRect.end_col); } for (int r = sR; r < eR; ++r) { int absR = cur + r; for (int c = sC; c < eC; ++c) { VTermScreenCell cell; if (absR < sb) { const SavedLine &l = m_scrollback[absR]; if (c < (int)l.size()) { const SavedCell &sc = l[c]; memcpy(cell.chars, sc.chars, sizeof(cell.chars)); cell.attrs = sc.attrs; cell.fg = sc.fg; cell.bg = sc.bg; cell.width = sc.width; } else { memset(&cell, 0, sizeof(cell)); cell.width = 1; } } else { vterm_screen_get_cell(m_vtermScreen, {r, c}, &cell); } if (cell.width == 0) { continue; } bool sel = false; if (hasS) { if (absR > sS.row && absR < sE.row) { sel = true; } else if (absR == sS.row && absR == sE.row) { sel = (c >= sS.col && c <= sE.col); } else if (absR == sS.row) { sel = (c >= sS.col); } else if (absR == sE.row) { sel = (c <= sE.col); } } if (useCache && sel == m_selectedCache[r * cols + c] && cellsEqual(cell, m_cellCache[r * cols + c])) { if (cell.width > 1) { c += (cell.width - 1); } continue; } if (useCache) { m_cellCache[r * cols + c] = cell; m_selectedCache[r * cols + c] = sel; } QColor fg = defFg, bg = defBg; if (!VTERM_COLOR_IS_DEFAULT_FG(&cell.fg)) { fg = mapColor(cell.fg, state); } if (!VTERM_COLOR_IS_DEFAULT_BG(&cell.bg)) { bg = mapColor(cell.bg, state); } if (cell.attrs.reverse ^ sel) { std::swap(fg, bg); } QRectF rect(c * m_cellSize.width(), r * m_cellSize.height(), cell.width * m_cellSize.width(), m_cellSize.height()); painter.fillRect(rect, bg); if (m_config.customBoxDrawing && isBoxChar(cell.chars[0])) { drawBoxChar(painter, rect.toRect(), cell.chars[0], fg); } else if (cell.chars[0] != 0) { int n_chars = 0; while (n_chars < VTERM_MAX_CHARS_PER_CELL && cell.chars[n_chars]) { n_chars++; } painter.setPen(fg); painter.drawText(rect, Qt::AlignCenter, QString::fromUcs4((const char32_t *)cell.chars, n_chars)); } if (cell.width > 1) { c += (cell.width - 1); } } } resetDirtyRect(); m_dirty = false; } int KodoTerm::onDamage(VTermRect r, void *u) { auto *w = static_cast(u); if (!w->m_pendingLogReplay.isEmpty()) { return 1; } w->m_dirtyRect.start_row = std::min(w->m_dirtyRect.start_row, r.start_row); w->m_dirtyRect.start_col = std::min(w->m_dirtyRect.start_col, r.start_col); w->m_dirtyRect.end_row = std::max(w->m_dirtyRect.end_row, r.end_row); w->m_dirtyRect.end_col = std::max(w->m_dirtyRect.end_col, r.end_col); w->m_dirty = true; if (!w->m_restoring) { w->update(); } return 1; } int KodoTerm::onMoveRect(VTermRect d, VTermRect s, void *u) { auto *w = static_cast(u); if (!w->m_pendingLogReplay.isEmpty()) { return 1; } int cols, rows; vterm_get_size(w->m_vterm, &rows, &cols); int h = s.end_row - s.start_row; if (d.start_row < s.start_row) { for (int r = 0; r < h; ++r) { int sr = s.start_row + r, dr = d.start_row + r; if (sr >= 0 && sr < rows && dr >= 0 && dr < rows) { std::copy(w->m_cellCache.begin() + sr * cols, w->m_cellCache.begin() + sr * cols + cols, w->m_cellCache.begin() + dr * cols); std::copy(w->m_selectedCache.begin() + sr * cols, w->m_selectedCache.begin() + sr * cols + cols, w->m_selectedCache.begin() + dr * cols); } } } else { for (int r = h - 1; r >= 0; --r) { int sr = s.start_row + r, dr = d.start_row + r; if (sr >= 0 && sr < rows && dr >= 0 && dr < rows) { std::copy(w->m_cellCache.begin() + sr * cols, w->m_cellCache.begin() + sr * cols + cols, w->m_cellCache.begin() + dr * cols); std::copy(w->m_selectedCache.begin() + sr * cols, w->m_selectedCache.begin() + sr * cols + cols, w->m_selectedCache.begin() + dr * cols); } } } w->m_dirtyRect.start_row = std::min({w->m_dirtyRect.start_row, d.start_row, s.start_row}); w->m_dirtyRect.end_row = std::max({w->m_dirtyRect.end_row, d.end_row, s.end_row}); w->m_dirtyRect.start_col = 0; w->m_dirtyRect.end_col = cols; w->m_dirty = true; if (!w->m_restoring) { w->update(); } return 1; } int KodoTerm::onMoveCursor(VTermPos p, VTermPos op, int v, void *u) { auto *w = static_cast(u); if (!w->m_pendingLogReplay.isEmpty()) { return 1; } w->m_cursorRow = p.row; w->m_cursorCol = p.col; w->m_cursorVisible = v; if (!w->m_restoring) { w->update(); } return 1; } int KodoTerm::onSetTermProp(VTermProp p, VTermValue *v, void *u) { auto *w = static_cast(u); if (!w->m_pendingLogReplay.isEmpty()) { return 1; } switch (p) { case VTERM_PROP_CURSORVISIBLE: w->m_cursorVisible = v->boolean; break; case VTERM_PROP_CURSORBLINK: w->m_cursorBlink = v->boolean; if (w->m_cursorBlink) { w->m_cursorBlinkTimer->start(); } else { w->m_cursorBlinkTimer->stop(); w->m_cursorBlinkState = true; } break; case VTERM_PROP_CURSORSHAPE: w->m_cursorShape = v->number; break; case VTERM_PROP_ALTSCREEN: w->m_altScreen = v->boolean; if (w->m_altScreen) { w->m_scrollBar->hide(); } else { w->m_scrollBar->show(); } w->updateTerminalSize(); break; case VTERM_PROP_TITLE: w->setWindowTitle(QString::fromUtf8(v->string.str, (int)v->string.len)); break; case VTERM_PROP_MOUSE: w->m_mouseMode = v->number; break; default: break; } if (!w->m_restoring) { w->damageAll(); } return 1; } int KodoTerm::onBell(void *u) { auto *w = static_cast(u); if (w->m_restoring || !w->m_pendingLogReplay.isEmpty()) { return 1; } if (w->m_config.audibleBell) { QApplication::beep(); } if (w->m_config.visualBell) { w->m_visualBellActive = true; w->update(); QTimer::singleShot(100, w, [w]() { w->m_visualBellActive = false; w->update(); }); } return 1; } void KodoTerm::resizeEvent(QResizeEvent *e) { int sb = m_scrollBar->sizeHint().width(); m_scrollBar->setGeometry(width() - sb, 0, sb, height()); updateTerminalSize(); QWidget::resizeEvent(e); } void KodoTerm::wheelEvent(QWheelEvent *e) { if (m_config.mouseWheelZoom && (e->modifiers() & Qt::ControlModifier)) { if (e->angleDelta().y() > 0) { zoomIn(); } else if (e->angleDelta().y() < 0) { zoomOut(); } return; } if (m_mouseMode > 0 && !(e->modifiers() & Qt::ShiftModifier)) { VTermModifier m = VTERM_MOD_NONE; if (e->modifiers() & Qt::ShiftModifier) { m = (VTermModifier)(m | VTERM_MOD_SHIFT); } if (e->modifiers() & Qt::ControlModifier) { m = (VTermModifier)(m | VTERM_MOD_CTRL); } if (e->modifiers() & Qt::AltModifier) { m = (VTermModifier)(m | VTERM_MOD_ALT); } int r = e->position().toPoint().y() / m_cellSize.height(), c = e->position().toPoint().x() / m_cellSize.width(), b = e->angleDelta().y() > 0 ? 4 : 5; vterm_mouse_move(m_vterm, r, c, m); vterm_mouse_button(m_vterm, b, true, m); vterm_screen_flush_damage(m_vtermScreen); return; } m_scrollBar->event(e); } void KodoTerm::mousePressEvent(QMouseEvent *e) { VTermModifier m = VTERM_MOD_NONE; if (e->modifiers() & Qt::ShiftModifier) { m = (VTermModifier)(m | VTERM_MOD_SHIFT); } if (e->modifiers() & Qt::ControlModifier) { m = (VTermModifier)(m | VTERM_MOD_CTRL); } if (e->modifiers() & Qt::AltModifier) { m = (VTermModifier)(m | VTERM_MOD_ALT); } int r = e->pos().y() / m_cellSize.height(), c = e->pos().x() / m_cellSize.width(); if (m_mouseMode > 0 && !(e->modifiers() & Qt::ShiftModifier)) { int b = 0; if (e->button() == Qt::LeftButton) { b = 1; } else if (e->button() == Qt::MiddleButton) { b = 2; } else if (e->button() == Qt::RightButton) { b = 3; } if (b > 0) { vterm_mouse_move(m_vterm, r, c, m); vterm_mouse_button(m_vterm, b, true, m); vterm_screen_flush_damage(m_vtermScreen); e->accept(); return; } } if (e->button() == Qt::LeftButton) { if (!m_clickTimer.isValid() || m_clickTimer.elapsed() > QApplication::doubleClickInterval() || (e->pos() - m_lastClickPos).manhattanLength() > 5) { m_clickCount = 1; } else { m_clickCount++; } m_clickTimer.restart(); m_lastClickPos = e->pos(); VTermPos vp = mouseToPos(e->pos()); if (m_clickCount == 3 && m_config.tripleClickSelectsLine) { int rows, cols; vterm_get_size(m_vterm, &rows, &cols); m_selectionStart = {vp.row, 0}; m_selectionEnd = {vp.row, cols - 1}; m_selecting = false; if (m_config.copyOnSelect) { copyToClipboard(); } damageAll(); } else if (m_clickCount == 1) { m_selecting = true; m_selectionStart = vp; m_selectionEnd = m_selectionStart; damageAll(); } update(); } else if (e->button() == Qt::MiddleButton && m_config.pasteOnMiddleClick) { pasteFromClipboard(); } e->accept(); } void KodoTerm::mouseDoubleClickEvent(QMouseEvent *e) { if (e->button() != Qt::LeftButton || (m_mouseMode > 0 && !(e->modifiers() & Qt::ShiftModifier))) { return; } m_clickCount = 2; m_clickTimer.restart(); m_lastClickPos = e->pos(); m_selecting = false; VTermPos vp = mouseToPos(e->pos()); int sb = (int)m_scrollback.size(), rows, cols; vterm_get_size(m_vterm, &rows, &cols); QString line; line.fill(' ', cols); int cc = vp.col; if (vp.row < sb) { const SavedLine &l = m_scrollback[vp.row]; int n = std::min((int)l.size(), cols); for (int c = 0; c < n; ++c) { if (l[c].chars[0] != 0) { line[c] = QChar(static_cast(l[c].chars[0] & 0xFFFF)); } } } else { int vr = vp.row - sb; if (vr < rows) { for (int c = 0; c < cols; ++c) { VTermScreenCell cell; vterm_screen_get_cell(m_vtermScreen, {vr, c}, &cell); if (cell.chars[0] != 0) { line[c] = QChar(static_cast(cell.chars[0] & 0xFFFF)); } } } } QRegularExpression re(m_config.wordSelectionRegex); if (re.isValid()) { QRegularExpressionMatchIterator it = re.globalMatch(line); while (it.hasNext()) { QRegularExpressionMatch m = it.next(); if (cc >= m.capturedStart() && cc < m.capturedEnd()) { m_selectionStart = {vp.row, static_cast(m.capturedStart())}; m_selectionEnd = {vp.row, static_cast(m.capturedEnd() - 1)}; if (m_config.copyOnSelect) { copyToClipboard(); } break; } } } damageAll(); e->accept(); } void KodoTerm::mouseMoveEvent(QMouseEvent *e) { VTermModifier m = VTERM_MOD_NONE; if (e->modifiers() & Qt::ShiftModifier) { m = (VTermModifier)(m | VTERM_MOD_SHIFT); } if (e->modifiers() & Qt::ControlModifier) { m = (VTermModifier)(m | VTERM_MOD_CTRL); } if (e->modifiers() & Qt::AltModifier) { m = (VTermModifier)(m | VTERM_MOD_ALT); } VTermPos vp = mouseToPos(e->pos()); int r = e->pos().y() / m_cellSize.height(), c = e->pos().x() / m_cellSize.width(); if (m_mouseMode > 0 && !(e->modifiers() & Qt::ShiftModifier)) { vterm_mouse_move(m_vterm, r, c, m); vterm_screen_flush_damage(m_vtermScreen); return; } if (m_selecting) { m_selectionEnd = vp; damageAll(); } QWidget::mouseMoveEvent(e); } void KodoTerm::mouseReleaseEvent(QMouseEvent *e) { VTermModifier m = VTERM_MOD_NONE; if (e->modifiers() & Qt::ShiftModifier) { m = (VTermModifier)(m | VTERM_MOD_SHIFT); } if (e->modifiers() & Qt::ControlModifier) { m = (VTermModifier)(m | VTERM_MOD_CTRL); } if (e->modifiers() & Qt::AltModifier) { m = (VTermModifier)(m | VTERM_MOD_ALT); } int r = e->pos().y() / m_cellSize.height(), c = e->pos().x() / m_cellSize.width(); if (m_mouseMode > 0 && !(e->modifiers() & Qt::ShiftModifier)) { int b = 0; if (e->button() == Qt::LeftButton) { b = 1; } else if (e->button() == Qt::MiddleButton) { b = 2; } else if (e->button() == Qt::RightButton) { b = 3; } if (b > 0) { vterm_mouse_move(m_vterm, r, c, m); vterm_mouse_button(m_vterm, b, false, m); vterm_screen_flush_damage(m_vtermScreen); e->accept(); return; } } if (e->button() == Qt::LeftButton && m_selecting) { m_selecting = false; m_selectionEnd = mouseToPos(e->pos()); if (m_selectionStart.row == m_selectionEnd.row && m_selectionStart.col == m_selectionEnd.col) { m_selectionStart = {-1, -1}; m_selectionEnd = {-1, -1}; } else if (m_config.copyOnSelect) { copyToClipboard(); } damageAll(); } e->accept(); } VTermPos KodoTerm::mouseToPos(const QPoint &p) const { if (m_cellSize.width() <= 0 || m_cellSize.height() <= 0) { return {0, 0}; } int r = p.y() / m_cellSize.height(), c = p.x() / m_cellSize.width(), sb = (int)m_scrollback.size(), cur = m_scrollBar->value(); VTermPos vp; vp.row = cur + r; vp.col = c; return vp; } bool KodoTerm::isSelected(int r, int c) const { if (m_selectionStart.row == -1) { return false; } VTermPos s = m_selectionStart, e = m_selectionEnd; if (s.row > e.row || (s.row == e.row && s.col > e.col)) { std::swap(s, e); } if (r < s.row || r > e.row) { return false; } if (r == s.row && r == e.row) { return c >= s.col && c <= e.col; } if (r == s.row) { return c >= s.col; } if (r == e.row) { return c <= e.col; } return true; } QString KodoTerm::getTextRange(VTermPos s, VTermPos e) { if (s.row > e.row || (s.row == e.row && s.col > e.col)) { std::swap(s, e); } QString t; int sb = (int)m_scrollback.size(), rs, cs; vterm_get_size(m_vterm, &rs, &cs); for (int r = s.row; r <= e.row; ++r) { int sc = (r == s.row) ? s.col : 0, ec = (r == e.row) ? e.col : 1000; if (r < sb) { const SavedLine &l = m_scrollback[r]; for (int c = sc; c <= ec && c < (int)l.size(); ++c) { for (int i = 0; i < VTERM_MAX_CHARS_PER_CELL && l[c].chars[i]; ++i) { t.append(QChar::fromUcs4(l[c].chars[i])); } } } else { int vr = r - sb; if (vr < rs) { for (int c = sc; c <= ec && c < cs; ++c) { VTermScreenCell cell; vterm_screen_get_cell(m_vtermScreen, {vr, c}, &cell); for (int i = 0; i < VTERM_MAX_CHARS_PER_CELL && cell.chars[i]; ++i) { t.append(QChar::fromUcs4(cell.chars[i])); } } } } if (r < e.row) { t.append('\n'); } } return t; } void KodoTerm::copyToClipboard() { if (m_selectionStart.row != -1) { QApplication::clipboard()->setText(getTextRange(m_selectionStart, m_selectionEnd)); } } void KodoTerm::pasteFromClipboard() { QString t = QApplication::clipboard()->text(); if (!t.isEmpty()) { m_pty->write(t.toUtf8()); } } void KodoTerm::selectAll() { int rs, cs; vterm_get_size(m_vterm, &rs, &cs); int sb = (int)m_scrollback.size(); m_selectionStart = {0, 0}; m_selectionEnd = {sb + rs - 1, cs - 1}; damageAll(); } void KodoTerm::clearScrollback() { m_scrollback.clear(); m_scrollBar->setRange(0, 0); m_scrollBar->setValue(0); damageAll(); } void KodoTerm::resetTerminal() { vterm_screen_reset(m_vtermScreen, 1); m_flowControlStopped = false; if (m_vterm && !m_backBuffer.isNull()) { VTermState *state = vterm_obtain_state(m_vterm); VTermColor dfg, dbg; vterm_state_get_default_colors(state, &dfg, &dbg); m_backBuffer.fill(mapColor(dbg, state)); } clearScrollback(); damageAll(); } void KodoTerm::openFileBrowser() { if (!m_cwd.isEmpty()) { QDir d(m_cwd); if (d.exists()) { QDesktopServices::openUrl(QUrl::fromLocalFile(d.absolutePath())); } } } void KodoTerm::kill() { if (m_pty) { m_pty->kill(); } } void KodoTerm::logData(const QByteArray &d) { if (m_logFile.isOpen()) { m_logFile.write(d); m_logFile.flush(); } } void KodoTerm::scrollToBottom() { if (m_scrollBar) { m_scrollBar->setValue(m_scrollBar->maximum()); } } void KodoTerm::contextMenuEvent(QContextMenuEvent *e) { if (m_mouseMode > 0 && !(QGuiApplication::keyboardModifiers() & Qt::ShiftModifier)) { return; } auto *m = new QMenu(this); auto *cA = m->addAction(tr("Copy"), this, &KodoTerm::copyToClipboard); cA->setEnabled(m_selectionStart.row != -1); cA->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_C)); auto *pA = m->addAction(tr("Paste"), this, &KodoTerm::pasteFromClipboard); pA->setEnabled(!QApplication::clipboard()->text().isEmpty()); pA->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_V)); m->addSeparator(); m->addAction(tr("Select All"), this, &KodoTerm::selectAll); m->addSeparator(); m->addAction(tr("Clear Scrollback"), this, &KodoTerm::clearScrollback); m->addAction(tr("Reset"), this, &KodoTerm::resetTerminal); m->addSeparator(); auto *oB = m->addAction(tr("Open current directory in file browser"), this, &KodoTerm::openFileBrowser); oB->setEnabled(!m_cwd.isEmpty() && QDir(m_cwd).exists()); m->addSeparator(); m->addAction(tr("Zoom In"), this, &KodoTerm::zoomIn); m->addAction(tr("Zoom Out"), this, &KodoTerm::zoomOut); m->addAction(tr("Reset Zoom"), this, &KodoTerm::resetZoom); m->addSeparator(); auto *tM = m->addMenu(tr("Themes")); auto tC = [this](const TerminalTheme::ThemeInfo &i) { setTheme(TerminalTheme::loadTheme(i.path)); }; populateThemeMenu(tM, tr("Konsole"), TerminalTheme::ThemeFormat::Konsole, tC); populateThemeMenu(tM, tr("Windows Terminal"), TerminalTheme::ThemeFormat::WindowsTerminal, tC); populateThemeMenu(tM, tr("iTerm"), TerminalTheme::ThemeFormat::ITerm, tC); emit contextMenuRequested(m, e->globalPos()); m->exec(e->globalPos()); delete m; } void KodoTerm::zoomIn() { qreal s = m_config.font.pointSizeF(); if (s <= 0) { s = m_config.font.pointSize(); } m_config.font.setPointSizeF(s + 1.0); m_config.font.setStyleStrategy(m_config.textAntialiasing ? QFont::PreferAntialias : QFont::NoAntialias); setFont(m_config.font); updateTerminalSize(); update(); } void KodoTerm::zoomOut() { qreal s = m_config.font.pointSizeF(); if (s <= 0) { s = m_config.font.pointSize(); } if (s > 1.0) { m_config.font.setPointSizeF(s - 1.0); m_config.font.setStyleStrategy(m_config.textAntialiasing ? QFont::PreferAntialias : QFont::NoAntialias); setFont(m_config.font); updateTerminalSize(); update(); } } void KodoTerm::resetZoom() { m_config.font.setPointSize(10); m_config.font.setStyleStrategy(m_config.textAntialiasing ? QFont::PreferAntialias : QFont::NoAntialias); setFont(m_config.font); updateTerminalSize(); update(); } QString KodoTerm::foregroundProcessName() const { return m_pty ? m_pty->foregroundProcessName() : QString(); } bool KodoTerm::isRoot() const { return m_pty && m_pty->isRoot(); } QColor KodoTerm::mapColor(const VTermColor &c, const VTermState *s) const { if (VTERM_COLOR_IS_RGB(&c)) { if (c.rgb.red == m_lastVTermFg.rgb.red && c.rgb.green == m_lastVTermFg.rgb.green && c.rgb.blue == m_lastVTermFg.rgb.blue) { return m_lastFg; } if (c.rgb.red == m_lastVTermBg.rgb.red && c.rgb.green == m_lastVTermBg.rgb.green && c.rgb.blue == m_lastVTermBg.rgb.blue) { return m_lastBg; } QColor col(c.rgb.red, c.rgb.green, c.rgb.blue); m_lastVTermBg = m_lastVTermFg; m_lastBg = m_lastFg; m_lastVTermFg = c; m_lastFg = col; return col; } else if (VTERM_COLOR_IS_INDEXED(&c)) { uint8_t i = c.indexed.idx; if (!m_paletteCacheValid[i]) { VTermColor rgb = c; vterm_state_convert_color_to_rgb(s, &rgb); m_paletteCache[i] = QColor(rgb.rgb.red, rgb.rgb.green, rgb.rgb.blue); m_paletteCacheValid[i] = true; } return m_paletteCache[i]; } return Qt::white; } void KodoTerm::paintEvent(QPaintEvent *e) { QElapsedTimer timer; timer.start(); // Ensure backbuffer resolution matches current screen resolution if (!m_backBuffer.isNull() && m_backBuffer.devicePixelRatio() != devicePixelRatioF()) { updateTerminalSize(); } if (m_dirty && !m_restoring) { renderToBackbuffer(); } QPainter painter(this); // Fill background to cover gaps if (m_vterm) { VTermState *state = vterm_obtain_state(m_vterm); VTermColor dfg, dbg; vterm_state_get_default_colors(state, &dfg, &dbg); painter.fillRect(rect(), mapColor(dbg, state)); } if (!m_restoring && !m_backBuffer.isNull()) { painter.setRenderHint(QPainter::Antialiasing, false); painter.drawImage(0, 0, m_backBuffer); } if (m_restorationBannerActive) { drawRestorationBanner(painter); } if (m_restoring) { return; } int sb = (int)m_scrollback.size(), cur = m_scrollBar->value(); if (hasFocus() && m_cursorVisible && cur == sb && (!m_cursorBlink || m_cursorBlinkState)) { QRect r(m_cursorCol * m_cellSize.width(), m_cursorRow * m_cellSize.height(), m_cellSize.width(), m_cellSize.height()); painter.setCompositionMode(QPainter::CompositionMode_Difference); switch (m_cursorShape) { case 2: case 3: painter.fillRect(r.x(), r.y() + r.height() - 2, r.width(), 2, Qt::white); break; case 4: case 5: painter.fillRect(r.x(), r.y(), 2, r.height(), Qt::white); break; case 1: default: painter.fillRect(r, Qt::white); break; } painter.setCompositionMode(QPainter::CompositionMode_SourceOver); } if (m_visualBellActive) { painter.setCompositionMode(QPainter::CompositionMode_Difference); painter.fillRect(rect(), Qt::white); painter.setCompositionMode(QPainter::CompositionMode_SourceOver); } if (m_flowControlStopped) { QString m = tr("Terminal stopped (Ctrl+S). Press Ctrl+Q to resume."); QFont f = font(); f.setBold(true); painter.setFont(f); painter.setRenderHint(QPainter::TextAntialiasing, m_config.textAntialiasing); painter.setRenderHint(QPainter::Antialiasing, m_config.textAntialiasing); QFontMetrics fm(f); QRect r = fm.boundingRect(m).adjusted(-5, -2, 5, 2); r.moveCenter(QPoint(width() / 2, r.height() / 2 + 10)); painter.fillRect(r, Qt::yellow); painter.setPen(Qt::black); painter.drawText(r, Qt::AlignCenter, m); } qint64 ns = timer.nsecsElapsed(); double ms = ns / 1000000.0; m_avgDrawTime = (m_avgDrawTime == 0.0) ? ms : (m_avgDrawTime * 0.9 + ms * 0.1); /* auto r = rect().adjusted(0, 0, -5, -5); painter.setPen(Qt::red); //painter.fillRect(r, Qt::yellow); painter.setCompositionMode(QPainter::CompositionMode_SourceOver); painter.drawText(r, Qt::AlignBottom | Qt::AlignRight, QString::number(m_avgDrawTime, 'f', 2) + " ms"); qDebug() << "Average draw time" << QString::number(m_avgDrawTime, 'f', 2); */ } void KodoTerm::keyPressEvent(QKeyEvent *e) { VTermModifier m = VTERM_MOD_NONE; if (e->modifiers() & Qt::ShiftModifier) { m = (VTermModifier)(m | VTERM_MOD_SHIFT); } if (e->modifiers() & Qt::ControlModifier) { m = (VTermModifier)(m | VTERM_MOD_CTRL); } if (e->modifiers() & Qt::AltModifier) { m = (VTermModifier)(m | VTERM_MOD_ALT); } int k = e->key(); if (k >= Qt::Key_F1 && k <= Qt::Key_F12) { vterm_keyboard_key(m_vterm, (VTermKey)(VTERM_KEY_FUNCTION(1 + k - Qt::Key_F1)), m); } else { switch (k) { case Qt::Key_Enter: case Qt::Key_Return: vterm_keyboard_key(m_vterm, VTERM_KEY_ENTER, m); break; case Qt::Key_Backspace: vterm_keyboard_key(m_vterm, VTERM_KEY_BACKSPACE, m); break; case Qt::Key_Tab: vterm_keyboard_key(m_vterm, VTERM_KEY_TAB, m); break; case Qt::Key_Escape: vterm_keyboard_key(m_vterm, VTERM_KEY_ESCAPE, m); break; case Qt::Key_Up: vterm_keyboard_key(m_vterm, VTERM_KEY_UP, m); break; case Qt::Key_Down: vterm_keyboard_key(m_vterm, VTERM_KEY_DOWN, m); break; case Qt::Key_Left: vterm_keyboard_key(m_vterm, VTERM_KEY_LEFT, m); break; case Qt::Key_Right: vterm_keyboard_key(m_vterm, VTERM_KEY_RIGHT, m); break; case Qt::Key_PageUp: if (e->modifiers() & Qt::ShiftModifier) { pageUp(); } else { vterm_keyboard_key(m_vterm, VTERM_KEY_PAGEUP, m); } break; case Qt::Key_PageDown: if (e->modifiers() & Qt::ShiftModifier) { pageDown(); } else { vterm_keyboard_key(m_vterm, VTERM_KEY_PAGEDOWN, m); } break; case Qt::Key_Home: if (e->modifiers() & Qt::ShiftModifier) { m_scrollBar->setValue(m_scrollBar->minimum()); } else { vterm_keyboard_key(m_vterm, VTERM_KEY_HOME, m); } break; case Qt::Key_End: if (e->modifiers() & Qt::ShiftModifier) { m_scrollBar->setValue(m_scrollBar->maximum()); } else { vterm_keyboard_key(m_vterm, VTERM_KEY_END, m); } break; case Qt::Key_Insert: vterm_keyboard_key(m_vterm, VTERM_KEY_INS, m); break; case Qt::Key_Delete: vterm_keyboard_key(m_vterm, VTERM_KEY_DEL, m); break; default: if (e->modifiers() & Qt::ControlModifier) { if (k == Qt::Key_Plus || k == Qt::Key_Equal) { zoomIn(); return; } else if (k == Qt::Key_Minus) { zoomOut(); return; } else if (k == Qt::Key_0) { resetZoom(); return; } } if ((e->modifiers() & Qt::ControlModifier) && (e->modifiers() & Qt::ShiftModifier)) { if (k == Qt::Key_C) { copyToClipboard(); return; } else if (k == Qt::Key_V) { pasteFromClipboard(); return; } } if ((m & VTERM_MOD_CTRL) && k >= Qt::Key_A && k <= Qt::Key_Z) { if (k == Qt::Key_S) { m_flowControlStopped = true; update(); } else if (k == Qt::Key_Q) { m_flowControlStopped = false; update(); } vterm_keyboard_unichar(m_vterm, k - Qt::Key_A + 1, VTERM_MOD_NONE); } else if (!e->text().isEmpty()) { for (const QChar &qc : e->text()) { vterm_keyboard_unichar(m_vterm, qc.unicode(), m); } } break; } } } bool KodoTerm::focusNextPrevChild(bool n) { return false; } void KodoTerm::focusInEvent(QFocusEvent *e) { QWidget::focusInEvent(e); m_cursorBlinkState = true; m_cursorBlinkTimer->start(); } void KodoTerm::focusOutEvent(QFocusEvent *e) { QWidget::focusOutEvent(e); m_cursorBlinkTimer->stop(); } void KodoTerm::populateThemeMenu(QMenu *pM, const QString &t, TerminalTheme::ThemeFormat f, const std::function &c) { QList ths = TerminalTheme::builtInThemes(); QList fT; for (const auto &theme : ths) { if (theme.format == f) { fT.append(theme); } } if (fT.isEmpty()) { return; } QMenu *mT = pM->addMenu(t); auto aTA = [&](QMenu *m, const TerminalTheme::ThemeInfo &i) { m->addAction(i.name, [c, i]() { c(i); }); }; if (fT.size() < 26) { for (const auto &i : fT) { aTA(mT, i); } } else { QMap sM; for (const auto &i : fT) { QChar fLC = i.name.isEmpty() ? QChar('#') : i.name[0].toUpper(); if (!fLC.isLetter()) { fLC = QChar('#'); } QString fL(fLC); if (!sM.contains(fL)) { sM[fL] = mT->addMenu(fL); } aTA(sM[fL], i); } } } static QDataStream &operator<<(QDataStream &out, const VTermColor &c) { out << (quint8)c.type; if (c.type == VTERM_COLOR_RGB) { out << c.rgb.red << c.rgb.green << c.rgb.blue; } else if (c.type == VTERM_COLOR_INDEXED) { out << c.indexed.idx; } return out; } static QDataStream &operator>>(QDataStream &in, VTermColor &c) { quint8 t; in >> t; c.type = (VTermColorType)t; if (c.type == VTERM_COLOR_RGB) { in >> c.rgb.red >> c.rgb.green >> c.rgb.blue; } else if (c.type == VTERM_COLOR_INDEXED) { in >> c.indexed.idx; } return in; } void KodoTerm::saveState(const QString &path) { QFile f(path); if (!f.open(QIODevice::WriteOnly)) { return; } QDataStream out(&f); out << (quint32)0x4B4F444F; // "KODO" out << (quint32)3; // Version 3 out << (quint32)m_cursorRow << (quint32)m_cursorCol; out << (quint32)m_scrollback.size(); for (const auto &line : m_scrollback) { out << (quint32)line.size(); if (!line.empty()) { out.writeRawData((const char *)line.data(), line.size() * sizeof(SavedCell)); } } // Save current screen int rows, cols; vterm_get_size(m_vterm, &rows, &cols); out << (quint32)rows << (quint32)cols; for (int r = 0; r < rows; ++r) { for (int c = 0; c < cols; ++c) { VTermScreenCell cell; vterm_screen_get_cell(m_vtermScreen, {r, c}, &cell); // Save as SavedCell for consistency/speed? // VTermScreenCell and SavedCell are slightly different. // SavedCell has fixed char array. VTermScreenCell has uint32_t // chars[VTERM_MAX_CHARS_PER_CELL]. SavedCell definition: uint32_t // chars[VTERM_MAX_CHARS_PER_CELL]; ... They are structurally nearly identical. Let's // stick to safe member-wise save for screen (it's small, 25x80). out.writeRawData((const char *)cell.chars, sizeof(cell.chars)); quint32 attrs = 0; memcpy(&attrs, &cell.attrs, std::min(sizeof(attrs), sizeof(cell.attrs))); out << attrs; out << cell.fg << cell.bg << (quint32)cell.width; } } } void KodoTerm::loadState(const QString &path) { m_restoring = true; QFile f(path); if (!f.open(QIODevice::ReadOnly)) { m_restoring = false; return; } QDataStream in(&f); quint32 magic, ver; in >> magic >> ver; if (magic != 0x4B4F444F) { m_restoring = false; return; } quint32 cR = 0, cC = 0; if (ver >= 2) { in >> cR >> cC; } quint32 sbSize; in >> sbSize; m_scrollback.clear(); // Fast path for Version 3+ if (ver >= 3) { for (quint32 i = 0; i < sbSize; ++i) { quint32 lineSize; in >> lineSize; SavedLine line; line.resize(lineSize); if (lineSize > 0) { in.readRawData((char *)line.data(), lineSize * sizeof(SavedCell)); } m_scrollback.push_back(std::move(line)); } } else { // Slow path for older versions for (quint32 i = 0; i < sbSize; ++i) { quint32 lineSize; in >> lineSize; SavedLine line; line.resize(lineSize); for (quint32 j = 0; j < lineSize; ++j) { in.readRawData((char *)line[j].chars, sizeof(line[j].chars)); quint32 attrs; in >> attrs; memcpy(&line[j].attrs, &attrs, std::min(sizeof(attrs), sizeof(line[j].attrs))); in >> line[j].fg >> line[j].bg; quint32 width; in >> width; line[j].width = width; } m_scrollback.push_back(std::move(line)); } } // Load screen lines and replay to vterm quint32 rows, cols; in >> rows >> cols; QByteArray replayData; for (quint32 r = 0; r < rows; ++r) { for (quint32 c = 0; c < cols; ++c) { SavedCell cell; in.readRawData((char *)cell.chars, sizeof(cell.chars)); quint32 attrs; in >> attrs; in >> cell.fg >> cell.bg; quint32 width; in >> width; // Replay Logic if (cell.chars[0] == 0) { replayData.append(' '); } else { if (cell.fg.type == VTERM_COLOR_RGB) { replayData.append(QString("\033[38;2;%1;%2;%3m") .arg(cell.fg.rgb.red) .arg(cell.fg.rgb.green) .arg(cell.fg.rgb.blue) .toUtf8()); } if (cell.bg.type == VTERM_COLOR_RGB) { replayData.append(QString("\033[48;2;%1;%2;%3m") .arg(cell.bg.rgb.red) .arg(cell.bg.rgb.green) .arg(cell.bg.rgb.blue) .toUtf8()); } int n = 0; while (n < VTERM_MAX_CHARS_PER_CELL && cell.chars[n]) { n++; } if (n > 0) { replayData.append(QString::fromUcs4((const char32_t *)cell.chars, n).toUtf8()); } } if (width > 1) { c += (width - 1); } } replayData.append("\r\n"); } replayData.append("\033[0m"); if (ver >= 2) { replayData.append(QString("\033[%1;%2H").arg(cR + 1).arg(cC + 1).toUtf8()); } vterm_input_write(m_vterm, replayData.constData(), replayData.size()); m_scrollBar->setRange(0, (int)m_scrollback.size()); m_scrollBar->setValue(m_scrollBar->maximum()); damageAll(); m_restoring = false; update(); }