Search code examples
pythonpython-3.xpyqtpyqt5carriage-return

Carriage return in QTextBrowser in PyQt5


I'm currently stuck. I'm trying to make it so that my QTextBrowser can do a nice presentation of the textfeed it gets(in multiple parts) from a programs output. But the download progress becomes several lines of text because i can't implement a way to handle carriage return. Here's a string that get's appended to my QTextBrowser, where \r and \n is shown:

dQw4w9WgXcQ: Downloading thumbnail ...\n

dQw4w9WgXcQ: Writing thumbnail to: D:\Musikk\DLs\Rick Astley - Never Gonna Give You Up.jpg\n

Destination: D:\Musikk\DLs\Rick Astley - Never Gonna Give You Up.webm\n

\r 0.9% of 3.33MiB at 4.03MiB/s ETA 00:00

\r 1.8% of 3.33MiB at 3.62MiB/s ETA 00:00

And the last two lines there will be a lot of, since it's going to download all 100% of it and several files.

My current implementation was simple (and doesn't handle this issue), with

self.textBrowser.append(text)

And you'd get results like this (snippet of it all):

dQw4w9WgXcQ: Downloading thumbnail ...

dQw4w9WgXcQ: Writing thumbnail to: D:\Musikk\DLs\Rick Astley - Never Gonna Give You Up.jpg

Destination: D:\Musikk\DLs\Rick Astley - Never Gonna Give You Up.webm

0.0% of 3.33MiB at 19.15KiB/s ETA 02:58

0.1% of 3.33MiB at 57.44KiB/s ETA 00:59

0.2% of 3.33MiB at 131.57KiB/s ETA 00:25

I could also remove the \n in the strings to have a smaller space between the lines where those are present.

I tried another partial solution for when the strings contain \r, instead of appending, however, some strings contain several \r characters, and this doesn't account for that.

        self.textbrowser.insertPlainText(text)
        if '\r' in text:
            self.textbrowser.moveCursor(QTextCursor.End, mode=QTextCursor.MoveAnchor)
            self.textbrowser.moveCursor(QTextCursor.StartOfLine, mode=QTextCursor.MoveAnchor)
            self.textbrowser.moveCursor(QTextCursor.End,mode=QTextCursor.KeepAnchor)
            self.textbrowser.textCursor().removeSelectedText()
            self.textbrowser.textCursor().deletePreviousChar() 

I also tried:

        self.textbrowser.append(text) 
        self.textbrowser.textCursor().deletePreviousChar() 

However it did remove some spaces between lines (removes the newline character .append() adds.) But it still doesn't really do anything but remove the \r character as if it wasn't there. And i can't at all get a consistent solution.

For reference, this is a wrapper for youtube-dl.exe. And if you use youtube-dl.exe in console, (Powershell on Windows 10 for example) it'll do the right thing and jump to the start of the line when updating percentage, so you'll get a nice download line that increases by itself, and the ETA counts down with too, without going on over many lines.

For simplified code, here's an example that shows it in use, no other solution than appending the decoded string. Take note this is from a SO answer some time ago, where there's a QTextEdit instead of QTextBrowser. And it requires youtube-dl.exe to be in the working directory of the python script. In this case, the QTextEdit is called self.edit.

import sys

from PyQt5.QtCore import QProcess
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout, QVBoxLayout, QTextEdit, QLabel, QLineEdit


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

        # Create an instance variable here (of type QTextEdit)
        self.startBtn = QPushButton('OK')
        self.stopBtn = QPushButton('Cancel')

        self.hbox = QHBoxLayout()
        self.hbox.addStretch(1)
        self.hbox.addWidget(self.startBtn)
        self.hbox.addWidget(self.stopBtn)

        self.label = QLabel("Url: ")
        self.lineEdit = QLineEdit()

        self.lineEdit.textChanged.connect(self.EnableStart)

        self.hbox2 = QHBoxLayout()
        self.hbox2.addWidget(self.label)
        self.hbox2.addWidget(self.lineEdit)

        self.edit = QTextEdit()
        self.edit.setWindowTitle("QTextEdit Standard Output Redirection")

        self.vbox = QVBoxLayout()
        self.vbox.addStretch(1)

        self.vbox.addLayout(self.hbox2)
        self.vbox.addWidget(self.edit)
        self.vbox.addLayout(self.hbox)

        self.central = QWidget()

        self.central.setLayout(self.vbox)
        self.central.show()

        self.startBtn.clicked.connect(self.startDownload)
        self.stopBtn.clicked.connect(self.kill)
        self.stateChanged.connect(self.slotChanged)

        self.EnableStart()

    def slotChanged(self, newState):
        if newState == QProcess.NotRunning:
            self.startBtn.setDisabled(False)
        elif newState == QProcess.Running:
            self.startBtn.setDisabled(True)

    def startDownload(self):
        self.start("youtube-dl", [self.lineEdit.text()])

    def readStdOutput(self):
        self.edit.append(str(self.readAllStandardOutput().data().decode('utf-8','ignore')))

    def EnableStart(self):
        self.startBtn.setDisabled(self.lineEdit.text() == "")


def main():
    app = QApplication(sys.argv)
    qProcess = GUI()

    qProcess.setProcessChannelMode(QProcess.MergedChannels)
    qProcess.readyReadStandardOutput.connect(qProcess.readStdOutput)

    return app.exec_()


if __name__ == '__main__':
    main()  

I don't really know if this is doable, but to sum it all up:

  • The strings appended to the QTextEdit does contain the newline characters and sometimes the carriage return character. (\n and \r respectively)

  • The strings need to be appended without a new line, possibly as simple as removing the last newline character after appending.

  • A few times, sentences get sent in one whole bunch so my second implementation doesn't work. Take note below to see, note that output is binary, because it hasn't been converted to string yet in this case. Printing the decoded stream below will give you the expected behavior in console. (But not in QTextEdit obviously)

b'[youtube] dQw4w9WgXcQ: Downloading webpage\n[youtube] dQw4w9WgXcQ: Downloading video info webpage\n[youtube] dQw4w9WgXcQ: Extracting video information\n[youtube] dQw4w9WgXcQ: Downloading MPD manifest\n[youtube] dQw4w9WgXcQ: Downloading thumbnail ...\n'

b'[youtube] dQw4w9WgXcQ: Writing thumbnail to: D:\\Musikk\\DLs\\Rick Astley - Never Gonna Give You Up.jpg\n'

b'[download] Destination: D:\\Musikk\\DLs\\Rick Astley - Never Gonna Give You Up.webm\n'

b'\r[download]   0.0% of 3.33MiB at 332.99KiB/s ETA 00:10\r[download]   0.1% of 3.33MiB at 856.39KiB/s ETA 00:03\r[download]   0.2% of 3.33MiB at  1.95MiB/s ETA 00:01 '

b'\r[download]   0.4% of 3.33MiB at  4.18MiB/s ETA 00:00 '

b'\r[download]   0.9% of 3.33MiB at  3.78MiB/s ETA 00:00 '

b'\r[download]   1.8% of 3.33MiB at  4.24MiB/s ETA 00:00 '

b'\r[download]   3.7% of 3.33MiB at  5.27MiB/s ETA 00:00 '
  • I can get the wanted output in the python console simply by printing each string supplied by the function, and setting end='' because then it does heed the \r in the console. (Using PyCharm if it matters)

It a long and messy question, that boils down to, can i get the console functionality of \r, in this case, to work with youtube-dl, without extra lines/spaces, and without the percentages going over many many lines? I had a bit of critique for my last question, so i did my best to include attempts and observations through this.

Any help appreciated!

Edit on request, output in bytes, by changing self.edit.append(...) to

print(str(self.readAllStandardOutput()))

byte output

And here it is if i decode output, and print with

print(str(self.readAllStandardOutput().data().decode('utf-8','ignore')), end='')

enter image description here

Take note, this only shows one line because it updates every time a new string is provided.


Solution

  • To remove the \n at the end of each text we use strip(), then we must recognize the lines that have percentage and the substring [download], this would suffice to solve the problem, but in several cases the input we get are Several lines in a single text, then what we need is the last line.

    def readStdOutput(self):
        data = self.readAllStandardOutput().data()
        text = data.decode('utf-8','ignore').strip()
    
        # get the last line of QTextEdit
        self.edit.moveCursor(QTextCursor.End, QTextCursor.MoveAnchor)
        self.edit.moveCursor(QTextCursor.StartOfLine, QTextCursor.MoveAnchor)
        self.edit.moveCursor(QTextCursor.End, QTextCursor.KeepAnchor)
        lastLine = self.edit.textCursor().selectedText()
    
        # Check if a percentage has already been placed.
        if "[download]" in lastLine and "%" in lastLine:
            self.edit.textCursor().removeSelectedText()
            self.edit.textCursor().deletePreviousChar()
            # Last line of text
            self.edit.append("[download] "+text.split("[download]")[-1])
        else:
            self.edit.moveCursor(QTextCursor.End, QTextCursor.MoveAnchor)
            if "[download]" in text and "%" in text:
                # Last line of text
                self.edit.append("[download] "+text.split("[download]")[-1])
            else:
                self.edit.append(text)