TheHidden
TheHidden

Reputation: 592

PyQt5 alternative to Qtermwidget

I am trying to find an alternative method to Qtermwidget to display terminal output and accept terminal input (much like a standard linux terminal).

The only issue I have with this is that on the target OS (ubuntu) it has to be manually compiled and reinstalled due to some issues.

I am trying to make the setup of my application as easy and quick as possible with most of the dependencies being simple pip packages or standard apt-installs.

So my question is:

Is there a standard library or way of using a terminal like in/out put in pyqt? I have considered just building it in javascript (easy enough) and using QWebEngineView but is this the best alternative?

Upvotes: 2

Views: 861

Answers (1)

eyllanesc
eyllanesc

Reputation: 244132

One possible option is to write the QTermWidget logic with pure python to make it portable but this can take time, so in this answer I will implement the logic using xterm.js with the help of QWebChannel:

index.html

<!doctype html>
  <html>
    <head>
      <style>
      * { padding: 0; margin: 0; }
      html, body, #terminal-container {
          min-height: 100% !important;
          width: 100%;
          height: 100%;
      }
      #terminal-container {
          background: black;
      }
      </style>
      <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/xterm.css" />
      <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/xterm.js"></script> 
      <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/xterm-addon-fit.js"></script> 
      <script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
      <script type="text/javascript" src="index.js"></script>
    </head>
    <body>
      <div id="terminal-container"></div>
    </body>
  </html>

index.js

window.onload = function () {
    const terminal = new Terminal();
    f = new FitAddon.FitAddon();
    terminal.loadAddon(f);
    const container = document.getElementById('terminal-container');
    terminal.open(container);
    terminal.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
    f.fit();
    new QWebChannel(qt.webChannelTransport, function (channel) {
        var socket = channel.objects.socket;
        var resize_listener = channel.objects.resize_listener;
        terminal.onKey(function(e){
            socket.send_data(e.key)
        });
        socket.dataChanged.connect(function(text){
            terminal.write(text)
        });
        resize_listener.resized.connect(function(){
            f.fit();
        });
    });
}

main.py

from functools import cached_property
import os

from PyQt5 import QtCore, QtWidgets, QtNetwork, QtWebEngineWidgets, QtWebChannel

CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))


class TerminalSocket(QtNetwork.QTcpSocket):
    dataChanged = QtCore.pyqtSignal(str)

    def __init__(self, parent=None):
        super().__init__(parent)

        self.readyRead.connect(self._handle_ready_read)
        self.error.connect(self._handle_error)

    @QtCore.pyqtSlot(str)
    def send_data(self, message):
        self.write(message.encode())

    def _handle_ready_read(self):
        data = self.readAll().data()
        self.dataChanged.emit(data.decode())

    def _handle_error(self):
        print(self.errorString())


class ResizeListener(QtCore.QObject):
    resized = QtCore.pyqtSignal()

    def __init__(self, widget):
        super().__init__(widget)
        self._widget = widget
        if isinstance(self.widget, QtWidgets.QWidget):
            self.widget.installEventFilter(self)

    @property
    def widget(self):
        return self._widget

    def eventFilter(self, obj, event):
        if obj is self.widget and event.type() == QtCore.QEvent.Resize:
            QtCore.QTimer.singleShot(100, self.resized.emit)
        return super().eventFilter(obj, event)


class TerminalWidget(QtWebEngineWidgets.QWebEngineView):
    def __init__(self, ipaddr, port, parent=None):
        super().__init__(parent)

        resize_listener = ResizeListener(self)
        self.page().setWebChannel(self.channel)
        self.channel.registerObject("resize_listener", resize_listener)
        self.channel.registerObject("socket", self.socket)
        filename = os.path.join(CURRENT_DIR, "index.html")
        self.load(QtCore.QUrl.fromLocalFile(filename))
        self.socket.connectToHost(ipaddr, port)

    @cached_property
    def socket(self):
        return TerminalSocket()

    @cached_property
    def channel(self):
        return QtWebChannel.QWebChannel()


def main():
    import sys

    app = QtWidgets.QApplication(sys.argv)

    QtCore.QCoreApplication.setApplicationName("QTermWidget Test")
    QtCore.QCoreApplication.setApplicationVersion("1.0")

    parser = QtCore.QCommandLineParser()
    parser.addHelpOption()
    parser.addVersionOption()
    parser.setApplicationDescription(
        "Example(client-side) for remote terminal of QTermWidget"
    )
    parser.addPositionalArgument("ipaddr", "adrress of host")
    parser.addPositionalArgument("port", "port of host")

    parser.process(QtCore.QCoreApplication.arguments())

    requiredArguments = parser.positionalArguments()
    if len(requiredArguments) != 2:
        parser.showHelp(1)
        sys.exit(-1)

    address, port = requiredArguments
    w = TerminalWidget(QtNetwork.QHostAddress(address), int(port))
    w.resize(640, 480)
    w.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

Upvotes: 3

Related Questions