univert 0.4.0

Universal file converter (library and CLI)
Documentation
use std::process::Command;

use crate::converters::scad::which_tool;
use crate::{Error, Job, Result, extension_of};

pub fn abc_to_ly(job: &Job) -> Result<()> {
  require("abc2ly")?;
  let status = Command::new("abc2ly")
    .arg("-o")
    .arg(job.output)
    .arg(job.input)
    .status()
    .map_err(|e| Error::ToolFailed {
      tool: "abc2ly",
      message: e.to_string(),
    })?;
  if !status.success() {
    return Err(Error::ToolFailed {
      tool: "abc2ly",
      message: format!("abc2ly exited with status {status}"),
    });
  }
  Ok(())
}

pub fn ly_to(job: &Job) -> Result<()> {
  require("lilypond")?;
  let to = extension_of(job.output).unwrap_or_default();

  // LilyPond picks up the output stem and appends an extension; point it at
  // a temp directory and then move the produced file into place.
  let tmp = tempfile::tempdir()?;
  let stem = job
    .output
    .file_stem()
    .ok_or_else(|| Error::other("output has no stem"))?;
  let tmp_base = tmp.path().join(stem);

  let mut cmd = Command::new("lilypond");
  match to.as_str() {
    "pdf" => {
      cmd.args(["--pdf"]);
    }
    "png" => {
      cmd.args(["--png"]);
    }
    "ps" => {
      cmd.args(["--ps"]);
    }
    "svg" => {
      cmd.args(["-dbackend=svg"]);
    }
    "midi" => {
      // lilypond emits MIDI when the source contains \midi; for a plain
      // conversion call, still run lilypond and look for .midi alongside.
    }
    other => {
      return Err(Error::other(format!("lilypond cannot emit .{other}")));
    }
  }

  cmd.arg("-o").arg(&tmp_base).arg(job.input);

  let status = cmd.status().map_err(|e| Error::ToolFailed {
    tool: "lilypond",
    message: e.to_string(),
  })?;
  if !status.success() {
    return Err(Error::ToolFailed {
      tool: "lilypond",
      message: format!("lilypond exited with status {status}"),
    });
  }

  let produced = tmp_base.with_extension(&to);
  if !produced.exists() {
    return Err(Error::ToolFailed {
      tool: "lilypond",
      message: format!(
        "expected {} but lilypond produced nothing",
        produced.display()
      ),
    });
  }
  std::fs::rename(&produced, job.output)
    .or_else(|_| std::fs::copy(&produced, job.output).map(|_| ()))?;
  Ok(())
}

fn require(name: &'static str) -> Result<()> {
  if which_tool(name) {
    Ok(())
  } else {
    Err(Error::MissingTool {
      from: String::new(),
      to: String::new(),
      tool: name,
    })
  }
}