From 019870ffd8308d94289d72d45abeb3db39ccde92 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Thu, 12 Mar 2026 04:29:03 -0500 Subject: Add per-executable Proton toggle and terminal launch option New checkboxes in Edit Executables dialog: - "Use Proton": controls whether the executable launches through Proton (default on for backward compat). Disable for native Linux tools. - "Open in terminal": launches the executable in a terminal window showing stdout/stderr output, useful for debugging. Auto-detects terminal emulator (konsole, gnome-terminal, alacritty, kitty, foot, xterm). Terminal stays open after process exits showing the exit code. Co-Authored-By: Claude Opus 4.6 --- src/src/editexecutablesdialog.cpp | 26 ++++++++++++ src/src/editexecutablesdialog.ui | 23 +++++++++++ src/src/executableslist.cpp | 20 ++++++++++ src/src/executableslist.h | 6 ++- src/src/processrunner.cpp | 3 ++ src/src/protonlauncher.cpp | 73 ++++++++++++++++++++++++++++++++++ src/src/protonlauncher.h | 2 + src/src/spawn.cpp | 84 ++++++++++++++++++++------------------- src/src/spawn.h | 2 + 9 files changed, 198 insertions(+), 41 deletions(-) diff --git a/src/src/editexecutablesdialog.cpp b/src/src/editexecutablesdialog.cpp index e5dcda9..4c6819d 100644 --- a/src/src/editexecutablesdialog.cpp +++ b/src/src/editexecutablesdialog.cpp @@ -126,6 +126,12 @@ EditExecutablesDialog::EditExecutablesDialog(OrganizerCore& oc, int sel, connect(ui->hide, &QCheckBox::toggled, [&] { save(); }); + connect(ui->useProton, &QCheckBox::toggled, [&] { + save(); + }); + connect(ui->useTerminal, &QCheckBox::toggled, [&] { + save(); + }); connect(ui->list->model(), &QAbstractItemModel::rowsMoved, [&] { saveOrder(); }); @@ -391,6 +397,10 @@ void EditExecutablesDialog::clearEdits() ui->minimizeToSystemTray->setChecked(false); ui->hide->setEnabled(false); ui->hide->setChecked(false); + ui->useProton->setEnabled(false); + ui->useProton->setChecked(true); + ui->useTerminal->setEnabled(false); + ui->useTerminal->setChecked(false); m_lastGoodTitle = ""; } @@ -407,6 +417,8 @@ void EditExecutablesDialog::setEdits(const Executable& e) ui->useApplicationIcon->setChecked(e.usesOwnIcon()); ui->minimizeToSystemTray->setChecked(e.minimizeToSystemTray()); ui->hide->setChecked(e.hide()); + ui->useProton->setChecked(e.useProton()); + ui->useTerminal->setChecked(e.useTerminal()); m_lastGoodTitle = e.title(); @@ -453,6 +465,8 @@ void EditExecutablesDialog::setEdits(const Executable& e) ui->forceLoadLibraries->setEnabled(true); ui->minimizeToSystemTray->setEnabled(true); ui->hide->setEnabled(true); + ui->useProton->setEnabled(true); + ui->useTerminal->setEnabled(true); } void EditExecutablesDialog::save() @@ -524,6 +538,18 @@ void EditExecutablesDialog::save() e->flags(e->flags() & (~Executable::Hide)); } + if (ui->useProton->isChecked()) { + e->flags(e->flags() | Executable::UseProton); + } else { + e->flags(e->flags() & (~Executable::UseProton)); + } + + if (ui->useTerminal->isChecked()) { + e->flags(e->flags() | Executable::UseTerminal); + } else { + e->flags(e->flags() & (~Executable::UseTerminal)); + } + setDirty(true); } diff --git a/src/src/editexecutablesdialog.ui b/src/src/editexecutablesdialog.ui index 005f3a1..8ac01f6 100644 --- a/src/src/editexecutablesdialog.ui +++ b/src/src/editexecutablesdialog.ui @@ -464,6 +464,29 @@ Right now the only case I know of where this needs to be overwritten is for the + + + + Launch this executable through Proton (Wine compatibility layer). Disable for native Linux executables. + + + Use Proton + + + true + + + + + + + Open a terminal window showing the executable's output. Useful for debugging. + + + Open in terminal + + + diff --git a/src/src/executableslist.cpp b/src/src/executableslist.cpp index ea7a2e5..5410c81 100644 --- a/src/src/executableslist.cpp +++ b/src/src/executableslist.cpp @@ -88,6 +88,14 @@ void ExecutablesList::load(const MOBase::IPluginGame* game, const Settings& s) if (map["hide"].toBool()) flags |= Executable::Hide; + // Default to UseProton=true for backward compat (existing executables + // were always launched with Proton). Only set false if explicitly saved. + if (map.contains("useProton") ? map["useProton"].toBool() : true) + flags |= Executable::UseProton; + + if (map["useTerminal"].toBool()) + flags |= Executable::UseTerminal; + if (map.contains("custom")) { // the "custom" setting only exists in older versions needsUpgrade = true; @@ -126,6 +134,8 @@ void ExecutablesList::store(Settings& s) map["workingDirectory"] = item.workingDirectory(); map["steamAppID"] = item.steamAppID(); map["minimizeToSystemTray"] = item.minimizeToSystemTray(); + map["useProton"] = item.useProton(); + map["useTerminal"] = item.useTerminal(); v.push_back(std::move(map)); } @@ -482,6 +492,16 @@ bool Executable::hide() const return m_flags.testFlag(Hide); } +bool Executable::useProton() const +{ + return m_flags.testFlag(UseProton); +} + +bool Executable::useTerminal() const +{ + return m_flags.testFlag(UseTerminal); +} + void Executable::mergeFrom(const Executable& other) { // this happens after executables are loaded from settings and plugin diff --git a/src/src/executableslist.h b/src/src/executableslist.h index b402461..9ab3927 100644 --- a/src/src/executableslist.h +++ b/src/src/executableslist.h @@ -48,7 +48,9 @@ public: ShowInToolbar = 0x02, UseApplicationIcon = 0x04, Hide = 0x08, - MinimizeToSystemTray = 0x16 + MinimizeToSystemTray = 0x16, + UseProton = 0x20, + UseTerminal = 0x40 }; Q_DECLARE_FLAGS(Flags, Flag); @@ -79,6 +81,8 @@ public: bool usesOwnIcon() const; bool minimizeToSystemTray() const; bool hide() const; + bool useProton() const; + bool useTerminal() const; void mergeFrom(const Executable& other); diff --git a/src/src/processrunner.cpp b/src/src/processrunner.cpp index 724886b..dee7b73 100644 --- a/src/src/processrunner.cpp +++ b/src/src/processrunner.cpp @@ -1099,6 +1099,9 @@ ProcessRunner &ProcessRunner::setFromExecutable(const Executable &exe) { setCustomOverwrite(customOverwrite); setForcedLibraries(forcedLibraries); + m_sp.useProton = exe.useProton(); + m_sp.useTerminal = exe.useTerminal(); + return *this; } diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp index e2895e2..3d1741e 100644 --- a/src/src/protonlauncher.cpp +++ b/src/src/protonlauncher.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -134,6 +135,64 @@ QString detectSteamPath() return {}; } +// Detect an available terminal emulator on the system. +QString findTerminal() +{ + static const char* candidates[] = { + "konsole", "gnome-terminal", "xfce4-terminal", "alacritty", + "kitty", "foot", "xterm"}; + for (const auto* name : candidates) { + const QString path = QStandardPaths::findExecutable(name); + if (!path.isEmpty()) + return path; + } + return {}; +} + +// Wrap a program + args in a terminal emulator. +// Modifies program and arguments in-place. +void wrapInTerminal(QString& program, QStringList& arguments) +{ + const QString term = findTerminal(); + if (term.isEmpty()) { + MOBase::log::warn("No terminal emulator found; launching without terminal"); + return; + } + + const QString termName = QFileInfo(term).fileName(); + + // Build a shell command for the inner program + args. + // Append a read prompt so the terminal stays open after exit. + QString innerCmd = "'" + QString(program).replace("'", "'\\''") + "'"; + for (const auto& a : arguments) + innerCmd += " '" + QString(a).replace("'", "'\\''") + "'"; + innerCmd += "; echo; echo '--- Process exited with code '$?' ---'; " + "echo 'Press Enter to close...'; read"; + + QStringList termArgs; + if (termName == "konsole") { + termArgs << "--hold" << "-e" << "bash" << "-c" << innerCmd; + } else if (termName == "gnome-terminal") { + termArgs << "--" << "bash" << "-c" << innerCmd; + } else if (termName == "xfce4-terminal") { + termArgs << "--hold" << "-e" << ("bash -c " + innerCmd); + } else if (termName == "alacritty") { + termArgs << "-e" << "bash" << "-c" << innerCmd; + } else if (termName == "kitty") { + termArgs << "bash" << "-c" << innerCmd; + } else if (termName == "foot") { + termArgs << "bash" << "-c" << innerCmd; + } else { + // xterm and others + termArgs << "-e" << "bash" << "-c" << innerCmd; + } + + MOBase::log::info("Launching in terminal: {} {}", term, + termArgs.join(' ')); + program = term; + arguments = termArgs; +} + bool startWithEnv(const QString& program, const QStringList& arguments, const QString& workingDir, const QProcessEnvironment& environment, qint64& pid) @@ -321,6 +380,12 @@ ProtonLauncher& ProtonLauncher::addEnvVar(const QString& key, const QString& val return *this; } +ProtonLauncher& ProtonLauncher::setUseTerminal(bool useTerminal) +{ + m_useTerminal = useTerminal; + return *this; +} + std::pair ProtonLauncher::launch() const { qint64 pid = -1; @@ -416,6 +481,10 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const env.insert("PWD", m_workingDir); } + if (m_useTerminal) { + wrapInTerminal(program, arguments); + } + return startWithEnv(program, arguments, m_workingDir, env, pid); } @@ -439,6 +508,10 @@ bool ProtonLauncher::launchDirect(qint64& pid) const env.insert(it.key(), it.value()); } + if (m_useTerminal) { + wrapInTerminal(program, arguments); + } + return startWithEnv(program, arguments, m_workingDir, env, pid); } diff --git a/src/src/protonlauncher.h b/src/src/protonlauncher.h index 363d92b..11d942a 100644 --- a/src/src/protonlauncher.h +++ b/src/src/protonlauncher.h @@ -23,6 +23,7 @@ public: ProtonLauncher& setSteamDrm(bool useSteamDrm); ProtonLauncher& setStoreVariant(const QString& variant); ProtonLauncher& addEnvVar(const QString& key, const QString& value); + ProtonLauncher& setUseTerminal(bool useTerminal); // Launch dispatch: Proton -> Direct std::pair launch() const; @@ -43,6 +44,7 @@ private: QString m_storeVariant; // "GOG", "Epic", or empty for Steam QMap m_envVars; QMap m_wrapperEnvVars; + bool m_useTerminal = false; }; #endif // PROTONLAUNCHER_H diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp index 4c91fbe..b0e7eea 100644 --- a/src/src/spawn.cpp +++ b/src/src/spawn.cpp @@ -636,54 +636,58 @@ int spawn(const SpawnParameters &sp, pid_t &processId) { logSpawning(sp, bin + " " + sp.arguments); - // Read per-instance settings from the instance INI (not the global - // QSettings). - const Settings *instanceForLaunch = Settings::maybeInstance(); - bool useSteamDrm = true; // default - QString storeVariant; - if (instanceForLaunch) { - const QSettings instanceIni(instanceForLaunch->filename(), - QSettings::IniFormat); - useSteamDrm = instanceIni.value("fluorine/steam_drm", true).toBool(); - // settingName() maps "General" section keys to top-level (no section - // prefix), so game_edition is stored as a plain key under [General] in the - // INI. - storeVariant = instanceIni.value("game_edition").toString().trimmed(); - } - ProtonLauncher launcher; launcher.setBinary(bin) .setArguments(argList) .setWorkingDir(cwd) - .setSteamAppId(parseSteamAppId(sp.steamAppID)) - .setSteamDrm(useSteamDrm) - .setStoreVariant(storeVariant); - - const QString prefixPath = resolvePrefixPath(); - if (prefixPath.isEmpty()) { - MOBase::log::warn( - "No Wine prefix configured - games may not launch correctly. " - "Configure a prefix in Settings > Proton or via fluorine-manager."); - } else if (!QDir(QDir(prefixPath).filePath("drive_c")).exists()) { - MOBase::log::warn( - "Wine prefix '{}' does not contain drive_c/ - prefix may be invalid", - prefixPath); - } else { - MOBase::log::info("Using Wine prefix: {}", prefixPath); - launcher.setPrefix(prefixPath); - } + .setSteamAppId(parseSteamAppId(sp.steamAppID)); + + if (sp.useProton) { + // Read per-instance settings from the instance INI (not the global + // QSettings). + const Settings *instanceForLaunch = Settings::maybeInstance(); + bool useSteamDrm = true; // default + QString storeVariant; + if (instanceForLaunch) { + const QSettings instanceIni(instanceForLaunch->filename(), + QSettings::IniFormat); + useSteamDrm = instanceIni.value("fluorine/steam_drm", true).toBool(); + storeVariant = instanceIni.value("game_edition").toString().trimmed(); + } - const QString protonPath = resolveProtonPath(); - if (!protonPath.isEmpty()) { - launcher.setProtonPath(protonPath); - } + launcher.setSteamDrm(useSteamDrm) + .setStoreVariant(storeVariant); + + const QString prefixPath = resolvePrefixPath(); + if (prefixPath.isEmpty()) { + MOBase::log::warn( + "No Wine prefix configured - games may not launch correctly. " + "Configure a prefix in Settings > Proton or via fluorine-manager."); + } else if (!QDir(QDir(prefixPath).filePath("drive_c")).exists()) { + MOBase::log::warn( + "Wine prefix '{}' does not contain drive_c/ - prefix may be invalid", + prefixPath); + } else { + MOBase::log::info("Using Wine prefix: {}", prefixPath); + launcher.setPrefix(prefixPath); + } + + const QString protonPath = resolveProtonPath(); + if (!protonPath.isEmpty()) { + launcher.setProtonPath(protonPath); + } - const QString wrapper = - QSettings().value("fluorine/launch_wrapper").toString().trimmed(); - if (!wrapper.isEmpty()) { - launcher.setWrapper(wrapper); + const QString wrapper = + QSettings().value("fluorine/launch_wrapper").toString().trimmed(); + if (!wrapper.isEmpty()) { + launcher.setWrapper(wrapper); + } + } else { + MOBase::log::info("Proton disabled for this executable, launching directly"); } + launcher.setUseTerminal(sp.useTerminal); + const auto [ok, pid] = launcher.launch(); if (!ok) { return (errno != 0 ? errno : EIO); diff --git a/src/src/spawn.h b/src/src/spawn.h index d922ddc..378c20f 100644 --- a/src/src/spawn.h +++ b/src/src/spawn.h @@ -57,6 +57,8 @@ struct SpawnParameters QDir currentDirectory; QString steamAppID; bool hooked = false; + bool useProton = true; + bool useTerminal = false; #ifdef _WIN32 HANDLE stdOut = INVALID_HANDLE_VALUE; HANDLE stdErr = INVALID_HANDLE_VALUE; -- cgit v1.3.1