rlyx 0.3.1

rlyx is a fast release manager that automatically bumps versions, creates changelogs, tags commits, and publishes GitHub releases across JS, Rust, and Python projects with first class monorepos support.
Documentation
use console::{style, Term};
use dialoguer::{Confirm, Input};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};

pub struct Theme;

impl Theme {
    pub fn gray(s: &str) -> String {
        style(s).dim().to_string()
    }
    pub fn cyan(s: &str) -> String {
        style(s).cyan().bold().to_string()
    }
    pub fn green(s: &str) -> String {
        style(s).green().bold().to_string()
    }
    pub fn red(s: &str) -> String {
        style(s).red().bold().to_string()
    }
    pub fn white(s: &str) -> String {
        style(s).white().bold().to_string()
    }
}

pub fn clear_and_header() {
    let term = Term::stdout();
    let _ = term.clear_screen();
    let (w, _) = term.size();
    let title = "rlyx release";
    let pad = if (w as usize) > title.len() {
        ((w as usize) - title.len()) / 2
    } else {
        0
    };
    let mut line = String::new();
    line.extend(std::iter::repeat_n(' ', pad));
    line.push_str(&Theme::cyan(title));
    eprintln!(
        "\n{}\n{}",
        line,
        Theme::gray(&"-".repeat(usize::min(60, w as usize)))
    );
}

pub fn section(title: &str) {
    eprintln!("\n{} {}", Theme::cyan("==>"), Theme::white(title));
    eprintln!("{}", Theme::gray("------------------------------------------------------------"));
}

pub fn info(msg: &str) {
    eprintln!("{} {}", Theme::cyan("[info]"), msg);
}
pub fn warn(msg: &str) {
    eprintln!("{} {}", style("[warn]").yellow().bold(), msg);
}
pub fn ok(msg: &str) {
    eprintln!("{} {}", Theme::green("[ok]"), msg);
}
pub fn err(msg: &str) {
    eprintln!("{} {}", Theme::red("[err]"), msg);
}

pub fn divider() {
    eprintln!("{}", Theme::gray("------------------------------------------------------------"));
}

pub fn summary_block(lines: &[(&str, &str)]) {
    eprintln!();
    eprintln!(
        "{}",
        Theme::cyan("+---------------- summary ----------------+")
    );
    for (k, v) in lines {
        eprintln!("  {:<14} {}", console::style(k).bold(), v);
    }
    eprintln!(
        "{}",
        Theme::cyan("+-----------------------------------------+")
    );
    eprintln!();
}

pub fn confirm(prompt: &str) -> bool {
    let term = Term::stderr();
    matches!(
        Confirm::new()
            .with_prompt(style(prompt).bold().white().to_string())
            .default(true)
            .interact_on(&term),
        Ok(true)
    )
}

pub fn mp() -> MultiProgress {
    MultiProgress::new()
}

pub fn spinner_style() -> ProgressStyle {
    ProgressStyle::with_template(
        "{prefix:.blue.bold} {spinner} {msg}",
    )
    .unwrap()
    .tick_chars("|/-\\")
}

pub fn done_style(dur: &str) -> String {
    format!("{} {}", Theme::green("[ok]"), Theme::gray(dur))
}

pub fn fail_style(dur: &str) -> String {
    format!("{} {}", Theme::red("[err]"), Theme::gray(dur))
}

pub fn make_spinner(
    mp: &MultiProgress,
    prefix: &str,
    msg: &str,
) -> ProgressBar {
    let pb = mp.add(ProgressBar::new_spinner());
    pb.set_style(spinner_style());
    pb.set_prefix(prefix.to_string());
    pb.set_message(msg.to_string());
    pb.enable_steady_tick(std::time::Duration::from_millis(80));
    pb
}

pub fn prompt_bump(label: &str) -> Option<crate::semver::BumpKind> {
    let term = Term::stderr();
    let s: String = Input::new()
        .with_prompt(format!(
            "bump {} [p]atch / [m]inor / [M]ajor / [s]kip",
            label
        ))
        .with_initial_text("p")
        .interact_text_on(&term)
        .unwrap_or_else(|_| "s".into());
    match s.trim() {
        "p" | "patch" => Some(crate::semver::BumpKind::Patch),
        "m" | "minor" => Some(crate::semver::BumpKind::Minor),
        "M" | "major" => Some(crate::semver::BumpKind::Major),
        "s" | "skip" | "" => None,
        _ => None,
    }
}

pub fn prompt_post_bump(initial: &str) -> Option<String> {
    let term = Term::stderr();
    let s: String = Input::new()
        .with_prompt("post-bump command [enter=accept, type to edit, 's' to skip]")
        .with_initial_text(initial)
        .interact_text_on(&term)
        .unwrap_or_else(|_| "s".into());
    let t = s.trim();
    if t.is_empty()
        || t.eq_ignore_ascii_case("s")
        || t.eq_ignore_ascii_case("skip")
    {
        None
    } else {
        Some(t.to_string())
    }
}