mod device;
mod edit;
mod editors;
mod file;
mod file_edit;
mod note;
mod piano;
mod sample;
mod slot;
mod summary;
mod ui;
use clap::{Args, Parser, Subcommand};
use nord_usb::ObjectClass;
use std::path::PathBuf;
use std::process::ExitCode;
use ui::{ColorChoice, Ui};
#[derive(Parser)]
#[command(name = "nord", about = "Inspect Clavia / Nord keyboard files", version)]
struct Cli {
#[arg(long, global = true, value_name = "WHEN", value_enum, default_value_t)]
color: ColorChoice,
#[arg(long, global = true, value_name = "PATH")]
record: Option<PathBuf>,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Inspect {
#[arg(required = true)]
files: Vec<PathBuf>,
#[arg(long)]
raw: bool,
},
Verify {
#[arg(required = true)]
files: Vec<PathBuf>,
},
Edit(file_edit::FileEditArgs),
Device {
#[command(subcommand)]
action: DeviceAction,
},
Program {
#[command(subcommand)]
action: ProgramAction,
},
Setlist {
#[command(subcommand)]
action: SetlistAction,
},
Live {
#[command(subcommand)]
action: LiveAction,
},
Settings {
#[command(subcommand)]
action: SettingsAction,
},
Sample {
#[command(subcommand)]
action: SampleAction,
},
Piano {
#[command(subcommand)]
action: PianoAction,
},
#[command(hide = true)]
Raw {
#[arg(long, global = true, value_name = "N", default_value_t = 4)]
class: u32,
#[command(subcommand)]
action: SlotAction,
},
}
#[derive(Subcommand)]
enum DeviceAction {
Controls {
#[arg(long, default_value_t = 0)]
from: u8,
#[arg(long, default_value_t = 15)]
to: u8,
#[arg(long, default_value_t = 64)]
len: usize,
#[arg(long)]
interface: bool,
#[arg(long, default_value_t = 0)]
value: u16,
#[arg(long, default_value_t = 0)]
index: u16,
},
Status {
#[arg(long, value_name = "SCRIPT")]
replay: Option<PathBuf>,
#[arg(long)]
json: bool,
},
Info,
Recover,
Geometry,
#[cfg(feature = "wedge")]
#[command(hide = true)]
Wedge {
#[arg(long, value_name = "N", default_value_t = 4)]
class: u32,
#[arg(long)]
yes: bool,
},
}
#[derive(Subcommand)]
enum ProgramAction {
#[command(flatten)]
Slot(SlotAction),
Edit(EditArgs),
}
#[derive(Subcommand)]
enum SetlistAction {
#[command(flatten)]
Slot(SlotAction),
Edit(EditArgs),
}
#[derive(Subcommand)]
enum SampleAction {
#[command(flatten)]
Slot(SlotAction),
Edit(sample::EditArgs),
Decode(sample::DecodeArgs),
Encode(sample::EncodeArgs),
Build(sample::BuildArgs),
Verify(sample::VerifyArgs),
Project {
#[command(subcommand)]
action: SampleProjectAction,
},
}
#[derive(Subcommand)]
enum PianoAction {
#[command(flatten)]
Slot(SlotAction),
Inspect(piano::InspectArgs),
Decode(piano::DecodeArgs),
Edit(piano::EditArgs),
Trim(piano::TrimArgs),
Split(piano::SplitArgs),
Verify(piano::VerifyArgs),
}
#[derive(Subcommand)]
enum SampleProjectAction {
New(sample::ProjectNewArgs),
}
#[derive(Subcommand)]
enum LiveAction {
#[command(flatten)]
Slot(LiveSlotAction),
Edit(EditArgs),
}
#[derive(Subcommand)]
enum SettingsAction {
#[command(flatten)]
Slot(SettingsSlotAction),
Edit(EditArgs),
}
#[derive(Subcommand)]
enum SettingsSlotAction {
Get {
#[arg(value_name = "FILE|BANK:SLOT")]
at: String,
#[arg(short, long, value_name = "FILE|DIR")]
out: Option<PathBuf>,
#[arg(long)]
body: bool,
#[arg(long, requires = "out")]
sweep: bool,
},
Info {
#[arg(value_name = "FILE|BANK:SLOT")]
at: String,
},
}
#[derive(Subcommand)]
enum LiveSlotAction {
Get {
#[arg(value_name = "FILE|BANK:SLOT")]
at: String,
#[arg(short, long, value_name = "FILE|DIR")]
out: Option<PathBuf>,
#[arg(long)]
body: bool,
#[arg(long, requires = "out")]
sweep: bool,
},
Info {
#[arg(value_name = "FILE|BANK:SLOT")]
at: String,
},
Deps {
#[arg(value_name = "FILE|BANK:SLOT")]
at: String,
},
}
#[derive(Subcommand)]
enum SlotAction {
Get {
#[arg(value_name = "FILE|BANK:SLOT")]
at: String,
#[arg(short, long, value_name = "FILE|DIR")]
out: Option<PathBuf>,
#[arg(long)]
body: bool,
#[arg(long, requires = "out")]
sweep: bool,
},
Put {
file: PathBuf,
#[arg(value_name = "BANK:SLOT")]
at: String,
#[arg(long)]
yes: bool,
},
Move {
#[arg(value_name = "FROM")]
from: String,
#[arg(value_name = "TO")]
to: String,
#[arg(long)]
yes: bool,
},
Rename {
#[arg(value_name = "BANK:SLOT")]
at: String,
name: String,
#[arg(long)]
yes: bool,
},
Duplicate {
#[arg(value_name = "FROM")]
from: String,
#[arg(value_name = "TO")]
to: String,
#[arg(long)]
yes: bool,
},
Delete {
#[arg(value_name = "BANK:SLOT", required = true)]
slots: Vec<String>,
#[arg(long)]
yes: bool,
},
Select {
#[arg(value_name = "BANK:SLOT")]
at: String,
},
Info {
#[arg(value_name = "FILE|BANK:SLOT")]
at: String,
},
Deps {
#[arg(value_name = "FILE|BANK:SLOT")]
at: String,
},
Focus,
List,
Probe {
#[arg(value_name = "OP", value_parser = parse_u32)]
op: u32,
#[arg(long = "arg", value_name = "N", value_parser = parse_u32)]
args: Vec<u32>,
#[arg(long, default_value_t = 5)]
wait: u64,
#[arg(long)]
yes: bool,
#[arg(long)]
bare: bool,
#[arg(long, default_value_t = 12)]
service: u32,
#[arg(long, default_value_t = 10)]
subsystem: u32,
},
}
fn parse_u32(s: &str) -> Result<u32, String> {
let s = s.trim();
match s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
Some(hex) => u32::from_str_radix(hex, 16),
None => s.parse(),
}
.map_err(|e| format!("{s}: {e}"))
}
#[derive(Args)]
pub struct EditArgs {
#[arg(
value_name = "FILE|BANK:SLOT",
required_unless_present_any = ["fields", "out"],
)]
pub target: Option<String>,
#[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,
}
fn main() -> ExitCode {
let cli = Cli::parse();
let ui = Ui::new(cli.color);
device::set_recording(cli.record);
let result = match cli.command {
Command::Inspect { files, raw } => inspect(&ui, &files, raw),
Command::Verify { files } => verify(&ui, &files),
Command::Edit(args) => file_edit::run(&ui, args),
Command::Device { action } => match action {
DeviceAction::Status { replay, json } => {
let source = match replay {
Some(path) => device::Source::Replay(path),
None => device::Source::Usb,
};
device::status(&ui, source, json)
}
DeviceAction::Info => device::info(&ui),
DeviceAction::Recover => device::recover(&ui),
DeviceAction::Geometry => device::geometry(&ui),
#[cfg(feature = "wedge")]
DeviceAction::Wedge { class, yes } => {
device::wedge(&ui, ObjectClass::from_raw(class), yes)
}
DeviceAction::Controls {
from,
to,
len,
interface,
value,
index,
} => device::controls(&ui, from, to, len, interface, value, index),
},
Command::Program { action } => match action {
ProgramAction::Slot(action) => slot_action(&ui, action, ObjectClass::Program),
ProgramAction::Edit(args) => edit::run(&ui, args, ObjectClass::Program),
},
Command::Sample { action } => match action {
SampleAction::Slot(action) => slot_action(&ui, action, ObjectClass::Sample),
SampleAction::Edit(args) => sample::run(&ui, args),
SampleAction::Decode(args) => sample::decode(&ui, args),
SampleAction::Encode(args) => sample::encode(&ui, args),
SampleAction::Build(args) => sample::build(&ui, args),
SampleAction::Verify(args) => sample::verify(&ui, args),
SampleAction::Project { action } => match action {
SampleProjectAction::New(args) => sample::project_new(&ui, args),
},
},
Command::Piano { action } => match action {
PianoAction::Slot(action) => slot_action(&ui, action, ObjectClass::Piano),
PianoAction::Inspect(args) => piano::inspect(&ui, args),
PianoAction::Decode(args) => piano::decode(&ui, args),
PianoAction::Edit(args) => piano::edit(&ui, args),
PianoAction::Trim(args) => piano::trim(&ui, args),
PianoAction::Split(args) => piano::split(&ui, args),
PianoAction::Verify(args) => piano::verify(&ui, args),
},
Command::Setlist { action } => match action {
SetlistAction::Slot(action) => slot_action(&ui, action, ObjectClass::SetList),
SetlistAction::Edit(args) => edit::run(&ui, args, ObjectClass::SetList),
},
Command::Live { action } => match action {
LiveAction::Slot(action) => slot_action(&ui, action.into(), ObjectClass::Live),
LiveAction::Edit(args) => edit::run(&ui, args, ObjectClass::Live),
},
Command::Settings { action } => match action {
SettingsAction::Slot(action) => slot_action(&ui, action.into(), ObjectClass::Settings),
SettingsAction::Edit(args) => edit::run(&ui, args, ObjectClass::Settings),
},
Command::Raw { class, action } => slot_action(&ui, action, ObjectClass::from_raw(class)),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
ui.note(format!("{}: {e}", ui.danger("error")));
ExitCode::FAILURE
}
}
}
impl From<SettingsSlotAction> for SlotAction {
fn from(action: SettingsSlotAction) -> SlotAction {
match action {
SettingsSlotAction::Get {
at,
out,
body,
sweep,
} => SlotAction::Get {
at,
out,
body,
sweep,
},
SettingsSlotAction::Info { at } => SlotAction::Info { at },
}
}
}
impl From<LiveSlotAction> for SlotAction {
fn from(action: LiveSlotAction) -> SlotAction {
match action {
LiveSlotAction::Get {
at,
out,
body,
sweep,
} => SlotAction::Get {
at,
out,
body,
sweep,
},
LiveSlotAction::Info { at } => SlotAction::Info { at },
LiveSlotAction::Deps { at } => SlotAction::Deps { at },
}
}
}
fn slot_action(ui: &Ui, action: SlotAction, class: ObjectClass) -> Result<(), String> {
match action {
SlotAction::Get {
at,
out,
body,
sweep,
} => match slot::target(&at)? {
slot::Target::File(path) if sweep => Err(format!(
"--sweep re-reads the instrument as the panel changes; {} has only one state",
path.display()
)),
slot::Target::File(path) => file::get(ui, &path, out, class, body),
slot::Target::Slot(at) => match (sweep, out) {
(true, Some(dir)) => device::sweep(ui, at, dir, class, body),
(true, None) => Err("--sweep fills a directory; give -o a path".into()),
(false, out) => device::get(ui, at, out, class, body),
},
},
SlotAction::Put { file, at, yes } => device::put(ui, file, slot::parse(&at)?, class, yes),
SlotAction::Move { from, to, yes } => {
device::move_object(ui, slot::parse(&from)?, slot::parse(&to)?, class, yes)
}
SlotAction::Rename { at, name, yes } => {
device::rename(ui, slot::parse(&at)?, name, class, yes)
}
SlotAction::Duplicate { from, to, yes } => {
device::duplicate(ui, slot::parse(&from)?, slot::parse(&to)?, class, yes)
}
SlotAction::Delete { slots, yes } => {
device::delete(ui, &slot::parse_all(&slots)?, class, yes)
}
SlotAction::Select { at } => device::select(ui, slot::parse(&at)?, class),
SlotAction::Info { at } => match slot::target(&at)? {
slot::Target::File(path) => file::info(ui, &path, class),
slot::Target::Slot(at) => device::slot_info(ui, at, class),
},
SlotAction::Deps { at } => match slot::target(&at)? {
slot::Target::File(path) => file::deps(ui, &path, class),
slot::Target::Slot(at) => device::deps(ui, at, class),
},
SlotAction::Focus => device::focus(ui, class),
SlotAction::List => device::list(ui, class),
SlotAction::Probe {
op,
args,
wait,
yes,
bare,
service,
subsystem,
} => device::probe(ui, class, op, &args, wait, yes, bare, service, subsystem),
}
}
fn inspect(ui: &Ui, files: &[PathBuf], raw: bool) -> Result<(), String> {
let mut failed = 0usize;
for (i, path) in files.iter().enumerate() {
if i > 0 {
ui.out("");
}
ui.out(path.display());
match nord_format::from_path(path) {
Ok(entity) if raw => ui.out(format!("{entity:#?}")),
Ok(entity) => summary::print(ui, &entity),
Err(e) => {
ui.note(format!(" error: {e}"));
failed += 1;
}
}
}
match failed {
0 => Ok(()),
n => Err(format!("{n} of {} file(s) did not parse", files.len())),
}
}
fn verify(ui: &Ui, files: &[PathBuf]) -> Result<(), String> {
let mut failed = 0usize;
for path in files {
let original = match std::fs::read(path) {
Ok(b) => b,
Err(e) => {
ui.out(format!("error {} ({e})", path.display()));
failed += 1;
continue;
}
};
let reencoded =
nord_format::from_path(path).and_then(|entity| nord_format::to_bytes(&entity));
match reencoded {
Ok(bytes) if bytes == original => {
ui.out(format!(
"ok {} ({} bytes)",
path.display(),
original.len()
));
}
Ok(bytes) => {
failed += 1;
let at = bytes
.iter()
.zip(&original)
.position(|(a, b)| a != b)
.map(|i| format!("{i:#x}"))
.unwrap_or_else(|| "the end (length differs)".to_string());
ui.out(format!(
"DIFFER {} (in {} bytes, out {}; first difference at {at})",
path.display(),
original.len(),
bytes.len(),
));
}
Err(e) => {
failed += 1;
ui.out(format!("error {} ({e})", path.display()));
}
}
}
match failed {
0 => Ok(()),
n => Err(format!("{n} of {} file(s) did not round-trip", files.len())),
}
}