From 906d25568fb9c409d798b97ae72c7c12cbedb914 Mon Sep 17 00:00:00 2001 From: "Mr. Big Britches" <111622360+MrBigBritches@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:06:45 -0700 Subject: [PATCH] launcher: option to close the terminal after a clean exit (#1869) launcher: close the terminal after a clean exit and run several games at once Co-authored-by: nmzik --- src/launcher/forms/main_dialog.ui | 7 ++ src/launcher/include/mainDialog.h | 2 +- src/launcher/src/mainDialog.cpp | 190 ++++++++++++------------------ src/loader/loadExec.cpp | 3 +- 4 files changed, 87 insertions(+), 115 deletions(-) diff --git a/src/launcher/forms/main_dialog.ui b/src/launcher/forms/main_dialog.ui index 267e70a61..dd42296a3 100644 --- a/src/launcher/forms/main_dialog.ui +++ b/src/launcher/forms/main_dialog.ui @@ -112,6 +112,13 @@ + + + + Close terminal automatically + + + diff --git a/src/launcher/include/mainDialog.h b/src/launcher/include/mainDialog.h index 53d4d91e0..067c42600 100644 --- a/src/launcher/include/mainDialog.h +++ b/src/launcher/include/mainDialog.h @@ -22,7 +22,7 @@ public: explicit MainDialog(QWidget* parent = nullptr); ~MainDialog() override = default; - void RunInterpreter(QProcess* process, const Configuration& info); + void RunInterpreter(QProcess* process, const Configuration& info, bool close_terminal); static void WriteSettings(QSettings& s); static void ReadSettings(QSettings& s); diff --git a/src/launcher/src/mainDialog.cpp b/src/launcher/src/mainDialog.cpp index 7768cc21e..6f6fa3479 100644 --- a/src/launcher/src/mainDialog.cpp +++ b/src/launcher/src/mainDialog.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -28,7 +27,6 @@ #include #include #include -#include #include #include @@ -47,23 +45,17 @@ class QWidget; #if defined(_WIN32) -constexpr char EMULATOR_EXE[] = "kyty_emulator.exe"; +constexpr char EMULATOR_EXE[] = "kyty_emulator.exe"; +constexpr char CMD_EXE[] = "cmd.exe"; +constexpr DWORD CMD_X_CHARS = 175; +constexpr DWORD CMD_Y_CHARS = 1000; #else constexpr char EMULATOR_EXE[] = "kyty_emulator"; #endif - -#if defined(_WIN32) -constexpr char CMD_EXE[] = "cmd.exe"; -#elif defined(__linux__) -constexpr char KYTY_BASH_FILE[] = "kyty_run.sh"; -#endif -#if defined(_WIN32) -constexpr DWORD CMD_X_CHARS = 175; -constexpr DWORD CMD_Y_CHARS = 1000; -#endif constexpr char SETTINGS_MAIN_DIALOG[] = "MainDialog"; constexpr char SETTINGS_MAIN_LAST_GEOMETRY[] = "geometry"; constexpr char SETTINGS_CHECK_UPDATES[] = "check_updates_on_startup"; +constexpr char SETTINGS_CLOSE_TERMINAL[] = "close_terminal"; constexpr char SETTINGS_THEME[] = "theme"; class MainDialogPrivate: public QObject { @@ -89,6 +81,7 @@ public: private: static QByteArray g_last_geometry; static bool g_check_updates_on_startup; + static bool g_close_terminal; static int g_theme; static void ApplyTheme(int theme) { @@ -108,13 +101,12 @@ private: TitleSound m_title_sound; QString m_interpreter; - QProcess m_process; - - QPointer m_running_item; + int m_running_games = 0; }; QByteArray MainDialogPrivate::g_last_geometry; bool MainDialogPrivate::g_check_updates_on_startup = true; +bool MainDialogPrivate::g_close_terminal = false; int MainDialogPrivate::g_theme = 0; MainDialog::MainDialog(QWidget* parent): QDialog(parent), m_p(new MainDialogPrivate(this)) { @@ -122,7 +114,6 @@ MainDialog::MainDialog(QWidget* parent): QDialog(parent), m_p(new MainDialogPriv } MainDialogPrivate::~MainDialogPrivate() { - m_process.disconnect(this); delete m_ui; } @@ -134,10 +125,14 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) { m_main_dialog = main_dialog; m_update_checker = new UpdateChecker(main_dialog); m_ui->check_updates_on_startup->setChecked(g_check_updates_on_startup); + m_ui->close_terminal->setChecked(g_close_terminal); m_ui->theme_combo->setCurrentIndex(g_theme); ApplyTheme(g_theme); m_ui->check_updates_link->setVisible(UpdateChecker::IsSupported()); m_ui->check_updates_on_startup->setVisible(UpdateChecker::IsSupported()); +#if defined(__APPLE__) + m_ui->close_terminal->setVisible(false); // The emulator runs without a terminal. +#endif main_dialog->setWindowFlags(Qt::Dialog /*| Qt::MSWindowsFixedSizeDialogHint*/); @@ -146,13 +141,12 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) { connect(m_ui->widget, &ConfigurationListWidget::Select, this, [this]() { Update(); const auto* item = m_ui->widget->GetSelectedItem(); - m_title_sound.SetTitle(m_process.state() == QProcess::NotRunning && item != nullptr - ? item->GetInfo().basedir - : QString()); + m_title_sound.SetTitle(m_running_games == 0 && item != nullptr ? item->GetInfo().basedir + : QString()); }); connect(m_ui->widget, &ConfigurationListWidget::PreviewControllerColor, this, [this](const QString& color) { - if (m_process.state() == QProcess::NotRunning) { + if (m_running_games == 0) { m_controller_preview.SetColor(color); } }); @@ -166,6 +160,10 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) { g_check_updates_on_startup = checked; m_ui->widget->WriteSettings(); }); + connect(m_ui->close_terminal, &QCheckBox::toggled, this, [this](bool checked) { + g_close_terminal = checked; + m_ui->widget->WriteSettings(); + }); connect(m_ui->theme_combo, &QComboBox::currentIndexChanged, this, [this](int index) { g_theme = index; ApplyTheme(index); @@ -176,16 +174,6 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) { m_ui->widget->WriteSettings(); }); - connect(&m_process, &QProcess::stateChanged, this, [this](QProcess::ProcessState state) { - m_update_checker->SetGameRunning(state != QProcess::NotRunning); - if (state == QProcess::NotRunning) { - if (m_running_item != nullptr) { - m_running_item->SetRunning(false); - } - Update(); - } - }); - m_ui->label_settings_file->setText(tr("Settings file: ") + m_ui->widget->GetSettingsFile()); m_main_dialog->restoreGeometry(g_last_geometry); @@ -327,32 +315,14 @@ static QStringList CreateEmulatorArgs(const Configuration& info) { } #ifdef __linux__ -static QString BashQuote(QString value) { - value.replace('\'', "'\\''"); - return QStringLiteral("'") + value + QStringLiteral("'"); -} - -static bool CreateBashScript(const QString& interpreter, const QStringList& args, - const QString& file_name) { - QFile file(file_name); - if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { - QTextStream s(&file); - - s << "#!/bin/bash\n"; - s << BashQuote(interpreter); - for (const auto& arg: args) { - s << " " << BashQuote(arg); - } - s << "\n"; - s << "echo Press any key...\n"; - s << "read -n1\n"; - - file.close(); - - return file.setPermissions(file.permissions() | QFile::ExeUser | QFile::ExeOwner | - QFile::ExeGroup); - } - return false; +// Runs the command in bash, then waits for a key (with close_on_clean_exit, only after an error). +// The command goes in as positional parameters ("$@"), so it needs no quoting. +static QStringList BashCommand(const QString& interpreter, const QStringList& args, + bool close_on_clean_exit) { + const QString run = close_on_clean_exit ? "\"$@\" && exit" : "\"$@\""; + return QStringList {"bash", "-c", run + "; echo Press any key...; read -n1", "kyty", + interpreter} + + args; } // Find a terminal and its command separator. @@ -416,28 +386,23 @@ static bool FindTerminal(QString* program, QStringList* prefix) { #endif #if defined(_WIN32) -// Quote one token for cmd.exe so paths with spaces survive /K parsing. -static QString WinCmdQuote(QString value) { - value.replace(QLatin1Char('"'), QStringLiteral("\\\"")); - return QLatin1Char('"') + value + QLatin1Char('"'); -} - -static QString BuildWinCmdKCommand(const QString& interpreter, const QStringList& args) { - QString command = WinCmdQuote(QDir::toNativeSeparators(interpreter)); - for (const auto& arg: args) { - command += QLatin1Char(' '); - command += WinCmdQuote(arg); - } - return command; +// cmd /K arguments that keep the console open (with close_on_clean_exit, only after an error). +// A quote is written as "" rather than \" so cmd's quote count stays even and operators inside +// arguments stay quoted. +static QString CmdCommand(const QString& interpreter, const QStringList& args, + bool close_on_clean_exit) { + static const QRegularExpression quote_or_end(QStringLiteral(R"((\\*)("|\z))")); + // The UCRT reads 2n backslashes before a quote as n. + auto quoted = QStringList {interpreter} + args; + quoted.replaceInStrings(quote_or_end, QStringLiteral(R"(\1\1\2\2)")); + return QStringLiteral("/K \"\"") + quoted.join(QStringLiteral("\" \"")) + + (close_on_clean_exit ? "\" && exit\"" : "\"\""); } #endif -void MainDialog::RunInterpreter(QProcess* process, const Configuration& info) { +void MainDialog::RunInterpreter(QProcess* process, const Configuration& info, bool close_terminal) { const auto& interpreter = m_p->GetInterpreter(); - QFileInfo f(interpreter); - auto dir = f.absoluteDir(); - auto args = CreateEmulatorArgs(info); if (args.isEmpty()) { QMessageBox::critical(this, tr("Error"), tr("Invalid emulator configuration")); @@ -445,40 +410,24 @@ void MainDialog::RunInterpreter(QProcess* process, const Configuration& info) { return; } -#ifdef __linux__ - auto bash_file_name = dir.filePath(KYTY_BASH_FILE); - if (!CreateBashScript(interpreter, args, bash_file_name)) { - QMessageBox::critical(this, tr("Error"), tr("Can't create file:\n") + bash_file_name); - QApplication::quit(); - return; - } - - { - QString terminal; - QStringList terminal_prefix; - // Pass the script as a file argument (not bash -c) so paths with spaces work. - if (FindTerminal(&terminal, &terminal_prefix)) { - process->setProgram(terminal); - process->setArguments(terminal_prefix + QStringList {"bash", bash_file_name}); - } else { - // Run without a terminal as a fallback. - process->setProgram(QStringLiteral("bash")); - process->setArguments({bash_file_name}); - } - } -#elif defined(_WIN32) - { - // Use nativeArguments so Qt does not re-quote the /K command string. - process->setProgram(CMD_EXE); - process->setArguments({}); - process->setNativeArguments(QStringLiteral("/K \"") + - BuildWinCmdKCommand(interpreter, args) + QLatin1Char('"')); - } -#else + // Without a terminal, the emulator runs directly. process->setProgram(interpreter); process->setArguments(args); +#ifdef __linux__ + QString terminal; + QStringList terminal_prefix; + if (FindTerminal(&terminal, &terminal_prefix)) { + process->setProgram(terminal); + process->setArguments(terminal_prefix + BashCommand(interpreter, args, close_terminal)); + } +#elif defined(_WIN32) + // Use nativeArguments so Qt does not re-quote the /K command string. + process->setProgram(CMD_EXE); + process->setArguments({}); + process->setNativeArguments( + CmdCommand(QDir::toNativeSeparators(interpreter), args, close_terminal)); #endif - process->setWorkingDirectory(dir.path()); + process->setWorkingDirectory(QFileInfo(interpreter).absolutePath()); #if defined(_WIN32) process->setCreateProcessArgumentsModifier([](QProcess::CreateProcessArguments* args) { args->flags |= static_cast(CREATE_NEW_CONSOLE); @@ -525,6 +474,7 @@ void MainDialogPrivate::WriteSettings(QSettings& s) { s.setValue(SETTINGS_MAIN_LAST_GEOMETRY, g_last_geometry); } s.setValue(SETTINGS_CHECK_UPDATES, g_check_updates_on_startup); + s.setValue(SETTINGS_CLOSE_TERMINAL, g_close_terminal); s.setValue(SETTINGS_THEME, g_theme); s.endGroup(); @@ -535,6 +485,7 @@ void MainDialogPrivate::ReadSettings(QSettings& s) { g_last_geometry = s.value(SETTINGS_MAIN_LAST_GEOMETRY, g_last_geometry).toByteArray(); g_check_updates_on_startup = s.value(SETTINGS_CHECK_UPDATES, true).toBool(); + g_close_terminal = s.value(SETTINGS_CLOSE_TERMINAL, false).toBool(); g_theme = s.value(SETTINGS_THEME, 0).toInt(); if (g_theme < 0 || g_theme > 2) { g_theme = 0; @@ -547,17 +498,30 @@ void MainDialogPrivate::Run() { if (m_update_checker->IsInstalling()) { return; } - m_running_item = m_ui->widget->GetSelectedItem(); - if (m_running_item == nullptr) { + QPointer item = m_ui->widget->GetSelectedItem(); + if (item == nullptr) { return; } - m_running_item->SetRunning(true); + item->SetRunning(true); + m_running_games++; m_controller_preview.SetSuspended(true); m_title_sound.SetTitle({}); - auto info = m_ui->widget->CreateConfiguration(*m_running_item); - m_main_dialog->RunInterpreter(&m_process, *info); + auto* process = new QProcess(this); + connect(process, &QProcess::stateChanged, this, + [this, process, item](QProcess::ProcessState state) { + if (state == QProcess::NotRunning) { + if (item != nullptr) { + item->SetRunning(false); + } + m_running_games--; + process->deleteLater(); + Update(); + } + }); + auto info = m_ui->widget->CreateConfiguration(*item); + m_main_dialog->RunInterpreter(process, *info, g_close_terminal); Update(); } @@ -565,8 +529,7 @@ void MainDialogPrivate::Run() { void MainDialogPrivate::Update() { const auto* item = m_ui->widget->GetSelectedItem(); - bool run_enabled = (m_process.state() == QProcess::NotRunning && item != nullptr && - !m_update_checker->IsInstalling()); + bool run_enabled = item != nullptr && !item->IsRunning() && !m_update_checker->IsInstalling(); if (run_enabled) { const auto& info = item->GetInfo(); @@ -575,7 +538,8 @@ void MainDialogPrivate::Update() { } m_ui->widget->SetRunEnabled(run_enabled); - if (m_process.state() != QProcess::NotRunning) { + m_update_checker->SetGameRunning(m_running_games > 0); + if (m_running_games > 0) { m_controller_preview.SetSuspended(true); return; } diff --git a/src/loader/loadExec.cpp b/src/loader/loadExec.cpp index 862fa842c..cd8d6d9d3 100644 --- a/src/loader/loadExec.cpp +++ b/src/loader/loadExec.cpp @@ -149,7 +149,8 @@ int LoadExec(const char* path, const char* const argv[]) { } CloseHandle(process.hThread); CloseHandle(process.hProcess); - std::quick_exit(0); + // Nonzero, or the launcher's "cmd /K ... && exit" closes the console the new process shares. + std::quick_exit(1); #else std::ifstream command("/proc/self/cmdline", std::ios::binary); std::vector host_arguments;