1

I am new in c++ programming , so i need help with the logic (code would be awesome). I want to make QTextEdit to work as a terminal like widget. I am trying to achieve that with QProcess like:

void QPConsole::command(QString cmd){
    QProcess* process = new QProcess();
    connect(process, &QProcess::readyReadStandardOutput, [=](){ print(process->readAllStandardOutput()); });
    connect(process, &QProcess::readyReadStandardError, [=](){ print(process->readAllStandardError()); });
    connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),[=](int exitCode, QProcess::ExitStatus exitStatus){ prepareCommandLine(); return; });

    process->setProgram("/bin/bash");
    process->start();
    process->write(cmd.toStdString().c_str());
//    process->write("\n\r");

    process->closeWriteChannel();
}

The problem is next: If i don't close write channel (process->closeWriteChannel) than i am stuked in infinite loop. And if i do close it, than i cannot remember state (kinda makes new session) for example if i do "pwd" i get result, but if i do next "cd .." and then "pwd" the result is same as the first output of "pwd"

So, my question is is it possible to achieve some kind of real time output + having previous state remembered (like in real terminal) with QProcess or i have to do in some other way. In python i did it with subprocess calls with pipes, but i don't know what is the equilavent for c++.

An code example would be great. I have QTextEdit part (which is sending QString param to function), i need part with interaction with console.

Antonio Dias
  • 2,523
  • 17
  • 35
  • I once posted a sample for [SO: Creating QT Application as GUI for existing console-based application on windows](https://stackoverflow.com/a/45209503/7478597). I'm not sure if it's exactly what you want. To make it simple, I separated the widget for (console) input and output. However, may be, it provides some inspiration for what you want to achieve. – Scheff's Cat Jan 25 '19 at 15:56

1 Answers1

1

The idea is simple: Keep an instance of QProcess and write commands to it followed by \n!

Here is a full example, with a QMainWindow and a Bash class, put it in main.cpp:

#include <QtCore>
#include <QtGui>
#include <QtWidgets>

class Bash : public QObject
{
    Q_OBJECT
public:
    explicit Bash(QObject *parent = nullptr) : QObject(parent)
    {

    }

    ~Bash()
    {
        closePrivate();
    }

signals:
    void readyRead(QString);

public slots:
    bool open(int timeout = -1)
    {
        if (m_bash_process)
            return false;

        m_timeout = timeout;

        return openPrivate();
    }

    void close()
    {
        closePrivate();
    }

    bool write(const QString &cmd)
    {
        return writePrivate(cmd);
    }

    void SIGINT()
    {
        SIGINTPrivate();
    }

private:
    //Functions
    bool openPrivate()
    {
        if (m_bash_process)
            return false;

        m_bash_process = new QProcess();

        m_bash_process->setProgram("/bin/bash");

        //Merge stdout and stderr in stdout channel
        m_bash_process->setProcessChannelMode(QProcess::MergedChannels);

        connect(m_bash_process, &QProcess::readyRead, this, &Bash::readyReadPrivate);
        connect(m_bash_process, QOverload<int>::of(&QProcess::finished), this, &Bash::closePrivate);

        m_bash_process->start();

        bool started = m_bash_process->waitForStarted(m_timeout);

        if (!started)
            m_bash_process->deleteLater();

        return started;
    }

    void closePrivate()
    {
        if (!m_bash_process)
            return;

        m_bash_process->closeWriteChannel();
        m_bash_process->waitForFinished(m_timeout);
        m_bash_process->terminate();
        m_bash_process->deleteLater();
    }

    void readyReadPrivate()
    {
        if (!m_bash_process)
            return;

        while (m_bash_process->bytesAvailable() > 0)
        {
            QString str = QLatin1String(m_bash_process->readAll());
            emit readyRead(str);
        }
    }

    bool writePrivate(const QString &cmd)
    {
        if (!m_bash_process)
            return false;

        if (runningPrivate())
            return false;

        m_bash_process->write(cmd.toLatin1());
        m_bash_process->write("\n");

        return m_bash_process->waitForBytesWritten(m_timeout);
    }

    void SIGINTPrivate()
    {
        if (!m_bash_process)
            return;

        QProcess::startDetached("pkill", QStringList() << "-SIGINT" << "-P" << QString::number(m_bash_process->processId()));
    }

    bool runningPrivate()
    {
        if (!m_bash_process)
            return false;

        QProcess process;
        process.setProgram("pgrep");
        process.setArguments(QStringList() << "-P" << QString::number(m_bash_process->processId()));
        process.setProcessChannelMode(QProcess::MergedChannels);

        process.start();

        process.waitForFinished(m_timeout);

        QString pids = QLatin1String(process.readAll());

        QStringList pid_list = pids.split(QRegularExpression("\n"), QString::SkipEmptyParts);

        return (pid_list.size() > 0);
    }

    //Variables
    QPointer<QProcess> m_bash_process;
    int m_timeout;
};

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr) : QMainWindow(parent)
    {
        connect(&m_bash, &Bash::readyRead, this, &MainWindow::readyRead);

        QWidget *central_widget = new QWidget(this);

        QVBoxLayout *layout = new QVBoxLayout(central_widget);
        layout->addWidget(&m_message_board);
        layout->addWidget(&m_cmd_edit);
        layout->addWidget(&m_sigint_btn);

        m_message_board.setReadOnly(true);

        m_cmd_edit.setEnabled(false);

        m_sigint_btn.setText("Send SIGINT(CTRL+C)");

        connect(&m_cmd_edit, &QLineEdit::returnPressed, this, &MainWindow::writeToBash);

        connect(&m_sigint_btn, &QPushButton::clicked, this, &MainWindow::SIGINT);

        setCentralWidget(central_widget);

        resize(640, 480);

        QTimer::singleShot(0, this, &MainWindow::startBash);
    }

    ~MainWindow()
    {
        m_bash.close(); //Not mandatory, called by destructor
    }

private slots:
    void startBash()
    {
        //Open and give to each operation a maximum of 1 second to complete, use -1 to unlimited
        if (m_bash.open(1000))
        {
            m_cmd_edit.setEnabled(true);
            m_cmd_edit.setFocus();
        }
        else
        {
            QMessageBox::critical(this, "Error", "Failed to open bash");
        }
    }

    void writeToBash()
    {
        QString cmd = m_cmd_edit.text();

        m_cmd_edit.clear();

        if (!m_bash.write(cmd))
        {
            QMessageBox::critical(this, "Error", "Failed to write to bash");
        }
    }

    void readyRead(const QString &str)
    {
        m_message_board.appendPlainText(str);
    }

    void SIGINT()
    {
        m_bash.SIGINT();
    }

private:
    Bash m_bash;
    QPlainTextEdit m_message_board;
    QLineEdit m_cmd_edit;
    QPushButton m_sigint_btn;
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

#include "main.moc"
Antonio Dias
  • 2,523
  • 17
  • 35
  • @Anotnio Dias Unfortunatelly it doesn't work in a way i need, i am still not able to execute multiple commands one by one. This way works only if you know which commands you want to use, but i need to proceed command from QTextEdit as input , execute it and see output in real time, like in real terminal – Dušan Atanacković Jan 26 '19 at 12:03
  • sorry form my another question , but i am really new to c++, how can i make this main.moc file, do i have to build it somehow or what? I am completely clueless. – Dušan Atanacković Jan 29 '19 at 09:24
  • `*.moc` is generated automatically by `qmake`, this include is needed when using a class with the `Q_OBJECT` macro in a .cpp file, if the class is in a header file, or split in a .h and a .cpp file, then it's not needed! See [here](https://doc.qt.io/qt-5/moc.html) – Antonio Dias Jan 29 '19 at 12:35
  • It works, i still have to figure it out how to execute some commands other than "ls", "pwd" and "cd.." but it works, thank you very much – Dušan Atanacković Jan 29 '19 at 15:51
  • @Dušan Atanacković: what commands are you currently unable to execute? Just type the command in the line edit and press enter! – Antonio Dias Jan 29 '19 at 16:53
  • I am unable to run commands with sudo, there's some problem with second inserting . For example when i try to run command with sudo and enter password i get "failed to write to bash", also when i run "python /path/to/script.py" and in script there are two lines: 1. read input from user and 2. print what is entered, i get same error, i am unable to make additional input (entering password for sudo, entering some text when user is asked...). Do you have any idea why is that happening? – Dušan Atanacković Jan 30 '19 at 08:31
  • The problem with typing `sudo`, `Python` commands, is they created their own processes, and you need to write directly to these processes, in the case of `sudo`, there is a [workaround](https://stackoverflow.com/a/11955358) `echo mypassword | sudo -S command`, i don't find a way to use python. – Antonio Dias Feb 02 '19 at 02:28