mod advanced_rs;
mod basic_js;
mod basic_rs;
use crate::prelude::*;
use async_std::fs;
use async_std::path::{Path, PathBuf};
use console::style;
use convert_case::{Case, Casing};
use question::{Answer, Question};
use std::collections::HashSet;
use std::sync::atomic::{AtomicBool, Ordering};
use uuid::Uuid;
const DEFAULT_APPLICATION_ICON: &[u8] =
include_bytes!("../../resources/images/default-application-icon.png");
const MACOS_DISK_IMAGE_BACKGROUND: &[u8] =
include_bytes!("../../resources/images/macos-disk-image-background.png");
const INNOSETUP_WIZARD_SMALL_IMAGE: &[u8] =
include_bytes!("../../resources/images/innosetup-wizard-small.png");
const INNOSETUP_WIZARD_LARGE_IMAGE: &[u8] =
include_bytes!("../../resources/images/innosetup-wizard-large.png");
const TRAY_ICON: &[u8] = include_bytes!("../../resources/images/tray-icon@2x.png");
#[derive(Debug)]
pub enum TemplateKind {
BasicRs,
BasicJs,
AdvancedRs,
}
#[derive(Debug)]
pub struct Options {
pub manifest: bool,
pub template_kind: TemplateKind,
pub force: bool,
}
#[derive(Debug)]
pub struct Project {
pub folder: PathBuf,
pub name: String,
pub title: String,
pub group: String,
pub version: String,
pub description: String,
pub uuid: Uuid,
pub options: Options,
pub manifest: AtomicBool,
}
impl Project {
pub fn try_new(name: String, folder: PathBuf, options: Options) -> Result<Project> {
let name = name.to_case(Case::Kebab);
let title = name.from_case(Case::Lower).to_case(Case::Title);
let group = title.clone();
let version = "0.1.0".to_string();
let description = "...".to_string();
let uuid = Uuid::new_v4();
let manifest = AtomicBool::new(options.manifest);
let project = Project {
folder,
name,
title,
group,
version,
description,
uuid,
options,
manifest,
};
Ok(project)
}
pub async fn generate(&mut self) -> Result<()> {
if !self.options.force && Path::new("nw.toml").exists().await {
return Err("existing nw.toml found ...aborting (use --force to re-create)".into());
}
if Path::new("package.json").exists().await {
let package_json = PackageJson::try_load("package.json")?;
self.name = package_json.name.to_lowercase().replace(' ', "-");
self.title = package_json.name;
if let Some(version) = package_json.version {
self.version = version;
}
if let Some(description) = package_json.description {
self.description = description;
}
log_info!("Project", "detected existing 'package.json' manifest");
log_info!(
"Project",
"name: '{}' title: '{}' version: '{}'",
self.name,
self.title,
self.version
);
self.manifest.store(true, Ordering::SeqCst);
} else {
let name = Question::new(&format!(
"Project name [default:'{}']:",
style(&self.name).yellow()
))
.ask();
if let Some(Answer::RESPONSE(name)) = name {
if !name.is_empty() {
if name.contains(' ') {
println!(
"{}",
style("\nError: project name can not contain spaces\n").red()
);
std::process::exit(1);
}
let name = name.to_case(Case::Kebab);
if name != self.name {
self.title = name
.replace('-', " ")
.from_case(Case::Kebab)
.to_case(Case::Title);
}
self.name = name;
}
}
let title = Question::new(&format!(
"Project title [default:'{}']:",
style(&self.title).yellow()
))
.ask();
if let Some(Answer::RESPONSE(title)) = title {
if !title.is_empty() {
self.title = title;
}
}
}
println!();
log_info!("Init", "creating '{}'", self.name);
println!();
let manifest = self.manifest.load(Ordering::SeqCst);
match self.options.template_kind {
TemplateKind::BasicJs => {
basic_js::generate(self, manifest).await?;
}
TemplateKind::BasicRs => {
basic_rs::generate(self, manifest).await?;
}
TemplateKind::AdvancedRs => {
advanced_rs::generate(self, manifest).await?;
}
}
Ok(())
}
async fn write_files(&self, files: &[(&str, String)], images: &[(&str, &[u8])]) -> Result<()> {
for (filename, content) in files.iter() {
if !self.options.force && Path::new(filename).exists().await {
log_warn!(
"Init",
"WARNING: file already exists! `{}` (use --force to overwrite) skipping...",
filename
);
}
fs::write(filename, &content).await?;
}
for (filename, data) in images.iter() {
fs::write(filename, data).await?;
}
Ok(())
}
async fn create_folders(
&self,
files: &[(&str, String)],
images: &[(&str, &[u8])],
) -> Result<()> {
let folders: HashSet<&Path> = files
.iter()
.map(|(f, _)| f)
.chain(images.iter().map(|(f, _)| f))
.filter_map(|path| Path::new(path).parent())
.collect();
for folder in folders {
fs::create_dir_all(folder).await?;
}
Ok(())
}
fn images(&self) -> Vec<(&'static str, &'static [u8])> {
vec![
("resources/setup/application.png", DEFAULT_APPLICATION_ICON),
("resources/setup/document.png", DEFAULT_APPLICATION_ICON),
(
"resources/setup/macos-application.png",
DEFAULT_APPLICATION_ICON,
),
(
"resources/setup/macos-dmg-background.png",
MACOS_DISK_IMAGE_BACKGROUND,
),
(
"resources/setup/innosetup-wizard-small.png",
INNOSETUP_WIZARD_SMALL_IMAGE,
),
(
"resources/setup/innosetup-wizard-large.png",
INNOSETUP_WIZARD_LARGE_IMAGE,
),
("resources/icons/tray-icon@2x.png", TRAY_ICON),
]
}
fn tpl(&self) -> Result<Tpl> {
let tpl: Tpl = [
("NAME", self.name.clone()),
(
"SNAKE",
self.name.from_case(Case::Kebab).to_case(Case::Snake),
),
("TITLE", self.title.clone()),
("UUID", self.uuid.to_string()),
("VERSION", self.version.to_string()),
("DESCRIPTION", self.description.to_string()),
]
.as_slice()
.try_into()?;
Ok(tpl)
}
}