#![cfg_attr(
not(any(feature = "clap", feature = "vst3", feature = "au")),
allow(unused_variables, unused_imports)
)]
#[cfg(not(target_os = "macos"))]
fn main() {
eprintln!("truce-rack-screenshot currently supports macOS only");
std::process::exit(2);
}
#[cfg(target_os = "macos")]
use truce_rack_core::plugin::PluginCore;
#[cfg(target_os = "macos")]
use truce_rack_standalone::Format;
#[cfg(target_os = "macos")]
use truce_rack_standalone::screenshot;
#[cfg(target_os = "macos")]
#[cfg_attr(
not(any(feature = "clap", feature = "vst3", feature = "au")),
allow(dead_code)
)]
fn capture_one<P: PluginCore>(
plugin: &mut P,
format: Format,
name: &str,
label: &str,
output_dir: &std::path::Path,
) -> i32 {
let tag = format.tag();
let Some(editor) = plugin.editor() else {
println!("[skip] {tag} — {label} (no editor)");
return 0;
};
let (w, h) = editor.size().unwrap_or((640, 480));
let slug = screenshot::sanitize(name);
let out = output_dir.join(format!("{tag}-{slug}.png"));
match screenshot::capture_editor(editor, w, h, &out) {
Ok(()) => {
println!("[ok] {tag} — {label} -> {}", out.display());
0
}
Err(e) => {
println!("[err] {tag} — {label}: {e}");
1
}
}
}
#[cfg(target_os = "macos")]
#[allow(clippy::too_many_lines)]
fn main() {
let mut pargs = pico_args::Arguments::from_env();
if pargs.contains(["-h", "--help"]) {
print_help();
return;
}
let only_format: Option<String> = match pargs.opt_value_from_str("--format") {
Ok(v) => v,
Err(e) => {
eprintln!("error: --format: {e}");
std::process::exit(2);
}
};
let output_dir: std::path::PathBuf = match pargs.opt_value_from_str("--output-dir") {
Ok(Some(p)) => p,
Ok(None) => screenshot::default_output_dir(),
Err(e) => {
eprintln!("error: --output-dir: {e}");
std::process::exit(2);
}
};
let single: Option<String> = match pargs.opt_value_from_str("--single") {
Ok(v) => v,
Err(e) => {
eprintln!("error: --single: {e}");
std::process::exit(2);
}
};
let no_isolation = pargs.contains("--no-isolation");
let leftover = pargs.finish();
if !leftover.is_empty() {
eprintln!("unknown arguments: {leftover:?}");
std::process::exit(2);
}
if let Err(e) = std::fs::create_dir_all(&output_dir) {
eprintln!("error: create_dir_all {}: {e}", output_dir.display());
std::process::exit(1);
}
let only = only_format.as_deref().and_then(Format::parse);
if let Some(spec) = single {
single_mode(&spec, &output_dir);
return;
}
let exe = match std::env::current_exe() {
Ok(p) => p,
Err(e) => {
eprintln!("error: current_exe: {e}");
std::process::exit(1);
}
};
walk_and_capture(only, &exe, &output_dir, no_isolation);
}
#[cfg(target_os = "macos")]
fn walk_and_capture(
only: Option<Format>,
exe: &std::path::Path,
output_dir: &std::path::Path,
no_isolation: bool,
) {
use truce_rack_core::scanner::PluginScanner;
let mut targets: Vec<(Format, String, String, String)> = Vec::new();
#[cfg(feature = "clap")]
if only.is_none_or(|f| f == Format::Clap) {
let scanner = truce_rack_clap::ClapScanner::new();
for info in scanner.scan().unwrap_or_default() {
let label = format!("{} — {}", info.vendor, info.name);
targets.push((
Format::Clap,
info.name.clone(),
label,
info.unique_id.clone(),
));
}
}
#[cfg(feature = "vst3")]
if only.is_none_or(|f| f == Format::Vst3) {
let scanner = truce_rack_vst3::Vst3Scanner::new();
for info in scanner.scan().unwrap_or_default() {
let label = format!("{} — {}", info.vendor, info.name);
targets.push((
Format::Vst3,
info.name.clone(),
label,
info.unique_id.clone(),
));
}
}
#[cfg(all(feature = "au", target_vendor = "apple"))]
if only.is_none_or(|f| f == Format::Au) {
let scanner = truce_rack_au::AuScanner::new();
for info in scanner.scan().unwrap_or_default() {
let label = format!("{} — {}", info.vendor, info.name);
targets.push((Format::Au, info.name.clone(), label, info.unique_id.clone()));
}
}
eprintln!("[walk] {} plugins across formats", targets.len());
for (idx, (fmt, _name, label, uid)) in targets.iter().enumerate() {
let tag = fmt.tag();
eprintln!("[{}/{}] {tag} {label}", idx + 1, targets.len());
if no_isolation {
direct_capture(*fmt, uid, output_dir);
continue;
}
let spec = format!("{tag}:{uid}");
let child = std::process::Command::new(exe)
.arg("--single")
.arg(&spec)
.arg("--output-dir")
.arg(output_dir)
.stdin(std::process::Stdio::null())
.spawn();
let Ok(mut child) = child else {
eprintln!("[err] spawn: {}", child.err().unwrap());
continue;
};
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15);
loop {
match child.try_wait() {
Ok(Some(status)) if status.success() => break,
Ok(Some(status)) => {
eprintln!("[crash] {tag} {label}: {status}");
break;
}
Ok(None) => {
if std::time::Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
eprintln!("[timeout] {tag} {label}");
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
Err(e) => {
eprintln!("[err] wait: {e}");
break;
}
}
}
}
}
#[cfg(target_os = "macos")]
fn single_mode(spec: &str, output_dir: &std::path::Path) {
let Some((tag, uid)) = spec.split_once(':') else {
eprintln!("error: --single expects <format>:<unique_id>, got {spec:?}");
std::process::exit(2);
};
let Some(format) = Format::parse(tag) else {
eprintln!("error: --single: unknown format tag {tag:?}");
std::process::exit(2);
};
direct_capture(format, uid, output_dir);
}
#[cfg(target_os = "macos")]
fn direct_capture(format: Format, uid: &str, output_dir: &std::path::Path) {
use truce_rack_core::scanner::PluginScanner;
let tag = format.tag();
let code = match format {
#[cfg(feature = "clap")]
Format::Clap => {
let scanner = truce_rack_clap::ClapScanner::new();
scanner
.scan()
.unwrap_or_default()
.iter()
.find(|p| p.unique_id == uid)
.map_or_else(
|| {
eprintln!("[err] {tag} no match for {uid:?}");
2
},
|info| match scanner.load(info) {
Ok(mut p) => {
let label = format!("{} — {}", info.vendor, info.name);
capture_one(&mut p, Format::Clap, &info.name, &label, output_dir)
}
Err(e) => {
eprintln!("[err] {tag} load: {e}");
1
}
},
)
}
#[cfg(feature = "vst3")]
Format::Vst3 => {
let scanner = truce_rack_vst3::Vst3Scanner::new();
scanner
.scan()
.unwrap_or_default()
.iter()
.find(|p| p.unique_id == uid)
.map_or_else(
|| {
eprintln!("[err] {tag} no match for {uid:?}");
2
},
|info| match scanner.load(info) {
Ok(mut p) => {
let label = format!("{} — {}", info.vendor, info.name);
capture_one(&mut p, Format::Vst3, &info.name, &label, output_dir)
}
Err(e) => {
eprintln!("[err] {tag} load: {e}");
1
}
},
)
}
#[cfg(all(feature = "au", target_vendor = "apple"))]
Format::Au => {
let scanner = truce_rack_au::AuScanner::new();
scanner
.scan()
.unwrap_or_default()
.iter()
.find(|p| p.unique_id == uid)
.map_or_else(
|| {
eprintln!("[err] {tag} no match for {uid:?}");
2
},
|info| match scanner.load(info) {
Ok(mut p) => {
let label = format!("{} — {}", info.vendor, info.name);
capture_one(&mut p, Format::Au, &info.name, &label, output_dir)
}
Err(e) => {
eprintln!("[err] {tag} load: {e}");
1
}
},
)
}
#[allow(unreachable_patterns)]
_ => {
eprintln!("[err] {tag} format not enabled in this build");
2
}
};
std::process::exit(code);
}
#[cfg(target_os = "macos")]
fn print_help() {
println!(
"truce-rack-screenshot — capture every installed plugin's editor to PNG
USAGE:
truce-rack-screenshot [--format <clap|vst3|au>] [--output-dir <path>]
OPTIONS:
--format <fmt> Only scan this format (clap, vst3, or au)
--output-dir <path> Override the default ~/truce-rack-screenshots
--no-isolation Capture without per-plugin subprocess isolation
(one crashing plugin kills the whole run)
--single <fmt:uid> Capture exactly one plugin and exit; used
internally to spawn isolated workers
-h, --help Print this help"
);
}