#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
#![doc = include_str!("../README.md")]
use std::{
error::Error,
ffi::OsStr,
fs,
os::unix::fs::DirBuilderExt,
path::{Path, PathBuf},
};
#[cfg(feature = "mangen")]
use {clap_mangen::Man, std::io};
#[cfg(feature = "complete")]
use {
clap_complete::{generate_to, shells},
clap_complete_nushell::Nushell,
};
#[cfg(feature = "icons")]
use {
tiny_skia::Transform,
usvg::{FitTo, Options, Tree},
};
pub struct Bootstrap {
name: String,
#[cfg(feature = "complete")]
cli: clap::Command,
outdir: PathBuf,
}
impl Bootstrap {
#[must_use]
pub fn new<P: AsRef<OsStr>>(
name: &str,
#[cfg(feature = "complete")] cli: clap::Command,
outdir: P,
) -> Self {
Self {
name: name.to_string(),
#[cfg(feature = "complete")]
cli,
outdir: Path::new(&outdir).to_path_buf(),
}
}
#[cfg(feature = "complete")]
fn gencomp(&self, gen: &str, outdir: PathBuf) -> Result<(), Box<dyn Error>> {
let mut cmd = self.cli.clone();
let path = match gen {
"bash" => generate_to(shells::Bash, &mut cmd, &self.name, &outdir)?,
"fish" => generate_to(shells::Fish, &mut cmd, &self.name, &outdir)?,
"nu" => generate_to(Nushell, &mut cmd, &self.name, &outdir)?,
"pwsh" => generate_to(shells::PowerShell, &mut cmd, &self.name, &outdir)?,
"zsh" => generate_to(shells::Zsh, &mut cmd, &self.name, &outdir)?,
"elvish" => generate_to(shells::Elvish, &mut cmd, &self.name, &outdir)?,
_ => unimplemented!(),
};
println!(" {}", path.display());
Ok(())
}
#[cfg(feature = "complete")]
pub fn completions(&self) -> Result<(), Box<dyn Error>> {
println!("Generating completions:");
["bash", "fish", "nu", "pwsh", "zsh", "elvish"]
.iter()
.try_for_each(|gen| {
let mut outdir = self.outdir.clone();
let base = match *gen {
"bash" => ["share", "bash-completion", "completions"],
"zsh" => ["share", "zsh", "site-functions"],
"nu" => ["share", "nu", "completions"],
"pwsh" => ["share", "pwsh", "completions"],
"fish" => ["share", "fish", "completions"],
"elvish" => ["share", "elvish", "completions"],
_ => unimplemented!(),
};
base.iter().for_each(|d| outdir.push(d));
let mut dirbuilder = fs::DirBuilder::new();
dirbuilder.recursive(true).mode(0o0755);
if !outdir.exists() {
dirbuilder.create(&outdir)?;
}
self.gencomp(gen, outdir)
})?;
Ok(())
}
fn copy_bin(&self, arch: Option<String>) -> Result<(), Box<dyn Error>> {
println!("Copying binary:");
let mut bindir = self.outdir.clone();
bindir.push("bin");
let mut dirbuilder = fs::DirBuilder::new();
dirbuilder.recursive(true).mode(0o0755);
if !bindir.exists() {
dirbuilder.create(&bindir)?;
}
let mut outfile = bindir;
outfile.push("hpk");
let infile: PathBuf = if let Some(arch) = arch {
["target", &arch, "release", &self.name].iter().collect()
} else {
["target", "release", &self.name].iter().collect()
};
if !infile.exists() {
eprintln!("Error: you must run \"cargo build --release\" first");
}
fs::copy(&infile, &outfile)?;
println!(" {} -> {}", infile.display(), outfile.display());
Ok(())
}
#[cfg(feature = "mangen")]
pub fn manpage(&self, section: u8) -> Result<(), io::Error> {
let fname = format!("{}.{section}", &self.name);
println!("Generating manpage {fname}:");
let command = self.cli.clone();
let mut outdir = self.outdir.clone();
["share", "man", &format!("man{section}")]
.iter()
.for_each(|d| outdir.push(d));
let mut dirbuilder = fs::DirBuilder::new();
dirbuilder.recursive(true).mode(0o0755);
if !outdir.exists() {
dirbuilder.create(&outdir)?;
}
let mut outfile = outdir;
outfile.push(fname);
let man = Man::new(command);
let mut buffer: Vec<u8> = vec![];
man.render(&mut buffer)?;
fs::write(&outfile, buffer)?;
println!(" {}", outfile.display());
Ok(())
}
#[cfg(feature = "icons")]
fn png(&self, tree: &Tree, size: u32, source: &Path) -> Result<(), Box<dyn Error>> {
let fit = FitTo::Size(size, size);
let transform = Transform::from_scale(1.0, 1.0);
let Some(mut pixmap) = tiny_skia::Pixmap::new(size, size) else {
return Err(String::from("Error creating png").into());
};
resvg::render(tree, fit, transform, pixmap.as_mut());
let mut outdir = self.outdir.clone();
let sizedir = format!("{size}x{size}");
["share", "icons", "hicolor", &sizedir, "apps"]
.iter()
.for_each(|d| outdir.push(d));
let mut dirbuilder = fs::DirBuilder::new();
dirbuilder.recursive(true).mode(0o0755);
if !outdir.exists() {
dirbuilder.create(&outdir)?;
}
let mut outfile = outdir;
let fname = source
.file_stem()
.and_then(|x| x.to_str().map(|x| format!("{x}.png")))
.unwrap();
outfile.push(&fname);
println!(" {} -> {}", source.display(), outfile.display());
pixmap.save_png(outfile)?;
Ok(())
}
#[cfg(feature = "icons")]
pub fn icons<P: AsRef<OsStr>>(&self, source: Option<P>) -> Result<(), Box<dyn Error>> {
println!("Creating png icons from svg:");
let infile = match source {
Some(s) => Path::new(&s).to_path_buf(),
None => ["data", &self.name].iter().collect::<PathBuf>(),
};
let data = fs::read(&infile)?;
let tree = Tree::from_data(&data, &Options::default().to_ref())?;
for size in [256, 128, 64, 48, 32, 24, 16] {
self.png(&tree, size, &infile)?;
}
Ok(())
}
pub fn install(
&self,
arch: Option<String>,
#[cfg(feature = "mangen")] section: u8,
) -> Result<(), Box<dyn Error>> {
self.copy_bin(arch)?;
#[cfg(feature = "complete")]
self.completions()?;
#[cfg(feature = "mangen")]
self.manpage(section)?;
#[cfg(feature = "icons")]
{
let infile: Option<&str> = None;
self.icons(infile)?;
}
Ok(())
}
}