use std::path::PathBuf;
use clap::Args;
use nord_format::Entity;
use crate::edit::{print_byte_diff, write_file};
use crate::editors::{self, ProjectEditor, SampleEditor, SongEditor};
use crate::ui::Ui;
use crate::EditArgs;
#[derive(Args)]
pub struct FileEditArgs {
pub file: PathBuf,
#[arg(long = "set", value_name = "PATH=VALUE")]
pub set: Vec<String>,
#[arg(long)]
pub dry_run: bool,
#[arg(long)]
pub fields: bool,
#[arg(short, long, value_name = "FILE")]
pub out: Option<PathBuf>,
#[arg(long)]
pub yes: bool,
}
pub fn editable(entity: &Entity) -> bool {
entity.registry().is_some()
|| matches!(
entity,
Entity::Song(nord_format::Song::Electro5(_))
| Entity::Sample(_)
| Entity::SampleProject(_)
)
}
pub fn run(ui: &Ui, args: FileEditArgs) -> Result<(), String> {
let path = &args.file;
let original = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
let mut entity = nord_format::from_stream(&mut std::io::Cursor::new(&original))
.map_err(|e| format!("{}: {e}", path.display()))?;
let noun_args = EditArgs {
target: None,
set: args.set.clone(),
dry_run: args.dry_run,
fields: args.fields,
out: None,
yes: args.yes,
};
let staged = if entity.registry().is_some() {
crate::edit::stage(ui, &noun_args, entity.registry_mut().unwrap())?
} else {
match &mut entity {
Entity::Song(nord_format::Song::Electro5(song)) => {
editors::stage(ui, args.fields, &args.set, &mut SongEditor(song))?
}
Entity::Sample(sample) => {
editors::stage(ui, args.fields, &args.set, &mut SampleEditor(sample))?
}
Entity::SampleProject(project) => {
editors::stage(ui, args.fields, &args.set, &mut ProjectEditor(project))?
}
_ => {
let id = entity.identity();
return Err(format!(
"nothing in a {} ({}) is settable yet; `nord inspect` still reads it",
id.kind, id.format,
));
}
}
};
let Some(changed) = staged else {
return Ok(());
};
if changed == 0 {
ui.note("no field changed; writing nothing");
return Ok(());
}
let edited = nord_format::to_bytes(&entity).map_err(|e| e.to_string())?;
print_byte_diff(ui, &original, &edited);
if args.dry_run {
ui.note("--dry-run: nothing written");
return Ok(());
}
match args.out {
Some(out) => write_file(ui, &out, &edited),
None => {
ui.note(format!(
"about to {} {} in place",
ui.danger("overwrite"),
path.display()
));
ui.confirm(args.yes)?;
write_file(ui, path, &edited)
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use nord_format::cbin::{Cbin, Header};
use nord_format::formats::{ne5, ns3};
pub fn stage3_program() -> Vec<u8> {
let body =
ns3::program::Program::try_from([0u8; ns3::program::BODY_LEN]).expect("legal body");
let file = Cbin {
header: Header::new(
ns3::program::FORMAT,
(0, 0),
ns3::program::KNOWN_VERSIONS[0],
),
body,
};
let mut out = std::io::Cursor::new(Vec::new());
file.write_to(&mut out).unwrap();
out.into_inner()
}
#[test]
fn editable_knows_every_shape() {
let stage3 = nord_format::from_stream(&mut std::io::Cursor::new(stage3_program())).unwrap();
assert!(editable(&stage3));
let song = Entity::Song(nord_format::Song::Electro5(ne5::song::new(
(0, 0).try_into().unwrap(),
ne5::song::DEFAULT_VERSION,
[(0, 0).try_into().unwrap(); 4],
)));
assert!(editable(&song));
let project = Entity::SampleProject(
nord_format::formats::nsmpproj::Project::new(
"X",
&[nord_format::formats::nsmpproj::NewZone {
path: "x.wav".into(),
sample_rate: 44100,
frames: 44100,
root_key: 60,
}],
0,
)
.unwrap(),
);
assert!(editable(&project));
let stub = Entity::PipeLibrary(Cbin {
header: Header::new("npip", (0xffff, 0xffff), 1),
body: nord_format::cbin::RawBody(vec![0; 4]),
});
assert!(!editable(&stub));
}
}