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 <Nmzik@mail.ru>
This commit is contained in:
Mr. Big Britches
2026-10-24 11:06:45 -07:00
committed by GitHub
parent 8291cfeb04
commit 906d25568f
4 changed files with 87 additions and 115 deletions
+7
View File
@@ -112,6 +112,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="close_terminal">
<property name="text">
<string>Close terminal automatically</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="themeLayout">
<item>
+1 -1
View File
@@ -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);
+77 -113
View File
@@ -17,7 +17,6 @@
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QIODevice>
#include <QLabel>
#include <QMessageBox>
#include <QObject>
@@ -28,7 +27,6 @@
#include <QSettings>
#include <QStringList>
#include <QStyleHints>
#include <QTextStream>
#include <QVariant>
#include <QtCore>
@@ -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<ConfigurationItem> 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<uint32_t>(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<ConfigurationItem> 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;
}
+2 -1
View File
@@ -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<std::string> host_arguments;