launch options

This commit is contained in:
Boof2015
2026-08-15 20:04:33 -04:00
parent 0763d84d84
commit 6debfb8b34
10 changed files with 156 additions and 11 deletions
+17 -1
View File
@@ -40,6 +40,20 @@ ParseResult parseArguments(const std::vector<std::string>& arguments) {
result.options.deviceId = arguments[++index];
continue;
}
if (argument == "--profile" || argument == "--theme") {
if (index + 1 >= arguments.size() || arguments[index + 1].empty() ||
arguments[index + 1][0] == '-') {
return {false, {}, argument + " requires a non-empty name or ID."};
}
std::string& selector = argument == "--profile"
? result.options.profileSelector
: result.options.themeSelector;
if (!selector.empty()) {
return {false, {}, argument + " may only be specified once."};
}
selector = arguments[++index];
continue;
}
return {false, {}, "Unknown argument: " + argument};
}
@@ -48,7 +62,7 @@ ParseResult parseArguments(const std::vector<std::string>& arguments) {
std::string usageText() {
return
"Usage: prism-tui [--output <id>]\n"
"Usage: prism-tui [--output <id>] [--profile <name>] [--theme <name>]\n"
" prism-tui --list-outputs\n"
" prism-tui --help\n"
" prism-tui --version\n\n"
@@ -57,6 +71,8 @@ std::string usageText() {
" --list-outputs List available system output devices.\n"
" --device <id> Alias for --output.\n"
" --list-devices Alias for --list-outputs.\n"
" --profile <name> Start with a saved profile (name or ID).\n"
" --theme <name> Start with a Prism .iro theme (name or ID).\n"
" -h, --help Show this help.\n"
" -V, --version Show the Prism TUI version.\n\n"
"Controls:\n"
+2
View File
@@ -15,6 +15,8 @@ enum class Command {
struct Options {
Command command = Command::Run;
std::string deviceId;
std::string profileSelector;
std::string themeSelector;
};
struct ParseResult {
+3 -1
View File
@@ -72,7 +72,9 @@ int run(const std::vector<std::string>& arguments) {
std::move(capture),
started,
parsed.options.deviceId,
outputDevices);
outputDevices,
parsed.options.profileSelector,
parsed.options.themeSelector);
}
} // namespace
+26 -1
View File
@@ -251,6 +251,24 @@ const TuiProfile* TuiProfileLibrary::find(const std::string& id) const {
return managed ? &managed->profile : nullptr;
}
const TuiProfile* TuiProfileLibrary::findSelector(
const std::string& nameOrId) const {
if (const auto* exactId = find(nameOrId)) return exactId;
const auto exactName = std::find_if(
managed_.begin(), managed_.end(), [&](const auto& entry) {
return entry.profile.name == nameOrId;
});
if (exactName != managed_.end()) return &exactName->profile;
const std::string expected = lowercaseAscii(trimName(nameOrId));
const auto insensitive = std::find_if(
managed_.begin(), managed_.end(), [&](const auto& entry) {
return lowercaseAscii(entry.profile.id) == expected ||
lowercaseAscii(entry.profile.name) == expected;
});
return insensitive == managed_.end() ? nullptr : &insensitive->profile;
}
TuiProfileLibrary::ManagedProfile* TuiProfileLibrary::findManaged(
const std::string& id) {
const auto found = std::find_if(
@@ -275,12 +293,19 @@ bool TuiProfileLibrary::writeActiveState(std::string* error) const {
}
bool TuiProfileLibrary::activate(const std::string& id, std::string* error) {
if (!selectForSession(id, error)) return false;
return writeActiveState(error);
}
bool TuiProfileLibrary::selectForSession(
const std::string& id,
std::string* error) {
if (!findManaged(id)) {
if (error) *error = "profile was not found";
return false;
}
activeProfileId_ = id;
return writeActiveState(error);
return true;
}
bool TuiProfileLibrary::nameIsAvailable(
+3
View File
@@ -27,8 +27,11 @@ public:
const std::vector<TuiProfile>& profiles() const { return profiles_; }
const std::string& activeProfileId() const { return activeProfileId_; }
const TuiProfile* find(const std::string& id) const;
const TuiProfile* findSelector(const std::string& nameOrId) const;
bool activate(const std::string& id, std::string* error = nullptr);
bool selectForSession(const std::string& id,
std::string* error = nullptr);
bool saveNew(const std::string& name,
const TuiSettings& settings,
std::string* createdId = nullptr,
+43 -7
View File
@@ -28,6 +28,7 @@
#include <cstdio>
#include <exception>
#include <iomanip>
#include <iostream>
#include <optional>
#include <sstream>
#include <stdexcept>
@@ -2359,12 +2360,13 @@ bool stdinAndStdoutAreTerminals() {
int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
const Prism::Capture::StartResult& started,
std::string requestedDeviceId,
std::vector<Prism::Capture::OutputDevice> outputDevices) {
std::vector<Prism::Capture::OutputDevice> outputDevices,
std::string startupProfileSelector,
std::string startupThemeSelector) {
using namespace ftxui;
signalRequested = 0;
SignalHandlerGuard signalHandlerGuard;
ScreenInteractive screen = ScreenInteractive::Fullscreen();
SnapshotStore<DisplayFrame> frameStore;
SnapshotStore<TuiSettings> settingsStore;
SnapshotStore<OutputSwitchRequest> outputSwitchRequestStore;
@@ -2382,22 +2384,56 @@ int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
} else if (!themeLoadWarning.empty()) {
interfaceState.settingsStatus = themeLoadWarning;
}
if (!themeLibrary.find(interfaceState.settings.themeId)) {
interfaceState.settings.themeId = "Default";
}
interfaceState.theme = themeLibrary.resolve(interfaceState.settings.themeId);
TuiProfileLibrary profileLibrary(
defaultProfileDirectory(), defaultProfileStatePath());
std::string profileLoadError;
if (!profileLibrary.load(&profileLoadError)) {
const bool profilesLoaded = profileLibrary.load(&profileLoadError);
if (!profilesLoaded) {
interfaceState.profileStatus =
"Could not load profiles: " + profileLoadError;
interfaceState.profileStatusError = true;
}
if (!startupProfileSelector.empty()) {
if (!profilesLoaded) {
std::cerr << "prism-tui: could not load profiles: "
<< profileLoadError << '\n';
return 1;
}
const TuiProfile* profile =
profileLibrary.findSelector(startupProfileSelector);
if (!profile) {
std::cerr << "prism-tui: profile not found: "
<< startupProfileSelector << '\n';
return 2;
}
interfaceState.settings = applyProfileSettings(
profile->settings, interfaceState.settings);
std::string selectionError;
if (!profileLibrary.selectForSession(profile->id, &selectionError)) {
std::cerr << "prism-tui: could not select profile: "
<< selectionError << '\n';
return 1;
}
}
if (!startupThemeSelector.empty()) {
const TuiTheme* theme = themeLibrary.findSelector(startupThemeSelector);
if (!theme) {
std::cerr << "prism-tui: theme not found: "
<< startupThemeSelector << '\n';
return 2;
}
interfaceState.settings.themeId = theme->id;
}
if (!themeLibrary.find(interfaceState.settings.themeId)) {
interfaceState.settings.themeId = "Default";
}
interfaceState.theme = themeLibrary.resolve(interfaceState.settings.themeId);
interfaceState.profiles = profileLibrary.profiles();
interfaceState.activeProfileId = profileLibrary.activeProfileId();
interfaceState.profileDirty =
calculateUnsavedProfileChanges(interfaceState);
ScreenInteractive screen = ScreenInteractive::Fullscreen();
settingsStore.publish(interfaceState.settings);
DisplayFrame initial;
initial.magnitudes.assign(kDefaultFftSize / 2, -100.0f);
+3 -1
View File
@@ -12,6 +12,8 @@ bool stdinAndStdoutAreTerminals();
int runInteractive(std::unique_ptr<Prism::Capture::SystemAudioCapture> capture,
const Prism::Capture::StartResult& started,
std::string requestedDeviceId,
std::vector<Prism::Capture::OutputDevice> outputDevices);
std::vector<Prism::Capture::OutputDevice> outputDevices,
std::string startupProfileSelector = {},
std::string startupThemeSelector = {});
} // namespace Prism::Tui
+18
View File
@@ -826,6 +826,24 @@ const TuiTheme* IroThemeLibrary::find(const std::string& id) const {
return found == themes_.end() ? nullptr : &*found;
}
const TuiTheme* IroThemeLibrary::findSelector(
const std::string& nameOrId) const {
if (const auto* exactId = find(nameOrId)) return exactId;
const auto exactName = std::find_if(
themes_.begin(), themes_.end(), [&](const TuiTheme& theme) {
return theme.name == nameOrId;
});
if (exactName != themes_.end()) return &*exactName;
const std::string expected = normalizeKey(nameOrId);
const auto insensitive = std::find_if(
themes_.begin(), themes_.end(), [&](const TuiTheme& theme) {
return normalizeKey(theme.id) == expected ||
normalizeKey(theme.name) == expected;
});
return insensitive == themes_.end() ? nullptr : &*insensitive;
}
const TuiTheme& IroThemeLibrary::resolve(const std::string& id) const {
if (const auto* theme = find(id)) return *theme;
if (!themes_.empty()) return themes_.front();
+1
View File
@@ -92,6 +92,7 @@ public:
bool load(std::string* warning = nullptr);
const std::vector<TuiTheme>& themes() const { return themes_; }
const TuiTheme* find(const std::string& id) const;
const TuiTheme* findSelector(const std::string& nameOrId) const;
const TuiTheme& resolve(const std::string& id) const;
std::string adjacentId(const std::string& id, int direction) const;
const std::filesystem::path& directory() const { return directory_; }
+40
View File
@@ -139,6 +139,15 @@ void testCli() {
"the output alias should retain its output ID");
require(Prism::Tui::parseArguments({"--list-outputs"}).options.command ==
Prism::Tui::Command::ListDevices, "the output-list alias should parse");
auto startup = Prism::Tui::parseArguments({
"--profile", "Studio Wide",
"--theme", "Alpha Centauri",
"--output", "output-id",
});
require(startup.ok && startup.options.profileSelector == "Studio Wide" &&
startup.options.themeSelector == "Alpha Centauri" &&
startup.options.deviceId == "output-id",
"profile, theme, and output startup selections should combine");
require(!Prism::Tui::parseArguments({"--device"}).ok, "missing device ID should fail");
require(!Prism::Tui::parseArguments({"--device", "--help"}).ok,
"an option should not be accepted as a device ID");
@@ -147,6 +156,14 @@ void testCli() {
"duplicate device options should fail");
require(!Prism::Tui::parseArguments({"--device", "fake", "--output", "fake"}).ok,
"mixed output aliases should still be rejected as duplicates");
require(!Prism::Tui::parseArguments({"--profile"}).ok &&
!Prism::Tui::parseArguments({"--theme", "--output"}).ok,
"startup selectors should require a value");
require(!Prism::Tui::parseArguments({
"--profile", "one", "--profile", "two"}).ok &&
!Prism::Tui::parseArguments({
"--theme", "one", "--theme", "two"}).ok,
"startup selectors should reject duplicate values");
require(!Prism::Tui::parseArguments({"--help", "--version"}).ok,
"exclusive commands should not combine");
require(Prism::Tui::usageText().find("Tab / Shift-Tab") != std::string::npos,
@@ -158,6 +175,9 @@ void testCli() {
require(Prism::Tui::usageText().find("--list-outputs") != std::string::npos &&
Prism::Tui::usageText().find("Choose the system output") != std::string::npos,
"help should describe output aliases and the in-app picker");
require(Prism::Tui::usageText().find("--profile <name>") != std::string::npos &&
Prism::Tui::usageText().find("--theme <name>") != std::string::npos,
"help should describe profile and theme startup selection");
}
void testOutputSwitching() {
@@ -529,6 +549,11 @@ line = 22, 23, 24
require(library.find("Redshift")->spectrumLine ==
Prism::Tui::ThemeColor{1, 2, 3},
"managed .iro files should override bundled themes with the same filename stem");
require(library.findSelector("alpha-centauri") &&
library.findSelector("alpha-centauri")->id == "Alpha Centauri" &&
library.findSelector("TEST THEME") &&
!library.findSelector("Not A Theme"),
"theme startup selectors should accept visible names and normalized IDs");
const std::string nextTheme = library.adjacentId("Default", 1);
require(!warning.empty() && nextTheme != "Default" &&
library.find(nextTheme) && library.adjacentId(nextTheme, -1) == "Default",
@@ -688,6 +713,21 @@ void testProfileLibrary() {
Prism::Tui::profileSettingsEqual(
library.find(profileId)->settings, settings),
"saving a profile should activate and preserve its scoped settings");
require(library.findSelector("studio wide") &&
library.findSelector("studio wide")->id == profileId &&
library.findSelector(profileId) &&
!library.findSelector("Missing Profile"),
"profile startup selectors should accept names and internal IDs");
require(library.activate(Prism::Tui::kDefaultTuiProfileId, &error) &&
library.selectForSession(profileId, &error) &&
library.activeProfileId() == profileId,
"startup profile selection should update the active session");
Prism::Tui::TuiProfileLibrary sessionReload(profilesPath, statePath);
require(sessionReload.load(&error) &&
sessionReload.activeProfileId() == Prism::Tui::kDefaultTuiProfileId,
"startup profile selection should not rewrite the persisted active profile");
require(library.activate(profileId, &error),
"profile fixtures should restore their persisted active selection");
bool foundProfileFile = false;
for (const auto& entry : std::filesystem::directory_iterator(profilesPath)) {