From e6bda0c239ca85b3b19c6462707c7d4a8ecdc33a Mon Sep 17 00:00:00 2001 From: lovecraft Date: Mon, 4 May 2026 16:23:23 +0200 Subject: [PATCH] Windows port: ConPTY via ptyqt, POSIX guards --- lib.pri | 32 +++++--- lib/ProcessInfo.cpp | 2 + lib/Pty.h | 189 +++++++++++++++++--------------------------- lib/Pty_win.cpp | 110 ++++++++++++++++++++++++++ lib/Session.cpp | 15 +++- qmltermwidget.pro | 11 ++- src/ksession.cpp | 13 ++- 7 files changed, 238 insertions(+), 134 deletions(-) create mode 100644 lib/Pty_win.cpp diff --git a/lib.pri b/lib.pri index 2950ef2..48c8ad7 100644 --- a/lib.pri +++ b/lib.pri @@ -11,11 +11,6 @@ HEADERS += $$PWD/lib/BlockArray.h \ $$PWD/lib/HistorySearch.h \ $$PWD/lib/KeyboardTranslator.h \ $$PWD/lib/konsole_wcwidth.h \ - $$PWD/lib/kprocess.h \ - $$PWD/lib/kptydevice.h \ - $$PWD/lib/kpty.h \ - $$PWD/lib/kpty_p.h \ - $$PWD/lib/kptyprocess.h \ $$PWD/lib/LineFont.h \ $$PWD/lib/Pty.h \ $$PWD/lib/ProcessInfo.h \ @@ -31,6 +26,16 @@ HEADERS += $$PWD/lib/BlockArray.h \ $$PWD/lib/mac-vkcode.h \ #$$PWD/lib/qtermwidget.h +# POSIX-only headers — not on Windows +!win32 { + HEADERS += \ + $$PWD/lib/kprocess.h \ + $$PWD/lib/kptydevice.h \ + $$PWD/lib/kpty.h \ + $$PWD/lib/kpty_p.h \ + $$PWD/lib/kptyprocess.h +} + SOURCES += $$PWD/lib/BlockArray.cpp \ $$PWD/lib/ColorScheme.cpp \ $$PWD/lib/Emulation.cpp \ @@ -39,12 +44,7 @@ SOURCES += $$PWD/lib/BlockArray.cpp \ $$PWD/lib/HistorySearch.cpp \ $$PWD/lib/KeyboardTranslator.cpp \ $$PWD/lib/konsole_wcwidth.cpp \ - $$PWD/lib/kprocess.cpp \ - $$PWD/lib/kpty.cpp \ - $$PWD/lib/kptydevice.cpp \ - $$PWD/lib/kptyprocess.cpp \ $$PWD/lib/ProcessInfo.cpp \ - $$PWD/lib/Pty.cpp \ #$$PWD/lib/qtermwidget.cpp \ $$PWD/lib/Screen.cpp \ $$PWD/lib/ScreenWindow.cpp \ @@ -56,4 +56,16 @@ SOURCES += $$PWD/lib/BlockArray.cpp \ $$PWD/lib/tools.cpp \ $$PWD/lib/Vt102Emulation.cpp +# Platform-specific PTY sources +win32 { + SOURCES += $$PWD/lib/Pty_win.cpp +} else { + SOURCES += \ + $$PWD/lib/kprocess.cpp \ + $$PWD/lib/kpty.cpp \ + $$PWD/lib/kptydevice.cpp \ + $$PWD/lib/kptyprocess.cpp \ + $$PWD/lib/Pty.cpp +} + #FORMS = $$PWD/lib/SearchBar.ui diff --git a/lib/ProcessInfo.cpp b/lib/ProcessInfo.cpp index f06a3c9..5c8c263 100644 --- a/lib/ProcessInfo.cpp +++ b/lib/ProcessInfo.cpp @@ -20,6 +20,7 @@ // Own #include "ProcessInfo.h" +#if !defined(Q_OS_WIN) // Unix #include #include @@ -27,6 +28,7 @@ #include #include #include +#endif // Qt #include diff --git a/lib/Pty.h b/lib/Pty.h index f427a53..e110d55 100644 --- a/lib/Pty.h +++ b/lib/Pty.h @@ -36,70 +36,88 @@ #include #include #include +#include + +#if defined(Q_OS_WIN) + +// ---- Windows implementation via ptyqt ConPTY ---- + +class IPtyProcess; + +namespace Konsole { + +class Pty : public QObject +{ + Q_OBJECT + +public: + explicit Pty(QObject *parent = nullptr); + explicit Pty(int ptyMasterFd, QObject *parent = nullptr); + ~Pty() override; + + int start(const QString &program, + const QStringList &arguments, + const QStringList &environment, + ulong winid, + bool addToUtmp); + + void setEmptyPTYProperties() {} + void setWriteable(bool) {} + void setFlowControlEnabled(bool) {} + bool flowControlEnabled() const { return true; } + void setWindowSize(int lines, int cols); + QSize windowSize() const; + void setErase(char erase) { _eraseChar = erase; } + char erase() const { return _eraseChar; } + int foregroundProcessGroup() const; + + // QProcess compatibility used by Session + QProcess::ProcessState state() const; + void setWorkingDirectory(const QString &dir) { _workingDir = dir; } + qint64 processId() const; + void *pty() const { return nullptr; } + +public slots: + void setUtf8Mode(bool) {} + void lockPty(bool) {} + void sendData(const char *buffer, int length); + +signals: + void receivedData(const char *buffer, int length); + void finished(int exitCode, QProcess::ExitStatus status); + void started(); + +private slots: + void onReadyRead(); + void onProcessFinished(); + +private: + IPtyProcess *_ptyProcess = nullptr; + char _eraseChar = 0; + int _windowColumns = 80; + int _windowLines = 24; + QString _workingDir; +}; + +} // namespace Konsole + +#else // !Q_OS_WIN + +// ---- POSIX implementation (Linux / macOS) ---- -// KDE #include "kptyprocess.h" namespace Konsole { -/** - * The Pty class is used to start the terminal process, - * send data to it, receive data from it and manipulate - * various properties of the pseudo-teletype interface - * used to communicate with the process. - * - * To use this class, construct an instance and connect - * to the sendData slot and receivedData signal to - * send data to or receive data from the process. - * - * To start the terminal process, call the start() method - * with the program name and appropriate arguments. - */ class Pty: public KPtyProcess { Q_OBJECT public: - - /** - * Constructs a new Pty. - * - * Connect to the sendData() slot and receivedData() signal to prepare - * for sending and receiving data from the terminal process. - * - * To start the terminal process, call the run() method with the - * name of the program to start and appropriate arguments. - */ explicit Pty(QObject* parent = nullptr); - - /** - * Construct a process using an open pty master. - * See KPtyProcess::KPtyProcess() - */ explicit Pty(int ptyMasterFd, QObject* parent = nullptr); - ~Pty() override; - /** - * Starts the terminal process. - * - * Returns 0 if the process was started successfully or non-zero - * otherwise. - * - * @param program Path to the program to start - * @param arguments Arguments to pass to the program being started - * @param environment A list of key=value pairs which will be added - * to the environment for the new process. At the very least this - * should include an assignment for the TERM environment variable. - * @param winid Specifies the value of the WINDOWID environment variable - * in the process's environment. - * @param addToUtmp Specifies whether a utmp entry should be created for - * the pty used. See K3Process::setUsePty() - * @param dbusService Specifies the value of the KONSOLE_DBUS_SERVICE - * environment variable in the process's environment. - * @param dbusSession Specifies the value of the KONSOLE_DBUS_SESSION - * environment variable in the process's environment. - */ int start( const QString& program, const QStringList& arguments, const QStringList& environment, @@ -107,99 +125,32 @@ Q_OBJECT bool addToUtmp ); - /** - * set properties for "EmptyPTY" - */ void setEmptyPTYProperties(); - - /** TODO: Document me */ void setWriteable(bool writeable); - - /** - * Enables or disables Xon/Xoff flow control. The flow control setting - * may be changed later by a terminal application, so flowControlEnabled() - * may not equal the value of @p on in the previous call to setFlowControlEnabled() - */ void setFlowControlEnabled(bool on); - - /** Queries the terminal state and returns true if Xon/Xoff flow control is enabled. */ bool flowControlEnabled() const; - - /** - * Sets the size of the window (in lines and columns of characters) - * used by this teletype. - */ void setWindowSize(int lines, int cols); - - /** Returns the size of the window used by this teletype. See setWindowSize() */ QSize windowSize() const; - - /** TODO Document me */ void setErase(char erase); - - /** */ char erase() const; - - /** - * Returns the process id of the teletype's current foreground - * process. This is the process which is currently reading - * input sent to the terminal via. sendData() - * - * If there is a problem reading the foreground process group, - * 0 will be returned. - */ int foregroundProcessGroup() const; public slots: - - /** - * Put the pty into UTF-8 mode on systems which support it. - */ void setUtf8Mode(bool on); - - /** - * Suspend or resume processing of data from the standard - * output of the terminal process. - * - * See K3Process::suspend() and K3Process::resume() - * - * @param lock If true, processing of output is suspended, - * otherwise processing is resumed. - */ void lockPty(bool lock); - - /** - * Sends data to the process currently controlling the - * teletype ( whose id is returned by foregroundProcessGroup() ) - * - * @param buffer Pointer to the data to send. - * @param length Length of @p buffer. - */ void sendData(const char* buffer, int length); signals: - - /** - * Emitted when a new block of data is received from - * the teletype. - * - * @param buffer Pointer to the data received. - * @param length Length of @p buffer - */ void receivedData(const char* buffer, int length); protected: void setupChildProcess() override; private slots: - // called when data is received from the terminal process void dataReceived(); private: void init(); - - // takes a list of key=value pairs and adds them - // to the environment for the process void addEnvironmentVariables(const QStringList& environment); int _windowColumns; @@ -209,6 +160,8 @@ Q_OBJECT bool _utf8; }; -} +} // namespace Konsole + +#endif // Q_OS_WIN #endif // PTY_H diff --git a/lib/Pty_win.cpp b/lib/Pty_win.cpp new file mode 100644 index 0000000..c1c92fe --- /dev/null +++ b/lib/Pty_win.cpp @@ -0,0 +1,110 @@ +/* + Windows ConPTY-based Pty implementation using ptyqt. + Replaces the POSIX kpty/kptyprocess stack on Windows. +*/ + +#if defined(Q_OS_WIN) + +#include "Pty.h" +#include // direct ConPTY backend — avoids WinPty dependency +#include + +using namespace Konsole; + +Pty::Pty(QObject *parent) : QObject(parent) {} + +Pty::Pty(int /*ptyMasterFd*/, QObject *parent) : QObject(parent) {} + +Pty::~Pty() +{ + if (_ptyProcess) { + _ptyProcess->stopProcess(); + delete _ptyProcess; + } +} + +int Pty::start(const QString &program, + const QStringList &arguments, + const QStringList &environment, + ulong /*winid*/, + bool /*addToUtmp*/) +{ + _ptyProcess = new ConPtyProcess(); + if (!ConPtyProcess::isAvailable()) { + qWarning() << "Pty: ConPTY not available on this Windows version (requires 10 build 1903+)"; + delete _ptyProcess; + _ptyProcess = nullptr; + return -1; + } + + connect(_ptyProcess, &IPtyProcess::readyRead, this, &Pty::onReadyRead); + connect(_ptyProcess, &IPtyProcess::finished, this, &Pty::onProcessFinished); + + // arguments[0] is a duplicate of program (argv[0]); ptyqt takes args without it + QStringList args = arguments.size() > 1 ? arguments.mid(1) : QStringList{}; + + bool ok = _ptyProcess->startProcess(program, args, _workingDir, environment, + static_cast(_windowColumns), + static_cast(_windowLines)); + if (!ok) { + qWarning() << "Pty: failed to start" << program << _ptyProcess->lastError(); + delete _ptyProcess; + _ptyProcess = nullptr; + return -1; + } + + emit started(); + return 0; +} + +void Pty::sendData(const char *buffer, int length) +{ + if (_ptyProcess && length > 0) + _ptyProcess->write(QByteArray(buffer, length)); +} + +void Pty::setWindowSize(int lines, int cols) +{ + _windowLines = lines; + _windowColumns = cols; + if (_ptyProcess) + _ptyProcess->setSize(static_cast(cols), static_cast(lines)); +} + +QSize Pty::windowSize() const +{ + return {_windowColumns, _windowLines}; +} + +int Pty::foregroundProcessGroup() const +{ + return _ptyProcess ? static_cast(_ptyProcess->pid()) : 0; +} + +QProcess::ProcessState Pty::state() const +{ + if (_ptyProcess && _ptyProcess->isRunning()) + return QProcess::Running; + return QProcess::NotRunning; +} + +qint64 Pty::processId() const +{ + return _ptyProcess ? static_cast(_ptyProcess->pid()) : -1; +} + +void Pty::onReadyRead() +{ + if (!_ptyProcess) + return; + QByteArray data = _ptyProcess->readAll(); + if (!data.isEmpty()) + emit receivedData(data.constData(), data.size()); +} + +void Pty::onProcessFinished() +{ + emit finished(0, QProcess::NormalExit); +} + +#endif // Q_OS_WIN diff --git a/lib/Session.cpp b/lib/Session.cpp index 1e185e7..d7d95f0 100644 --- a/lib/Session.cpp +++ b/lib/Session.cpp @@ -84,7 +84,11 @@ Session::Session(QObject* parent) : //create teletype for I/O with shell process _shellProcess = new Pty(); +#if !defined(Q_OS_WIN) ptySlaveFd = _shellProcess->pty()->slaveFd(); +#else + ptySlaveFd = -1; +#endif //create emulation backend _emulation = new Vt102Emulation(); @@ -258,11 +262,15 @@ void Session::run() * their computing time on any system - especially with the problem on arch linux beeing there too. */ QString exec = QString::fromLocal8Bit(QFile::encodeName(_program)); +#if defined(Q_OS_WIN) + if (exec.isEmpty()) { + exec = QString::fromLocal8Bit(qgetenv("COMSPEC")); + if (exec.isEmpty()) + exec = QStringLiteral("cmd.exe"); + } +#else // if 'exec' is not specified, fall back to default shell. if that // is not set then fall back to /bin/sh - - // here we expect full path. If there is no fullpath let's expect it's - // a custom shell (eg. python, etc.) available in the PATH. if (exec.startsWith(QLatin1Char('/')) || exec.isEmpty()) { const QString defaultShell{QLatin1String("/bin/sh")}; @@ -278,6 +286,7 @@ void Session::run() exec = defaultShell; } } +#endif // _arguments sometimes contain ("") so isEmpty() // or count() does not work as expected... diff --git a/qmltermwidget.pro b/qmltermwidget.pro index 8b159b0..c89a586 100644 --- a/qmltermwidget.pro +++ b/qmltermwidget.pro @@ -7,9 +7,18 @@ include(lib.pri) DESTDIR = $$OUT_PWD/QMLTermWidget -DEFINES += HAVE_POSIX_OPENPT HAVE_SYS_TIME_H +!win32:DEFINES += HAVE_POSIX_OPENPT HAVE_SYS_TIME_H macx:DEFINES += HAVE_UTMPX _UTMPX_COMPAT HAVE_PTSNAME HAVE_UNLOCKPT HAVE_GRANTPT +win32 { + # ptyqt ConPTY backend (sibling directory at repo root) + INCLUDEPATH += $$PWD/../ptyqt/src + SOURCES += \ + $$PWD/../ptyqt/src/iptyprocess.cpp \ + $$PWD/../ptyqt/src/conptyprocess.cpp + LIBS += -lUser32 -lAdvapi32 +} + INCLUDEPATH += $$PWD/lib DEPENDPATH += $$PWD/lib INCLUDEPATH += $$PWD/src diff --git a/src/ksession.cpp b/src/ksession.cpp index 204a9eb..8edec92 100644 --- a/src/ksession.cpp +++ b/src/ksession.cpp @@ -69,11 +69,16 @@ Session *KSession::createSession(QString name) //cool-old-term: There is another check in the code. Not sure if useful. +#if defined(Q_OS_WIN) + QString envshell = QString::fromLocal8Bit(qgetenv("COMSPEC")); + QString shellProg = envshell.isEmpty() ? QStringLiteral("cmd.exe") : envshell; + _putenv_s("TERM", "xterm"); +#else QString envshell = getenv("SHELL"); QString shellProg = envshell != NULL ? envshell : "/bin/bash"; - session->setProgram(shellProg); - setenv("TERM", "xterm", 1); +#endif + session->setProgram(shellProg); //session->setProgram(); @@ -152,6 +157,9 @@ void KSession::changeDir(const QString &dir) the foreground before attempting to change the directory. It may not be portable to anything other than Linux. */ +#if defined(Q_OS_WIN) + sendText(QStringLiteral("cd /d ") + dir + QStringLiteral("\r\n")); +#else QString strCmd; strCmd.setNum(getShellPID()); strCmd.prepend("ps -j "); @@ -162,6 +170,7 @@ void KSession::changeDir(const QString &dir) QString cmd = "cd " + dir + "\n"; sendText(cmd); } +#endif } void KSession::setEnvironment(const QStringList &environment)