Files
orbithub/third_party/KodoTerm/src/KodoTerm.cpp
T

1848 lines
59 KiB
C++

// SPDX-License-Identifier: MIT
// Author: Diego Iastrubni <diegoiast@gmail.com>
#include "KodoTerm/KodoTerm.hpp"
#include "PtyProcess.h"
#include <vterm.h>
#include <QApplication>
#include <QBuffer>
#include <QClipboard>
#include <QDataStream>
#include <QDateTime>
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QKeyEvent>
#include <QMap>
#include <QMenu>
#include <QMouseEvent>
#include <QPainter>
#include <QRegularExpression>
#include <QSettings>
#include <QTextStream>
#include <QUrl>
#include <algorithm>
#include <cstring>
static void vterm_output_callback(const char *s, size_t len, void *user) {
auto *pty = static_cast<PtyProcess *>(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<KodoTerm *>(user)->pushScrollback(cols, cells);
}
int KodoTerm::onSbPopLine(int cols, VTermScreenCell *cells, void *user) {
return static_cast<KodoTerm *>(user)->popScrollback(cols, cells);
}
int KodoTerm::onOsc(int command, VTermStringFragment frag, void *user) {
auto *w = static_cast<KodoTerm *>(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<KodoTerm *>(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<KodoTerm *>(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<KodoTerm *>(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<KodoTerm *>(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<KodoTerm *>(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<ushort>(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<ushort>(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<int>(m.capturedStart())};
m_selectionEnd = {vp.row, static_cast<int>(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<void(const TerminalTheme::ThemeInfo &)> &c) {
QList<TerminalTheme::ThemeInfo> ths = TerminalTheme::builtInThemes();
QList<TerminalTheme::ThemeInfo> 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<QString, QMenu *> 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();
}