Integrate KodoTerm for SSH terminal sessions
This commit is contained in:
1847
third_party/KodoTerm/src/KodoTerm.cpp
vendored
Normal file
1847
third_party/KodoTerm/src/KodoTerm.cpp
vendored
Normal file
File diff suppressed because it is too large
Load Diff
456
third_party/KodoTerm/src/KodoTermConfig.cpp
vendored
Normal file
456
third_party/KodoTerm/src/KodoTermConfig.cpp
vendored
Normal 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
27
third_party/KodoTerm/src/PtyProcess.cpp
vendored
Normal 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
54
third_party/KodoTerm/src/PtyProcess.h
vendored
Normal 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();
|
||||
};
|
||||
184
third_party/KodoTerm/src/PtyProcess_unix.cpp
vendored
Normal file
184
third_party/KodoTerm/src/PtyProcess_unix.cpp
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
32
third_party/KodoTerm/src/PtyProcess_unix.h
vendored
Normal file
32
third_party/KodoTerm/src/PtyProcess_unix.h
vendored
Normal 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;
|
||||
};
|
||||
229
third_party/KodoTerm/src/PtyProcess_win.cpp
vendored
Normal file
229
third_party/KodoTerm/src/PtyProcess_win.cpp
vendored
Normal 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); }
|
||||
36
third_party/KodoTerm/src/PtyProcess_win.h
vendored
Normal file
36
third_party/KodoTerm/src/PtyProcess_win.h
vendored
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user