use anyhow::Context;
use std::{io::Read, sync::mpsc};
use uxn::{Backend, Uxn, UxnRam};
use varvara::Varvara;
use anyhow::Result;
use eframe::egui;
use log::info;
use clap::Parser;
use crate::uxn::audio_setup;
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn test_cli_args_parsing() {
let argv = vec![
"cardinal-gui",
"--timeout=30",
"--widget",
"--ontop=true",
"--debug",
"--transparent=ffffff",
"--no-decorations",
"--scale=2.5",
"--native",
"--color_transform=invert",
"--color_params=1.0,2.0,3.0",
"test.rom",
"extra1",
"extra2",
];
let args = Args::parse_from(argv);
assert_eq!(args.timeout, Some(30.0));
assert!(args.widget);
assert_eq!(args.ontop, Some(true));
assert!(args.debug);
assert_eq!(args.transparent.as_deref(), Some("ffffff"));
assert!(args.no_decorations);
assert_eq!(args.scale, Some(2.5));
assert!(args.native);
assert_eq!(args.color_transform, "invert");
assert_eq!(args.color_params, vec![1.0, 2.0, 3.0]);
assert_eq!(args.rom, std::path::PathBuf::from("test.rom"));
assert_eq!(args.args, vec!["extra1", "extra2"]);
}
}
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None, disable_help_flag = true)]
struct Args {
#[clap(long)]
mouse_resize: Option<String>,
#[clap(long, default_value = "3")]
efxt: Option<f32>,
#[clap(long)]
efx: Option<String>,
#[clap(long)]
efxmode: Option<String>,
#[clap(long, short = 't')]
timeout: Option<f32>,
rom: std::path::PathBuf,
#[clap(long)]
scale: Option<f32>,
#[clap(long, short = 'x')]
x: Option<i32>,
#[clap(long, short = 'y')]
y: Option<i32>,
#[clap(long, short = 'w')]
w: Option<u32>,
#[clap(long, short = 'h')]
h: Option<u32>,
#[clap(long)]
fit: Option<String>,
#[clap(long)]
native: bool,
#[clap(long, value_name = "COLOR")]
transparent: Option<String>,
#[clap(long)]
no_decorations: bool,
#[clap(long)]
widget: bool,
#[clap(long)]
ontop: Option<bool>,
#[clap(long, short = 'd')]
debug: bool,
#[clap(long)]
mouse: Option<String>,
#[arg(trailing_var_arg = true)]
args: Vec<String>,
#[clap(long, alias = "color_transform", default_value = "none")]
color_transform: String,
#[clap(long, alias = "color_params", value_delimiter = ',')]
color_params: Vec<f32>,
}
pub fn run() -> Result<()> {
let args = Args::parse();
let mouse_resize = if let Some(ref resize_str) = args.mouse_resize {
match resize_str.to_lowercase().as_str() {
"none" => crate::stage::MouseResizeMode::None,
"middledrag" => crate::stage::MouseResizeMode::MiddleDrag,
_ => crate::stage::MouseResizeMode::None,
}
} else if args.widget {
crate::stage::MouseResizeMode::MiddleDrag
} else {
crate::stage::MouseResizeMode::None
};
let env = env_logger::Env::default()
.filter_or("UXN_LOG", "info")
.write_style_or("UXN_LOG", "always");
env_logger::init_from_env(env);
let args = Args::parse();
println!(
"[cardinal-gui] CLI args: {:?}",
std::env::args().collect::<Vec<_>>()
);
println!("[cardinal-gui] Parsed Args struct: {:?}", args);
println!(
"[cardinal-gui] Requested window size: w={:?}, h={:?}, scale={:?}, fit_mode={:?}",
args.w, args.h, args.scale, args.fit
);
let mouse_mode = if let Some(ref mouse_str) = args.mouse {
match mouse_str.to_lowercase().as_str() {
"none" => crate::stage::MouseDragMode::None,
"doubledrag" => crate::stage::MouseDragMode::DoubleDrag,
_ => crate::stage::MouseDragMode::None,
}
} else if args.widget {
crate::stage::MouseDragMode::DoubleDrag
} else {
crate::stage::MouseDragMode::None
};
println!("[cardinal-gui] Mouse drag mode: {:?}", mouse_mode);
let mut f =
std::fs::File::open(&args.rom).with_context(|| format!("failed to open {:?}", args.rom))?;
let mut rom = vec![];
f.read_to_end(&mut rom).context("failed to read file")?;
let ram = UxnRam::new();
let mut vm = Uxn::new(
ram.leak(),
if args.native {
#[cfg(not(target_arch = "aarch64"))]
anyhow::bail!("no native implementation for this arch");
#[cfg(target_arch = "aarch64")]
Backend::Native
} else {
Backend::Interpreter
},
);
let mut dev = Varvara::default();
let extra = vm.reset(&rom);
dev.reset(extra);
dev.init_args(&mut vm, &args.args);
let _audio = audio_setup(dev.audio_streams());
let start = std::time::Instant::now();
vm.run(&mut dev, 0x100);
info!("startup complete in {:?}", start.elapsed());
dev.output(&vm).check()?;
dev.send_args(&mut vm, &args.args).check()?;
let size @ (width, height) = dev.output(&vm).size;
let scale = args.scale.unwrap_or(if width < 320 { 2.0 } else { 1.0 });
info!("creating window with size ({width}, {height}) and scale {scale}");
let rom_title = args
.rom
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
let transparent = args.widget || args.transparent.is_some();
let transparent_color = if let Some(color) = args.transparent.as_deref() {
Some(color)
} else if args.widget {
Some("ffffff")
} else {
None
};
let no_decorations = args.widget || args.no_decorations;
let window_x = args.x.unwrap_or(0);
let window_y = args.y.unwrap_or(0);
let window_w = if args.w.is_some() {
args.w.unwrap()
} else {
(width as f32 * scale) as u32
};
let window_h = if args.h.is_some() {
args.h.unwrap()
} else {
(height as f32 * scale) as u32
};
let fit_mode = args.fit.clone().unwrap_or_else(|| "contain".to_string());
println!(
"[cardinal-gui] Requested window size: w={}, h={}, scale={}, fit_mode={}",
window_w, window_h, scale, fit_mode
);
let mut viewport_builder = egui::ViewportBuilder::default()
.with_inner_size([window_w as f32, window_h as f32])
.with_position([window_x as f32, window_y as f32])
.with_transparent(transparent)
.with_decorations(!no_decorations);
let ontop = match args.ontop {
Some(val) => val,
None => args.widget,
};
if ontop {
viewport_builder = viewport_builder.with_always_on_top();
}
let options = eframe::NativeOptions {
viewport: viewport_builder,
..Default::default()
};
let (tx, rx) = mpsc::channel();
varvara::spawn_console_worker(move |c| tx.send(crate::stage::Event::Console(c)));
let color_transform = args.color_transform.clone();
let color_params = args.color_params.clone();
let efx = args.efx.clone();
let efxt = args.efxt.unwrap_or(3.0);
let efxmode = args.efxmode.clone();
let fit_mode = args.fit.clone().unwrap_or_else(|| "contain".to_string());
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
let should_exit = Arc::new(AtomicBool::new(false));
let should_exit_clone = should_exit.clone();
let timeout_opt = args.timeout;
let run_result = eframe::run_native(
"Varvara",
options,
Box::new(move |cc| {
if let Some(timeout_secs) = timeout_opt {
println!("[cardinal-gui] Timeout set: {} seconds", timeout_secs);
let ctx = cc.egui_ctx.clone();
let should_exit = should_exit_clone.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs_f32(timeout_secs));
should_exit.store(true, Ordering::Relaxed);
ctx.request_repaint();
});
}
Ok(Box::new(crate::stage::Stage::new(
vm,
dev,
rx,
crate::stage::StageConfig {
size,
scale,
rom_title: rom_title.clone(),
transparent: transparent_color.map(|s| s.to_string()),
color_transform_name: color_transform,
color_params,
effects: crate::stage::EffectsConfig {
effect_mode: 0,
effect_order: (0..crate::effects::EFFECT_COUNT).collect(),
efx,
efxt,
efxmode,
efx_ndx: 0,
last_effect_switch: 0.0,
},
fit_mode,
mouse_mode,
mouse_resize,
},
Some(should_exit_clone.clone()),
)))
}),
);
run_result.map_err(|e| anyhow::anyhow!("got egui error: {e:?}"))?;
if should_exit.load(std::sync::atomic::Ordering::Relaxed) {
std::process::exit(0);
}
Ok(())
}