Files
effector/src/cli/options.cpp
T
SiandClaude Opus 5 eb417bb41a Let a chain stage be switched off without taking it apart
Trying a stage out meant removing it and building it again from scratch,
settings and all, to see what the picture looked like without it. A stage now
carries an `enabled` flag and can simply be switched off: it keeps its place
in the chain and everything it was set to.

`enabledStages` is the one place that reads the flag, and renderChain,
chainNeedsSources, chainTitle and chainSlug all go through it. A disabled
stage is skipped whole — handed no image and drawing nothing from the shared
rng — so switching one off renders byte for byte what the chain rendered
before that stage was added, which is the only thing that makes the switch
worth anything while comparing. Whichever stage runs first is the one handed
the sources, so disabling a leading scanlines lets blur lead and the chain
needs no images at all. The status line and the generated file name follow
too: they name what was rendered, not what sits in the sidebar. A chain with
every stage disabled throws rather than quietly rendering nothing.

In the interface it is a check on the chain row: the row dims, the params
heading says "(off)" and the settings stay editable — a stage being worked on
is not the same as one being deleted. On the command line it is
`--disable NAME`, which picks its stage by effect name exactly as the params
do. A project writes `"enabled": false` only for a stage that is off, so
existing project files are unchanged and an ordinary one stays uncluttered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 21:11:17 +02:00

228 lines
9.8 KiB
C++

#include "cli/options.hpp"
#include <algorithm>
#include <charconv>
#include <cstdint>
#include <cstring>
#include <sstream>
#include "core/canvas.hpp"
#include "core/chain.hpp"
#include "core/effect.hpp"
#include "core/registry.hpp"
namespace effector {
namespace {
bool parseLong(const std::string& text, long& out) {
const char* begin = text.data();
const char* end = begin + text.size();
const auto result = std::from_chars(begin, end, out);
return result.ec == std::errc{} && result.ptr == end;
}
bool parseSize(const std::string& text, int& width, int& height) {
const auto sep = text.find_first_of("xX*,");
if (sep == std::string::npos) return false;
long w = 0;
long h = 0;
if (!parseLong(text.substr(0, sep), w) || !parseLong(text.substr(sep + 1), h)) return false;
// Checked as longs, before the narrowing: 4294968296 cast straight to int
// is 1000, which would render a plausible canvas nobody asked for.
if (w < kMinCanvasSide || w > kMaxCanvasSide) return false;
if (h < kMinCanvasSide || h > kMaxCanvasSide) return false;
width = static_cast<int>(w);
height = static_cast<int>(h);
return true;
}
// Every effect's params share one flag namespace, exactly as in the interface.
const ParamSpec* findSpec(const std::string& flag) {
for (const auto& effect : effects()) {
for (const auto& spec : effect->params()) {
if (spec.flag() == flag) return &spec;
}
}
return nullptr;
}
std::string requireValue(int argc, char** argv, int& i, const std::string& flag) {
if (i + 1 >= argc) throw EffectError(flag + " needs a value");
return argv[++i];
}
} // namespace
Options parseOptions(int argc, char** argv) {
Options options;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
if (arg == "-h" || arg == "--help") {
options.showHelp = true;
} else if (arg == "--list-effects") {
options.listEffects = true;
} else if (arg == "-e" || arg == "--effect") {
const std::string name = requireValue(argc, argv, i, arg);
effectByName(name); // validate now, fail early
options.effects.push_back(name);
} else if (arg == "-o" || arg == "--output") {
options.output = requireValue(argc, argv, i, arg);
} else if (arg == "-s" || arg == "--size") {
const std::string value = requireValue(argc, argv, i, arg);
if (!parseSize(value, options.width, options.height)) {
throw EffectError("invalid size '" + value + "', expected e.g. 1920x1080 with "
"each side between " + std::to_string(kMinCanvasSide) + " and " +
std::to_string(kMaxCanvasSide));
}
} else if (arg == "--format") {
const std::string value = requireValue(argc, argv, i, arg);
if (canvasFormatByKey(value) == nullptr) {
throw EffectError("unknown canvas format '" + value + "', see --help");
}
options.format = value;
} else if (arg == "--dpi") {
const std::string value = requireValue(argc, argv, i, arg);
long dpi = 0;
if (!parseLong(value, dpi) || dpi < kMinDpi || dpi > kMaxDpi) {
throw EffectError("invalid dpi '" + value + "', expected " +
std::to_string(kMinDpi) + "-" + std::to_string(kMaxDpi));
}
options.dpi = static_cast<int>(dpi);
} else if (arg == "--disable") {
const std::string name = requireValue(argc, argv, i, arg);
effectByName(name); // validate now, exactly as -e does
options.disabled.push_back(name);
} else if (arg == "--seed") {
const std::string value = requireValue(argc, argv, i, arg);
long seed = 0;
if (!parseLong(value, seed)) throw EffectError("invalid seed '" + value + "'");
options.seed = static_cast<std::uint64_t>(seed);
} else if (arg.rfind("--no-", 0) == 0) {
const std::string flag = "--" + arg.substr(5);
const ParamSpec* spec = findSpec(flag);
if (spec == nullptr || spec->type != ParamSpec::Type::Bool) {
throw EffectError("unknown option '" + arg + "'");
}
options.params[spec->key] = false;
} else if (arg.rfind("--", 0) == 0) {
const ParamSpec* spec = findSpec(arg);
if (spec == nullptr) throw EffectError("unknown option '" + arg + "'");
switch (spec->type) {
case ParamSpec::Type::Bool:
options.params[spec->key] = true;
break;
case ParamSpec::Type::Int: {
long value = 0;
const std::string text = requireValue(argc, argv, i, arg);
if (!parseLong(text, value)) {
throw EffectError(arg + ": '" + text + "' is not a whole number");
}
options.params[spec->key] = value;
break;
}
case ParamSpec::Type::Double: {
const std::string text = requireValue(argc, argv, i, arg);
try {
options.params[spec->key] = std::stod(text);
} catch (const std::exception&) {
throw EffectError(arg + ": '" + text + "' is not a number");
}
break;
}
case ParamSpec::Type::Path:
case ParamSpec::Type::Text:
options.params[spec->key] = requireValue(argc, argv, i, arg);
break;
case ParamSpec::Type::Color: {
const std::string text = requireValue(argc, argv, i, arg);
std::uint8_t rgb[3];
if (!parseColor(text, rgb)) {
throw EffectError(arg + ": '" + text +
"' is not a colour, expected e.g. #ff8800");
}
options.params[spec->key] = formatColor(rgb);
break;
}
case ParamSpec::Type::Choice: {
const std::string text = requireValue(argc, argv, i, arg);
bool valid = false;
for (const auto& choice : spec->choices) valid = valid || choice == text;
if (!valid) throw EffectError(arg + ": unknown value '" + text + "'");
options.params[spec->key] = text;
break;
}
}
} else if (arg.rfind("-", 0) == 0 && arg.size() > 1) {
throw EffectError("unknown option '" + arg + "'");
} else {
options.images.push_back(arg);
}
}
// Applied once everything is parsed, so --dpi may come before --format and
// so a format always wins over an -s given alongside it.
if (const auto* format = canvasFormatByKey(options.format)) {
canvasFormatSize(*format, options.dpi, options.width, options.height);
}
return options;
}
std::string usage(const char* programName) {
std::ostringstream out;
out << "Usage: " << programName << " [options] [image ...]\n\n"
<< "Image effects app. Runs the GTK interface unless --output is given.\n\n"
<< "Options:\n"
<< " -e, --effect NAME effect to apply, repeat to cascade (--output"
" defaults to " << defaultEffectName() << ")\n"
<< " -o, --output FILE render headlessly to this file and exit\n"
<< " -s, --size WxH canvas size, each side " << kMinCanvasSide << "-"
<< kMaxCanvasSide << " (default: 1920x1080)\n"
<< " --format NAME canvas format, overrides --size (see below)\n"
<< " --dpi N resolution of a print format, " << kMinDpi << "-" << kMaxDpi
<< " (default: " << kDefaultDpi << ")\n"
<< " --disable NAME keep this effect in the chain but skip it\n"
<< " --seed N random seed (default: random)\n"
<< " --list-effects list available effects and exit\n"
<< " -h, --help show this help and exit\n";
out << "\nCanvas formats (--format):\n";
for (const auto& format : canvasFormats()) {
out << " " << format.key;
if (format.key.size() < 20) out << std::string(20 - format.key.size(), ' ');
out << " " << format.label << ", " << canvasFormatDetail(format)
<< (format.print ? " at --dpi" : "") << "\n";
}
for (const auto& effect : effects()) {
out << "\n" << effect->title() << " options:\n";
for (const auto& spec : effect->params()) {
std::string flag = spec.flag();
if (spec.type != ParamSpec::Type::Bool) flag += " VALUE";
out << " " << flag;
if (flag.size() < 20) out << std::string(20 - flag.size(), ' ');
out << " " << (spec.help.empty() ? spec.label : spec.help) << "\n";
}
}
return out.str();
}
std::vector<ChainStage> buildChain(const Options& options) {
std::vector<ChainStage> stages;
for (const auto& name : options.effects) {
ChainStage stage;
stage.effect = effectByName(name);
stage.values = stage.effect->defaults();
stage.enabled = std::find(options.disabled.begin(), options.disabled.end(), name) ==
options.disabled.end();
for (const auto& spec : stage.effect->params()) {
const auto it = options.params.find(spec.key);
if (it != options.params.end()) stage.values[spec.key] = it->second;
}
stages.push_back(std::move(stage));
}
return stages;
}
} // namespace effector