Integrate KodoTerm for SSH terminal sessions

This commit is contained in:
Keith Smith
2026-03-01 10:36:06 -07:00
parent c3369b8e48
commit 776ddc1a53
503 changed files with 22870 additions and 95 deletions

1847
third_party/KodoTerm/src/KodoTerm.cpp vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,456 @@
// SPDX-License-Identifier: MIT
// Author: Diego Iastrubni <diegoiast@gmail.com>
#include "KodoTerm/KodoTermConfig.hpp"
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMap>
#include <QSettings>
#include <QStandardPaths>
#include <QTextStream>
#include <QXmlStreamReader>
TerminalTheme TerminalTheme::defaultTheme() {
return {"Default",
QColor(170, 170, 170),
QColor(0, 0, 0),
{QColor(0, 0, 0), QColor(170, 0, 0), QColor(0, 170, 0), QColor(170, 85, 0),
QColor(0, 0, 170), QColor(170, 0, 170), QColor(0, 170, 170), QColor(170, 170, 170),
QColor(85, 85, 85), QColor(255, 85, 85), QColor(85, 255, 85), QColor(255, 255, 85),
QColor(85, 85, 255), QColor(255, 85, 255), QColor(85, 255, 255),
QColor(255, 255, 255)}};
}
TerminalTheme TerminalTheme::loadTheme(const QString &path) {
if (path.endsWith(".colorscheme")) {
return loadKonsoleTheme(path);
} else if (path.endsWith(".itermcolors")) {
return loadITermTheme(path);
} else if (path.endsWith(".json")) {
return loadWindowsTerminalTheme(path);
}
// Fallback: try to guess content or return default
return defaultTheme();
}
TerminalTheme TerminalTheme::loadKonsoleTheme(const QString &path) {
TerminalTheme theme = defaultTheme();
QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return theme;
}
theme.name = QFileInfo(path).baseName();
auto parseColor = [](const QString &s) -> QColor {
QStringList parts = s.split(',');
if (parts.size() >= 3) {
return QColor(parts[0].toInt(), parts[1].toInt(), parts[2].toInt());
}
return QColor();
};
QMap<QString, QMap<QString, QString>> sections;
QString currentSection;
QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
if (line.isEmpty() || line.startsWith(';')) {
continue;
}
if (line.startsWith('[') && line.endsWith(']')) {
currentSection = line.mid(1, line.length() - 2);
} else if (!currentSection.isEmpty()) {
int eq = line.indexOf('=');
if (eq != -1) {
QString key = line.left(eq).trimmed();
QString value = line.mid(eq + 1).trimmed();
sections[currentSection][key] = value;
}
}
}
if (sections.contains("General") && sections["General"].contains("Description")) {
theme.name = sections["General"]["Description"];
}
QColor fg = parseColor(sections["Foreground"]["Color"]);
if (fg.isValid()) {
theme.foreground = fg;
}
QColor bg = parseColor(sections["Background"]["Color"]);
if (bg.isValid()) {
theme.background = bg;
}
for (int i = 0; i < 16; ++i) {
QString section = QString("Color%1%2").arg(i % 8).arg(i >= 8 ? "Intense" : "");
QColor c = parseColor(sections[section]["Color"]);
if (c.isValid()) {
theme.palette[i] = c;
}
}
return theme;
}
TerminalTheme TerminalTheme::loadWindowsTerminalTheme(const QString &path) {
TerminalTheme theme = defaultTheme();
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
return theme;
}
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
QJsonObject obj = doc.object();
if (obj.contains("name")) {
theme.name = obj.value("name").toString();
}
if (obj.contains("foreground")) {
theme.foreground = QColor(obj.value("foreground").toString());
}
if (obj.contains("background")) {
theme.background = QColor(obj.value("background").toString());
}
QStringList keys = {"black", "red", "green", "yellow",
"blue", "purple", "cyan", "white",
"brightBlack", "brightRed", "brightGreen", "brightYellow",
"brightBlue", "brightPurple", "brightCyan", "brightWhite"};
for (int i = 0; i < 16; ++i) {
if (obj.contains(keys[i])) {
QColor c(obj.value(keys[i]).toString());
if (c.isValid()) {
theme.palette[i] = c;
}
}
}
return theme;
}
TerminalTheme TerminalTheme::loadITermTheme(const QString &path) {
TerminalTheme theme = defaultTheme();
theme.name = QFileInfo(path).baseName();
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
return theme;
}
QXmlStreamReader xml(&file);
if (xml.readNextStartElement() && xml.name() == QLatin1String("plist")) {
if (xml.readNextStartElement() && xml.name() == QLatin1String("dict")) {
while (xml.readNextStartElement()) {
if (xml.name() == QLatin1String("key")) {
QString keyName = xml.readElementText();
if (xml.readNextStartElement()) {
if (xml.name() == QLatin1String("dict")) {
double red = 0.0, green = 0.0, blue = 0.0;
while (xml.readNextStartElement()) {
if (xml.name() == QLatin1String("key")) {
QString componentKey = xml.readElementText();
if (xml.readNextStartElement()) {
if (xml.name() == QLatin1String("real")) {
double val = xml.readElementText().toDouble();
if (componentKey == "Red Component") {
red = val;
} else if (componentKey == "Green Component") {
green = val;
} else if (componentKey == "Blue Component") {
blue = val;
}
} else {
xml.skipCurrentElement();
}
}
} else {
xml.skipCurrentElement();
}
}
QColor color;
color.setRgbF(red, green, blue);
if (keyName == "Background Color") {
theme.background = color;
} else if (keyName == "Foreground Color") {
theme.foreground = color;
} else if (keyName.startsWith("Ansi ") && keyName.endsWith(" Color")) {
int index = keyName.mid(5, keyName.length() - 11).toInt();
if (index >= 0 && index < 16) {
theme.palette[index] = color;
}
}
} else {
xml.skipCurrentElement();
}
}
} else {
xml.skipCurrentElement();
}
}
}
}
return theme;
}
QList<TerminalTheme::ThemeInfo> TerminalTheme::builtInThemes() {
Q_INIT_RESOURCE(KodoTermThemes);
QList<ThemeInfo> themes;
QDirIterator it(":/KodoTermThemes",
QStringList() << "*.colorscheme" << "*.json" << "*.itermcolors", QDir::Files,
QDirIterator::Subdirectories);
while (it.hasNext()) {
it.next();
ThemeInfo info;
info.path = it.filePath();
if (info.path.endsWith(".colorscheme")) {
info.format = ThemeFormat::Konsole;
QSettings settings(info.path, QSettings::IniFormat);
info.name = settings.value("General/Description", it.fileName()).toString();
} else if (info.path.endsWith(".itermcolors")) {
info.format = ThemeFormat::ITerm;
info.name = QFileInfo(info.path).baseName();
// Optional: Parse the file to find "Name" comment if available, but filename is usually
// good enough.
} else {
info.format = ThemeFormat::WindowsTerminal;
QFile file(info.path);
if (file.open(QIODevice::ReadOnly)) {
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
info.name = doc.object().value("name").toString();
}
if (info.name.isEmpty()) {
info.name = it.fileName();
}
}
themes.append(info);
}
std::sort(themes.begin(), themes.end(), [](const ThemeInfo &a, const ThemeInfo &b) {
return a.name.compare(b.name, Qt::CaseInsensitive) < 0;
});
return themes;
}
QJsonObject TerminalTheme::toJson() const {
QJsonObject obj;
obj["name"] = name;
obj["foreground"] = foreground.name();
obj["background"] = background.name();
QJsonArray paletteArray;
for (const auto &c : palette) {
paletteArray.append(c.name());
}
obj["palette"] = paletteArray;
return obj;
}
TerminalTheme TerminalTheme::fromJson(const QJsonObject &json) {
TerminalTheme theme = defaultTheme();
if (json.contains("name")) {
theme.name = json["name"].toString();
}
if (json.contains("foreground")) {
theme.foreground = QColor(json["foreground"].toString());
}
if (json.contains("background")) {
theme.background = QColor(json["background"].toString());
}
if (json.contains("palette") && json["palette"].isArray()) {
QJsonArray arr = json["palette"].toArray();
for (int i = 0; i < std::min(16, (int)arr.size()); ++i) {
theme.palette[i] = QColor(arr[i].toString());
}
}
return theme;
}
void TerminalTheme::save(QSettings &settings, const QString &group) const {
if (!group.isEmpty()) {
settings.beginGroup(group);
}
settings.setValue("name", name);
settings.setValue("foreground", foreground.name());
settings.setValue("background", background.name());
QStringList paletteList;
for (const auto &c : palette) {
paletteList << c.name();
}
settings.setValue("palette", paletteList);
if (!group.isEmpty()) {
settings.endGroup();
}
}
void TerminalTheme::load(QSettings &settings, const QString &group) {
if (!group.isEmpty()) {
settings.beginGroup(group);
}
name = settings.value("name", "Default").toString();
foreground = QColor(settings.value("foreground", "#aaaaaa").toString());
background = QColor(settings.value("background", "#000000").toString());
QStringList paletteList = settings.value("palette").toStringList();
if (paletteList.size() >= 16) {
for (int i = 0; i < 16; ++i) {
palette[i] = QColor(paletteList[i]);
}
} else {
*this = defaultTheme(); // Fallback if palette is incomplete or missing
// Re-apply overrides if any
if (settings.contains("foreground")) {
foreground = QColor(settings.value("foreground").toString());
}
if (settings.contains("background")) {
background = QColor(settings.value("background").toString());
}
}
if (!group.isEmpty()) {
settings.endGroup();
}
}
KodoTermConfig::KodoTermConfig(QSettings &settings) {
setDefaults();
load(settings);
}
KodoTermConfig::KodoTermConfig() { setDefaults(); }
void KodoTermConfig::setDefaults() {
font = QFont("Monospace", 10);
font.setStyleHint(QFont::Monospace);
font.setKerning(false);
textAntialiasing = false;
font.setStyleStrategy(QFont::NoAntialias);
customBoxDrawing = false;
copyOnSelect = true;
pasteOnMiddleClick = true;
mouseWheelZoom = true;
visualBell = true;
audibleBell = true;
tripleClickSelectsLine = true;
enableLogging = true;
logDirectory =
QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/KodoShell";
wordSelectionRegex = "[a-zA-Z0-9_\\.\\-\\/~\\:]+";
maxScrollback = 1000;
theme = TerminalTheme::defaultTheme();
}
void KodoTermConfig::load(const QJsonObject &json) {
if (json.contains("font")) {
QJsonObject fontObj = json["font"].toObject();
font.setFamily(fontObj["family"].toString());
font.setPointSizeF(fontObj["size"].toDouble());
}
if (json.contains("textAntialiasing")) {
textAntialiasing = json["textAntialiasing"].toBool();
}
font.setKerning(false);
font.setStyleStrategy(textAntialiasing ? QFont::PreferAntialias : QFont::NoAntialias);
if (json.contains("customBoxDrawing")) {
customBoxDrawing = json["customBoxDrawing"].toBool();
}
if (json.contains("copyOnSelect")) {
copyOnSelect = json["copyOnSelect"].toBool();
}
if (json.contains("pasteOnMiddleClick")) {
pasteOnMiddleClick = json["pasteOnMiddleClick"].toBool();
}
if (json.contains("mouseWheelZoom")) {
mouseWheelZoom = json["mouseWheelZoom"].toBool();
}
if (json.contains("visualBell")) {
visualBell = json["visualBell"].toBool();
}
if (json.contains("audibleBell")) {
audibleBell = json["audibleBell"].toBool();
}
if (json.contains("tripleClickSelectsLine")) {
tripleClickSelectsLine = json["tripleClickSelectsLine"].toBool();
}
if (json.contains("enableLogging")) {
enableLogging = json["enableLogging"].toBool();
}
if (json.contains("logDirectory")) {
logDirectory = json["logDirectory"].toString();
}
if (json.contains("wordSelectionRegex")) {
wordSelectionRegex = json["wordSelectionRegex"].toString();
}
if (json.contains("maxScrollback")) {
maxScrollback = json["maxScrollback"].toInt();
}
if (json.contains("theme")) {
theme = TerminalTheme::fromJson(json["theme"].toObject());
}
}
QJsonObject KodoTermConfig::saveToJson() const {
QJsonObject obj;
QJsonObject fontObj;
fontObj["family"] = font.family();
fontObj["size"] = font.pointSizeF();
obj["font"] = fontObj;
obj["textAntialiasing"] = textAntialiasing;
obj["customBoxDrawing"] = customBoxDrawing;
obj["copyOnSelect"] = copyOnSelect;
obj["pasteOnMiddleClick"] = pasteOnMiddleClick;
obj["mouseWheelZoom"] = mouseWheelZoom;
obj["visualBell"] = visualBell;
obj["audibleBell"] = audibleBell;
obj["tripleClickSelectsLine"] = tripleClickSelectsLine;
obj["enableLogging"] = enableLogging;
obj["logDirectory"] = logDirectory;
obj["wordSelectionRegex"] = wordSelectionRegex;
obj["maxScrollback"] = maxScrollback;
obj["theme"] = theme.toJson();
return obj;
}
void KodoTermConfig::load(QSettings &settings) {
if (settings.contains("font/family")) {
font.setFamily(settings.value("font/family").toString());
font.setPointSizeF(settings.value("font/size", 10).toDouble());
}
textAntialiasing = settings.value("textAntialiasing", textAntialiasing).toBool();
font.setKerning(false);
font.setStyleStrategy(textAntialiasing ? QFont::PreferAntialias : QFont::NoAntialias);
customBoxDrawing = settings.value("customBoxDrawing", customBoxDrawing).toBool();
copyOnSelect = settings.value("copyOnSelect", copyOnSelect).toBool();
pasteOnMiddleClick = settings.value("pasteOnMiddleClick", pasteOnMiddleClick).toBool();
mouseWheelZoom = settings.value("mouseWheelZoom", mouseWheelZoom).toBool();
visualBell = settings.value("visualBell", visualBell).toBool();
audibleBell = settings.value("audibleBell", audibleBell).toBool();
tripleClickSelectsLine =
settings.value("tripleClickSelectsLine", tripleClickSelectsLine).toBool();
enableLogging = settings.value("enableLogging", enableLogging).toBool();
logDirectory = settings.value("logDirectory", logDirectory).toString();
wordSelectionRegex = settings.value("wordSelectionRegex", wordSelectionRegex).toString();
maxScrollback = settings.value("maxScrollback", maxScrollback).toInt();
theme.load(settings, "Theme");
}
void KodoTermConfig::save(QSettings &settings) const {
settings.setValue("font/family", font.family());
settings.setValue("font/size", font.pointSizeF());
settings.setValue("textAntialiasing", textAntialiasing);
settings.setValue("customBoxDrawing", customBoxDrawing);
settings.setValue("copyOnSelect", copyOnSelect);
settings.setValue("pasteOnMiddleClick", pasteOnMiddleClick);
settings.setValue("mouseWheelZoom", mouseWheelZoom);
settings.setValue("visualBell", visualBell);
settings.setValue("audibleBell", audibleBell);
settings.setValue("tripleClickSelectsLine", tripleClickSelectsLine);
settings.setValue("enableLogging", enableLogging);
settings.setValue("logDirectory", logDirectory);
settings.setValue("wordSelectionRegex", wordSelectionRegex);
settings.setValue("maxScrollback", maxScrollback);
theme.save(settings, "Theme");
}

27
third_party/KodoTerm/src/PtyProcess.cpp vendored Normal file
View File

@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
// Author: Diego Iastrubni <diegoiast@gmail.com>
#include "PtyProcess.h"
#include <QtGlobal>
#if defined(Q_OS_UNIX)
#include "PtyProcess_unix.h"
#elif defined(Q_OS_WIN)
#include "PtyProcess_win.h"
#endif
bool PtyProcess::start(const QString &program, const QStringList &arguments, const QSize &size) {
setProgram(program);
setArguments(arguments);
return start(size);
}
PtyProcess *PtyProcess::create(QObject *parent) {
#if defined(Q_OS_UNIX)
return new PtyProcessUnix(parent);
#elif defined(Q_OS_WIN)
return new PtyProcessWin(parent);
#else
return nullptr;
#endif
}

54
third_party/KodoTerm/src/PtyProcess.h vendored Normal file
View File

@@ -0,0 +1,54 @@
// SPDX-License-Identifier: MIT
// Author: Diego Iastrubni <diegoiast@gmail.com>
#pragma once
#include <QObject>
#include <QProcessEnvironment>
#include <QSize>
#include <QStringList>
class PtyProcess : public QObject {
Q_OBJECT
public:
explicit PtyProcess(QObject *parent = nullptr) : QObject(parent) {}
virtual ~PtyProcess() = default;
void setProgram(const QString &program) { m_program = program; }
QString program() const { return m_program; }
void setArguments(const QStringList &arguments) { m_arguments = arguments; }
QStringList arguments() const { return m_arguments; }
void setWorkingDirectory(const QString &workingDirectory) {
m_workingDirectory = workingDirectory;
}
QString workingDirectory() const { return m_workingDirectory; }
void setProcessEnvironment(const QProcessEnvironment &environment) {
m_environment = environment;
}
QProcessEnvironment processEnvironment() const { return m_environment; }
virtual bool start(const QSize &size) = 0;
virtual bool start(const QString &program, const QStringList &arguments, const QSize &size);
virtual void write(const QByteArray &data) = 0;
virtual void resize(const QSize &size) = 0;
virtual void kill() = 0;
virtual bool isRoot() const = 0;
virtual QString foregroundProcessName() const = 0;
// Factory method
static PtyProcess *create(QObject *parent = nullptr);
signals:
void readyRead(const QByteArray &data);
void finished(int exitCode, int exitStatus);
protected:
QString m_program;
QStringList m_arguments;
QString m_workingDirectory;
QProcessEnvironment m_environment = QProcessEnvironment::systemEnvironment();
};

View File

@@ -0,0 +1,184 @@
// SPDX-License-Identifier: MIT
// Author: Diego Iastrubni <diegoiast@gmail.com>
#include "PtyProcess_unix.h"
#include <QCoreApplication>
#include <QDebug>
#if defined(__APPLE__) || defined(__FreeBSD__)
#include <util.h>
#else
#include <pty.h>
#endif
#include <QFile>
#include <QFileInfo>
#include <cerrno>
#include <fcntl.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <termios.h>
#include <unistd.h>
PtyProcessUnix::PtyProcessUnix(QObject *parent) : PtyProcess(parent) {}
PtyProcessUnix::~PtyProcessUnix() {
kill();
if (m_masterFd >= 0) {
::close(m_masterFd);
m_masterFd = -1;
}
}
bool PtyProcessUnix::start(const QSize &size) {
if (m_program.isEmpty()) {
return false;
}
struct winsize ws;
ws.ws_row = (unsigned short)size.height();
ws.ws_col = (unsigned short)size.width();
ws.ws_xpixel = 0;
ws.ws_ypixel = 0;
pid_t pid = forkpty(&m_masterFd, nullptr, nullptr, &ws);
if (pid == -1) {
qWarning() << "Failed to forkpty";
return false;
}
if (pid == 0) {
// Child
// Working directory
if (!m_workingDirectory.isEmpty()) {
if (chdir(m_workingDirectory.toLocal8Bit().constData()) != 0) {
_exit(1);
}
}
// Environment
for (const auto &key : m_environment.keys()) {
setenv(key.toLocal8Bit().constData(),
m_environment.value(key).toLocal8Bit().constData(), 1);
}
setenv("TERM", "xterm-256color", 1);
// Convert args to char* array
std::vector<char *> args;
QByteArray progBytes = m_program.toLocal8Bit();
args.push_back(progBytes.data());
// Helper to keep storage alive
std::vector<QByteArray> storage;
storage.reserve(m_arguments.size());
for (const auto &arg : m_arguments) {
storage.push_back(arg.toLocal8Bit());
args.push_back(storage.back().data());
}
args.push_back(nullptr);
execvp(m_program.toLocal8Bit().constData(), args.data());
// If execvp returns, it failed
_exit(1);
} else {
// Parent
m_pid = pid;
m_notifier = new QSocketNotifier(m_masterFd, QSocketNotifier::Read, this);
connect(m_notifier, &QSocketNotifier::activated, this, &PtyProcessUnix::onReadyRead);
return true;
}
}
bool PtyProcessUnix::start(const QString &program, const QStringList &arguments,
const QSize &size) {
return PtyProcess::start(program, arguments, size);
}
void PtyProcessUnix::write(const QByteArray &data) {
if (m_masterFd >= 0) {
::write(m_masterFd, data.constData(), data.size());
}
}
void PtyProcessUnix::resize(const QSize &size) {
if (m_masterFd >= 0) {
struct winsize ws;
ws.ws_row = (unsigned short)size.height();
ws.ws_col = (unsigned short)size.width();
ws.ws_xpixel = 0;
ws.ws_ypixel = 0;
ioctl(m_masterFd, TIOCSWINSZ, &ws);
}
}
void PtyProcessUnix::kill() {
if (m_pid > 0) {
::kill(m_pid, SIGTERM);
// Wait? usually waitpid via signal handler, but for now just cleanup
m_pid = -1;
}
}
bool PtyProcessUnix::isRoot() const {
if (m_masterFd < 0) {
return false;
}
pid_t pgrp = tcgetpgrp(m_masterFd);
if (pgrp <= 0) {
// Fallback to initial pid
if (m_pid <= 0) {
return false;
}
pgrp = m_pid;
}
struct stat st;
char path[64];
snprintf(path, sizeof(path), "/proc/%d", (int)pgrp);
if (stat(path, &st) == 0) {
return st.st_uid == 0;
}
return false;
}
QString PtyProcessUnix::foregroundProcessName() const {
if (m_masterFd < 0) {
return QString();
}
pid_t pgrp = tcgetpgrp(m_masterFd);
if (pgrp <= 0) {
return QFileInfo(m_program).baseName();
}
char path[64];
snprintf(path, sizeof(path), "/proc/%d/comm", (int)pgrp);
QFile file(path);
if (file.open(QIODevice::ReadOnly)) {
return QString::fromUtf8(file.readAll().trimmed());
}
// Fallback to initial program
return QFileInfo(m_program).baseName();
}
void PtyProcessUnix::onReadyRead() {
char buffer[4096];
ssize_t len = ::read(m_masterFd, buffer, sizeof(buffer));
if (len > 0) {
emit readyRead(QByteArray(buffer, (int)len));
} else if (len < 0 && errno != EAGAIN) {
m_notifier->setEnabled(false);
emit finished(-1, -1); // Error
} else if (len == 0) {
m_notifier->setEnabled(false);
emit finished(0, 0); // EOF
}
}

View File

@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
// Author: Diego Iastrubni <diegoiast@gmail.com>
#pragma once
#include "PtyProcess.h"
#include <QSocketNotifier>
#include <sys/types.h>
class PtyProcessUnix : public PtyProcess {
Q_OBJECT
public:
PtyProcessUnix(QObject *parent = nullptr);
~PtyProcessUnix();
bool start(const QSize &size) override;
bool start(const QString &program, const QStringList &arguments, const QSize &size) override;
void write(const QByteArray &data) override;
void resize(const QSize &size) override;
void kill() override;
bool isRoot() const override;
QString foregroundProcessName() const override;
private slots:
void onReadyRead();
private:
int m_masterFd = -1;
pid_t m_pid = -1;
QSocketNotifier *m_notifier = nullptr;
};

View File

@@ -0,0 +1,229 @@
// SPDX-License-Identifier: MIT
// Author: Diego Iastrubni <diegoiast@gmail.com>
#include "PtyProcess_win.h"
#include <QDebug>
#include <QDir>
#include <vector>
// Define necessary types if building on older SDKs or mingw that might lack them
// But assuming standard modern environment.
class PtyProcessWin::ReaderThread : public QThread {
public:
ReaderThread(HANDLE hPipe, PtyProcessWin *parent) : m_hPipe(hPipe), m_parent(parent) {}
void run() override {
char buffer[4096];
DWORD bytesRead;
while (m_running) {
if (ReadFile(m_hPipe, buffer, sizeof(buffer), &bytesRead, NULL)) {
if (bytesRead > 0) {
QByteArray data(buffer, (int)bytesRead);
QMetaObject::invokeMethod(m_parent, "onReadThreadData", Qt::QueuedConnection,
Q_ARG(QByteArray, data));
} else {
// EOF (bytesRead == 0)
break;
}
} else {
DWORD err = GetLastError();
if (err == ERROR_BROKEN_PIPE || err == ERROR_HANDLE_EOF) {
break;
}
break;
}
}
}
void stop() { m_running = false; }
private:
HANDLE m_hPipe;
PtyProcessWin *m_parent;
bool m_running = true;
};
PtyProcessWin::PtyProcessWin(QObject *parent) : PtyProcess(parent) {
ZeroMemory(&m_pi, sizeof(PROCESS_INFORMATION));
m_hPC = INVALID_HANDLE_VALUE;
m_hPipeIn = INVALID_HANDLE_VALUE;
m_hPipeOut = INVALID_HANDLE_VALUE;
}
PtyProcessWin::~PtyProcessWin() {
kill(); // kill() now closes everything properly
}
bool PtyProcessWin::start(const QSize &size) {
if (m_program.isEmpty()) {
return false;
}
HANDLE hPipePTYIn = INVALID_HANDLE_VALUE;
HANDLE hPipePTYOut = INVALID_HANDLE_VALUE;
// Create pipes
if (!CreatePipe(&hPipePTYIn, &m_hPipeOut, NULL, 0)) {
return false;
}
if (!CreatePipe(&m_hPipeIn, &hPipePTYOut, NULL, 0)) {
return false;
}
// Create Pseudo Console
COORD origin = {(SHORT)size.width(), (SHORT)size.height()};
HRESULT hr = CreatePseudoConsole(origin, hPipePTYIn, hPipePTYOut, 0, &m_hPC);
// Close the sides we don't need
CloseHandle(hPipePTYIn);
CloseHandle(hPipePTYOut);
if (FAILED(hr)) {
return false;
}
// Prepare Startup Info
STARTUPINFOEX si;
ZeroMemory(&si, sizeof(STARTUPINFOEX));
si.StartupInfo.cb = sizeof(STARTUPINFOEX);
SIZE_T bytesRequired = 0;
InitializeProcThreadAttributeList(NULL, 1, 0, &bytesRequired);
si.lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, bytesRequired);
if (!si.lpAttributeList) {
return false;
}
if (!InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &bytesRequired)) {
return false;
}
if (!UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
m_hPC, sizeof(HPCON), NULL, NULL)) {
return false;
}
// Command Line
QString programNative = QDir::toNativeSeparators(m_program);
QString cmd = programNative;
if (cmd.contains(' ')) {
cmd = "\"" + cmd + "\"";
}
for (const auto &arg : m_arguments) {
cmd += " " + arg; // Simple quoting might be needed
}
// Working directory
wchar_t *pWorkingDirectory = nullptr;
std::vector<wchar_t> workingDir;
if (!m_workingDirectory.isEmpty()) {
workingDir.resize(m_workingDirectory.length() + 1);
m_workingDirectory.toWCharArray(workingDir.data());
workingDir[m_workingDirectory.length()] = 0;
pWorkingDirectory = workingDir.data();
}
// Environment
std::vector<wchar_t> envBlock;
for (const auto &key : m_environment.keys()) {
QString entry = key + "=" + m_environment.value(key);
for (QChar c : entry) {
envBlock.push_back(c.unicode());
}
envBlock.push_back(0);
}
envBlock.push_back(0);
// Create Process
std::vector<wchar_t> cmdLine(cmd.length() + 1);
cmd.toWCharArray(cmdLine.data());
cmdLine[cmd.length()] = 0;
BOOL success = CreateProcessW(NULL, cmdLine.data(), NULL, NULL, FALSE,
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT,
envBlock.data(), pWorkingDirectory, &si.StartupInfo, &m_pi);
// Cleanup attribute list
DeleteProcThreadAttributeList(si.lpAttributeList);
HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
if (!success) {
return false;
}
// Start reader thread
m_readerThread = new ReaderThread(m_hPipeIn, this);
m_readerThread->start();
return true;
}
bool PtyProcessWin::start(const QString &program, const QStringList &arguments, const QSize &size) {
return PtyProcess::start(program, arguments, size);
}
void PtyProcessWin::write(const QByteArray &data) {
if (m_hPipeOut != INVALID_HANDLE_VALUE) {
DWORD bytesWritten;
WriteFile(m_hPipeOut, data.constData(), data.size(), &bytesWritten, NULL);
}
}
void PtyProcessWin::resize(const QSize &size) {
if (m_hPC != INVALID_HANDLE_VALUE) {
COORD origin = {(SHORT)size.width(), (SHORT)size.height()};
ResizePseudoConsole(m_hPC, origin);
}
}
void PtyProcessWin::kill() {
// First, close the pseudo console — this is the key fix
// It immediately unblocks any pending ReadFile on the pipes
if (m_hPC != INVALID_HANDLE_VALUE) {
ClosePseudoConsole(m_hPC);
m_hPC = INVALID_HANDLE_VALUE;
}
// Stop and clean up reader thread
if (m_readerThread) {
m_readerThread->stop();
// Close our read handle as a safety net (though ClosePseudoConsole already unblocked)
if (m_hPipeIn != INVALID_HANDLE_VALUE) {
CloseHandle(m_hPipeIn);
m_hPipeIn = INVALID_HANDLE_VALUE;
}
m_readerThread->wait(3000); // give it a reasonable timeout
if (m_readerThread->isRunning()) {
m_readerThread->terminate(); // last resort
m_readerThread->wait();
}
delete m_readerThread;
m_readerThread = nullptr;
}
// Close write pipe
if (m_hPipeOut != INVALID_HANDLE_VALUE) {
CloseHandle(m_hPipeOut);
m_hPipeOut = INVALID_HANDLE_VALUE;
}
// Terminate child process if still running
if (m_pi.hProcess) {
TerminateProcess(m_pi.hProcess, 1);
WaitForSingleObject(m_pi.hProcess, 5000);
CloseHandle(m_pi.hProcess);
CloseHandle(m_pi.hThread);
m_pi.hProcess = NULL;
m_pi.hThread = NULL;
}
}
bool PtyProcessWin::isRoot() const {
// Could check for elevation here
return false;
}
QString PtyProcessWin::foregroundProcessName() const { return QFileInfo(m_program).baseName(); }
void PtyProcessWin::onReadThreadData(const QByteArray &data) { emit readyRead(data); }

View File

@@ -0,0 +1,36 @@
// SPDX-License-Identifier: MIT
// Author: Diego Iastrubni <diegoiast@gmail.com>
#pragma once
#include "PtyProcess.h"
#include <QThread>
#include <windows.h>
class PtyProcessWin : public PtyProcess {
Q_OBJECT
public:
explicit PtyProcessWin(QObject *parent = nullptr);
~PtyProcessWin() override;
bool start(const QSize &size) override;
bool start(const QString &program, const QStringList &arguments, const QSize &size) override;
void write(const QByteArray &data) override;
void resize(const QSize &size) override;
void kill() override;
bool isRoot() const override;
QString foregroundProcessName() const override;
private slots:
void onReadThreadData(const QByteArray &data);
private:
HPCON m_hPC = INVALID_HANDLE_VALUE;
HANDLE m_hPipeIn = INVALID_HANDLE_VALUE;
HANDLE m_hPipeOut = INVALID_HANDLE_VALUE;
PROCESS_INFORMATION m_pi;
class ReaderThread;
ReaderThread *m_readerThread = nullptr;
};