use anyhow::{bail, ensure, Result};
use clap::builder::styling::{AnsiColor, Effects, Styles};
use clap::{Parser, Subcommand};
use log::debug;
use parse_int::parse;
use ultimate64::{
aux,
drives::{self, Drive},
vicstream, Rest,
};
extern crate pretty_env_logger;
use pretty_env_logger::env_logger::DEFAULT_FILTER_ENV;
use prettytable::{format, Cell, Row, Table};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use url::{Host, Url};
const BASIC_LOAD_ADDR: u16 = 0x0801;
fn styles() -> Styles {
Styles::styled()
.header(AnsiColor::Yellow.on_default() | Effects::BOLD)
.usage(AnsiColor::Red.on_default() | Effects::BOLD)
.literal(AnsiColor::Blue.on_default() | Effects::BOLD)
.placeholder(AnsiColor::Green.on_default())
}
fn has_disk_image_extension<P: AsRef<Path>>(file: P) -> Result<()> {
drives::DiskImageType::from_file_name(file).map(|_| ())
}
#[derive(Debug, Parser)] #[command(name = "ultimate64")]
#[command(author = "Mikael Lund aka Wombat")]
#[command(about = "Network Control for Ultimate series", version)]
#[command(color = clap::ColorChoice::Auto)]
#[command(styles=styles())]
struct Cli {
#[clap(env = "ULTIMATE_HOST")]
#[arg(value_parser = Host::parse)]
host: Host,
#[command(subcommand)]
command: Commands,
#[clap(long, short = 'v', action)]
pub verbose: bool,
#[clap(env = "ULTIMATE_PASSWORD")]
#[clap(long, short = 'p')]
pub password: Option<String>,
}
#[derive(Debug, Subcommand)]
enum Commands {
Drives,
Info,
Load {
file: PathBuf,
#[clap(long, short = '@', default_value = None)]
#[arg(value_parser = parse::<u16>)]
address: Option<u16>,
#[clap(long, short = 'r', action, default_value_t = false)]
run: bool,
#[clap(long, action, default_value_t = false)]
reset: bool,
},
Menu,
Mount {
file: PathBuf,
#[clap(long, short = 'd', default_value = "a")]
drive: String,
#[clap(long, short = 'm', default_value = "ro")]
#[arg(value_enum)]
mode: drives::MountMode,
#[clap(long, short = 'r', action, default_value_t = false)]
run: bool,
},
Pause,
Peek {
#[arg(value_parser = parse::<u16>)]
address: u16,
#[clap(long, short = 'n', default_value = "1")]
#[arg(value_parser = parse::<u16>)]
length: u16,
#[clap(long, short = 'o')]
outfile: Option<PathBuf>,
#[clap(long = "dasm", short = 'd', action, conflicts_with = "outfile")]
disassemble: bool,
},
Play {
file: PathBuf,
#[clap(short = 'n')]
#[arg(value_parser = parse::<u8>)]
songnr: Option<u8>,
},
Poke {
#[arg(value_parser = parse::<u16>)]
address: u16,
#[arg(value_parser = parse::<u8>)]
value: u8,
#[clap(long = "and", action, conflicts_with_all = ["bitwise_or", "bitwise_xor"])]
bitwise_and: bool,
#[clap(long = "or", action, conflicts_with_all = ["bitwise_and", "bitwise_xor"])]
bitwise_or: bool,
#[clap(long = "xor", action, conflicts_with_all = ["bitwise_and", "bitwise_or"])]
bitwise_xor: bool,
#[clap(long, short = 'f', conflicts_with_all = ["bitwise_and", "bitwise_or", "bitwise_xor"])]
#[arg(value_parser = parse::<u16>)]
fill: Option<u16>,
},
Poweroff,
Reboot,
Reset,
Resume,
#[command(arg_required_else_help = true)]
Run {
file: PathBuf,
},
Screenshot {
#[clap(long, short = 'o', default_value = None)]
output: Option<PathBuf>,
#[clap(long, default_value = "http://239.0.1.64:11000")]
#[arg(value_parser = Url::parse)]
url: Url,
#[clap(long, short = 'x', default_value_t = 1)]
scale: u32,
},
Type {
text: String,
},
}
fn print_disassembled(bytes: &[u8], address: u16) -> Result<()> {
disasm6502::from_addr_array(bytes, address)
.unwrap()
.iter()
.for_each(|line| {
println!("{line}");
});
Ok(())
}
fn do_main() -> Result<()> {
let args = Cli::parse();
let ultimate = Rest::new(&args.host, args.password.clone())?;
if args.verbose && std::env::var(DEFAULT_FILTER_ENV).is_err() {
std::env::set_var(DEFAULT_FILTER_ENV, "Debug");
} else {
std::env::set_var(DEFAULT_FILTER_ENV, "Info");
}
pretty_env_logger::init();
match args.command {
Commands::Drives => {
let drives = ultimate.drive_list()?;
print_drive_table(drives);
}
Commands::Info => {
let info = ultimate.info()?;
println!("{info}");
}
Commands::Load {
file,
address,
run,
reset,
} => {
let data = fs::read(file)?;
if reset {
ultimate.reset()?;
}
let (address, _) = ultimate.load_data(&data, address)?;
if run {
if address == BASIC_LOAD_ADDR {
ultimate.type_text("run\n")?;
} else {
ultimate.type_text(&format!("sys{address}\n"))?
}
}
}
Commands::Menu => {
ultimate.menu()?;
}
Commands::Mount {
file,
drive: drive_id,
mode,
run,
} => {
has_disk_image_extension(&file)?;
ultimate.mount_disk_image(&file, drive_id, mode, run)?;
}
Commands::Pause => {
ultimate.pause()?;
}
Commands::Poweroff => {
ultimate.poweroff()?;
}
Commands::Peek {
address,
length,
outfile,
disassemble,
} => {
let data = ultimate.read_mem(address, length)?;
if disassemble {
print_disassembled(&data, address)?;
} else if outfile.is_some() {
fs::write(outfile.unwrap(), &data)?;
} else {
data.iter().for_each(|byte| {
print!("{byte:#04x} ");
});
println!()
}
}
Commands::Play { file, songnr } => {
let data = fs::read(&file)?;
let ext = aux::get_extension(&file).unwrap_or_default();
match ext.as_str() {
"sid" => ultimate.sid_play(&data, songnr)?,
"mod" => ultimate.mod_play(&data)?,
_ => bail!("Unsupported music file format: {ext}"),
}
}
Commands::Poke {
address,
value,
bitwise_and,
bitwise_or,
bitwise_xor,
fill,
} => {
if let Some(fill) = fill {
ensure!(fill > 0, "fill must be greater than zero");
let data = vec![value; fill as usize];
ultimate.write_mem(address, &data)?;
debug!(
"Filled [{:#06x}-{:#06x}] with {:#04x}",
address,
address + fill - 1,
value
);
return Ok(());
};
let value = if bitwise_and {
ultimate.read_mem(address, 1)?[0] & value
} else if bitwise_or {
ultimate.read_mem(address, 1)?[0] | value
} else if bitwise_xor {
ultimate.read_mem(address, 1)?[0] ^ value
} else {
value
};
debug!("Poke {value:#04x} to {address:#06x}");
ultimate.write_mem(address, &[value])?;
}
Commands::Reboot => {
ultimate.reboot()?;
}
Commands::Reset => {
ultimate.reset()?;
}
Commands::Resume => {
ultimate.resume()?;
}
Commands::Run { file } => {
let data = fs::read(&file)?;
match aux::get_extension(&file).unwrap_or_default().as_str() {
"crt" => ultimate.run_crt(&data)?,
_ => ultimate.run_prg(&data)?,
}
}
Commands::Screenshot { output, url, scale } => {
vicstream::take_snapshot(&url, output.as_deref(), Some(scale))?;
}
Commands::Type { text } => {
ultimate.type_text(&text)?;
}
}
Ok(())
}
fn print_drive_table(drives: HashMap<String, Drive>) {
let mut table = Table::new();
table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
table.set_titles(Row::new(vec![
Cell::new("Drive"),
Cell::new("Id"),
Cell::new("Type"),
Cell::new("Enabled"),
Cell::new("Image file"),
]));
for (name, drive) in drives {
table.add_row(Row::new(vec![
Cell::new(&name),
Cell::new(&drive.bus_id.to_string()),
Cell::new(
&drive
.drive_type
.map_or("Unknown".to_string(), |t| t.to_string()),
),
Cell::new(&drive.enabled.to_string()),
Cell::new(drive.image_file.as_deref().unwrap_or("")),
]));
}
table.printstd();
}
fn main() {
if let Err(err) = do_main() {
eprintln!("Error: {}", &err);
std::process::exit(1);
}
}