sneakerweb 1.0.1

A parallel web transported by physical media
use anyhow::{Context, Result, bail};
use colored::{Color, Colorize};
use smol::{Unblock, fs::*, io::AsyncWriteExt};
use std::{
    env::home_dir,
    io::{Error, ErrorKind, prelude::*, stderr, stdin, stdout},
    path::PathBuf,
};
use willow25::prelude::*;

pub const SNEAKERWEB_NAMESPACE_ID_BYTES: [u8; NAMESPACE_ID_WIDTH] = [
    159, 196, 204, 134, 202, 217, 77, 17, 2, 90, 252, 247, 94, 13, 171, 36, 188, 60, 108, 145, 240,
    205, 146, 251, 224, 202, 87, 77, 70, 156, 104, 30,
];

pub const _SNEAKERWEB_SERVICE_NAME: &str = "org.worm-blossom.sneakerweb";

/// Requests input from the user, displaying a message and optionally styling the prompt for reply.
pub fn user_input<F>(message: &str, prompt_style: Option<F>) -> Result<Option<String>, Error>
where
    F: Fn(&str) -> String,
{
    println!("{}", message);
    match prompt_style {
        Some(style) => print!("{} ", style(">")),
        None => print!("{} ", ">".bold()),
    }
    stdout().flush()?;
    stdin().lines().next().transpose()
}

/// Retrieves the path within the user's home directory where sneakerweb configuration and data are located.
pub async fn sneakerweb_dir() -> Result<PathBuf, Error> {
    let mut store_path = home_dir().ok_or(Error::new(
        ErrorKind::NotFound,
        "could not determine user home directory",
    ))?;
    store_path.push(".sneakerweb");

    create_dir_all(&store_path).await?;
    Ok(store_path)
}

/// Styles text using the conventions for sneakerweb domains.
pub fn domain_style(message: &str) -> String {
    format!("{}", message.bold().color(Color::Green))
}

/// Styles text using the conventions for sneakerweb secrets.
pub fn secret_style(message: &str) -> String {
    format!("{}", message.bold().color(Color::Magenta))
}

/// Styles text celebratorily.
pub fn yay_style(message: &str) -> String {
    format!("{}", message.bold().color(Color::Green))
}

/// Styles text commiseratively.
pub fn oh_no_style(message: &str) -> String {
    format!("{}", message.bold().color(Color::Magenta))
}

/// Styles text contemplatively.
pub fn hmm_style(message: &str) -> String {
    format!("{}", message.bold().color(Color::Yellow))
}

/// Styles text emphatically.
pub fn emph_style(message: &str) -> String {
    format!("{}", message.bold())
}

/// Celebrate something with the user.
pub async fn yay(message: &str) {
    let mut stdout = Unblock::new(stdout());
    stdout
        .write_all(format!("{} {}\n", yay_style("Yay:"), message).as_bytes())
        .await
        .unwrap();
    stdout.flush().await.unwrap();
}

/// Commiserate something with the user.
pub async fn oh_no(message: &str) {
    let mut stderr = Unblock::new(stderr());
    stderr
        .write_all(format!("{} {}\n", oh_no_style("Oh No:"), message).as_bytes())
        .await
        .unwrap();
    stderr.flush().await.unwrap();
}

/// Contemplate something with the user.
pub async fn hmm(message: &str) {
    let mut stdout = Unblock::new(stdout());
    stdout
        .write_all(format!("{} {}\n", hmm_style("Hmm:"), message).as_bytes())
        .await
        .unwrap();
    stdout.flush().await.unwrap();
}

/// Decodes a [`SubspaceId`] "domain" from a provided base-16 string, or requests such a string from the user if none is provided.
pub fn get_domain(encoded: Option<&str>, action: &str) -> Result<(SubspaceId, String)> {
    let domain = match encoded {
        Some(domain) => domain.trim().to_owned(),
        None => user_input(
            &format!(
                "Which {} do you want to {}?",
                domain_style("domain"),
                action
            ),
            Some(domain_style),
        )
        .context("failed to retrieve user input")?
        .unwrap_or_default(),
    };

    match base16::decode(&domain) {
        Ok(decoded) if decoded.len() == 32 => Ok((
            SubspaceId::from_bytes(decoded.as_array().expect("this is a vec of 32 bytes")),
            domain,
        )),
        _ => bail!("invalid domain"),
    }
}

/// Decodes a [`SubspaceSecret`] for some `domain` from a provided base-16 string, or requests such a string from the user if none is provided.
pub fn get_secret(encoded: Option<&str>, domain: &str) -> Result<SubspaceSecret> {
    let secret_string = match encoded {
        Some(secret) => secret.to_owned(),
        None => {
            // TODO: Should we bring in, e.g., the `rpassword` crate to hide the secret key?
            user_input(
                &format!(
                    "Please provide the {} for {}:",
                    secret_style("secret key"),
                    domain_style(domain)
                ),
                Some(secret_style),
            )
            .context("failed to retrieve user input")?
            .unwrap_or_default()
        }
    };

    match base16::decode(&secret_string) {
        Ok(decoded) if decoded.len() == 32 => Ok(SubspaceSecret::from_bytes(
            decoded.as_array().expect("this is a vec of 32 bytes"),
        )),
        _ => bail!("invalid secret"),
    }
}