mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-18 11:34:19 +02:00
launch options
This commit is contained in:
+17
-1
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user