Revision control
++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:significant
++
++ #include "qqmldebugconsole_p.h"
++ #include "qqmldebugcommandlistener_p.h"
++
++ #include <iostream>
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace Qt::StringLiterals;
++
++ QQmlDebugConsole::QQmlDebugConsole(QObject *parent) : QObject(parent)
++ {
++ registerCommand({ "help"_L1, "h"_L1 }, {}, tr("Show the list of available commands."),
++ [this](const QStringList &) {
++ printHelp();
++ prompt();
++ });
++ }
++
++ QQmlDebugConsole::~QQmlDebugConsole()
++ {
++ stop();
++ }
++
++ void QQmlDebugConsole::registerCommand(const QStringList &names, const QString &argsHint,
++ const QString &description, CommandHandler handler)
++ {
++ m_commands.append(Command{ names, argsHint, description, std::move(handler) });
++ }
++
++ void QQmlDebugConsole::start()
++ {
++ Q_ASSERT(!m_listenerThread.isRunning());
++
++ m_listener = new QQmlDebugCommandListener;
++ m_listener->moveToThread(&m_listenerThread);
++ connect(m_listener, &QQmlDebugCommandListener::command, this, &QQmlDebugConsole::handleLine);
++ connect(m_listener, &QQmlDebugCommandListener::endOfInput, this, &QQmlDebugConsole::endOfInput);
++ connect(this, &QQmlDebugConsole::requestCommand, m_listener,
++ &QQmlDebugCommandListener::readCommand);
++
++ m_listenerThread.start();
++ }
++
++ void QQmlDebugConsole::stop()
++ {
++ // The listener thread is idle in its event loop between commands, so quitting
++ // it from here (after the last command has been processed) unblocks wait().
++ if (m_listenerThread.isRunning()) {
++ m_listenerThread.quit();
++ m_listenerThread.wait();
++ }
++
++ delete std::exchange(m_listener, nullptr);
++ }
++
++ void QQmlDebugConsole::prompt(const QString &message, bool ready)
++ {
++ if (!message.isEmpty())
++ std::cerr << qPrintable(message) << std::endl;
++ std::cerr << "> ";
++ if (ready)
++ emit requestCommand();
++ }
++
++ void QQmlDebugConsole::printLine(const QString &message)
++ {
++ std::cerr << qPrintable(message) << std::endl;
++ }
++
++ void QQmlDebugConsole::askConfirmation(const QString &question, std::function<void()> onConfirmed,
++ std::function<void()> onDeclined)
++ {
++ m_pendingQuestion = question;
++ m_onConfirmed = std::move(onConfirmed);
++ m_onDeclined = std::move(onDeclined);
++ prompt(question);
++ }
++
++ void QQmlDebugConsole::handleLine(const QString &line)
++ {
++ const QStringList args = line.split(QChar::Space, Qt::SkipEmptyParts);
++ if (args.isEmpty()) {
++ prompt();
++ return;
++ }
++
++ const QString keyword = args.first().toLower();
++ const QStringList rest = args.mid(1);
++
++ if (!m_pendingQuestion.isEmpty()) {
++ if (keyword == "y"_L1 || keyword == "yes"_L1) {
++ m_pendingQuestion.clear();
++ m_onDeclined = {};
++ if (auto onConfirmed = std::exchange(m_onConfirmed, {}))
++ onConfirmed();
++ } else if (keyword == "n"_L1 || keyword == "no"_L1) {
++ m_pendingQuestion.clear();
++ m_onConfirmed = {};
++ if (auto onDeclined = std::exchange(m_onDeclined, {}))
++ onDeclined();
++ else
++ prompt();
++ } else {
++ prompt(m_pendingQuestion);
++ }
++ return;
++ }
++
++ for (const Command &cmd : std::as_const(m_commands)) {
++ if (cmd.names.contains(keyword)) {
++ cmd.handler(rest);
++ return;
++ }
++ }
++
++ printHelp();
++ prompt();
++ }
++
++ void QQmlDebugConsole::printHelp()
++ {
++ printLine(tr("The following commands are available:"));
++ for (const Command &cmd : std::as_const(m_commands)) {
++ QStringList quoted;
++ quoted.reserve(cmd.names.size());
++ for (const QString &name : cmd.names)
++ quoted << u'\'' + name + u'\'';
++ QString header = quoted.join(", "_L1);
++ if (!cmd.argsHint.isEmpty())
++ header += u' ' + cmd.argsHint;
++ printLine(header);
++ printLine(" "_L1 + cmd.description);
++ }
++ }
++
++ QT_END_NAMESPACE
++
++ #include "moc_qqmldebugconsole_p.cpp"