Internal
Public Access
1848 lines
59 KiB
C++
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();
|
|
}
|