use std::sync::atomic::{AtomicBool, Ordering};
use colored::*;
use dialoguer::{Confirm, MultiSelect};
use indicatif::ProgressBar;
use crate::locale::Language;
use crate::models::{InstallMethod, PackageInfo, PrebuiltAvailability, UpdateResult};
static JSON_MODE: AtomicBool = AtomicBool::new(false);
pub fn set_json_mode(enabled: bool) {
JSON_MODE.store(enabled, Ordering::SeqCst);
}
pub fn is_json_mode() -> bool {
JSON_MODE.load(Ordering::SeqCst)
}
static DEBUG_MODE: AtomicBool = AtomicBool::new(false);
pub fn set_debug_mode(enabled: bool) {
DEBUG_MODE.store(enabled, Ordering::SeqCst);
}
pub fn is_debug_mode() -> bool {
DEBUG_MODE.load(Ordering::SeqCst)
}
const STATUS_WIDTH: usize = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusStyle {
Ok,
Warn,
Err,
Dim,
}
pub fn format_status_line(verb: &str, msg: &str, style: StatusStyle) -> String {
let padded = format!("{verb:>STATUS_WIDTH$}");
let colored_verb = match style {
StatusStyle::Ok => padded.green().bold().to_string(),
StatusStyle::Warn => padded.yellow().bold().to_string(),
StatusStyle::Err => padded.red().bold().to_string(),
StatusStyle::Dim => padded.dimmed().to_string(),
};
format!("{colored_verb} {msg}")
}
pub fn status(verb: &str, msg: &str) {
if is_json_mode() {
return;
}
anstream::eprintln!("{}", format_status_line(verb, msg, StatusStyle::Ok));
}
pub fn status_warn(verb: &str, msg: &str) {
if is_json_mode() {
return;
}
anstream::eprintln!("{}", format_status_line(verb, msg, StatusStyle::Warn));
}
pub fn status_err(verb: &str, msg: &str) {
if is_json_mode() {
return;
}
anstream::eprintln!("{}", format_status_line(verb, msg, StatusStyle::Err));
}
pub fn status_dim(verb: &str, msg: &str) {
if is_json_mode() {
return;
}
anstream::eprintln!("{}", format_status_line(verb, msg, StatusStyle::Dim));
}
pub fn status_debug(scope: &str, msg: &str) {
if !is_debug_mode() {
return;
}
anstream::eprintln!(
"{}",
format_status_line("debug", &format!("{scope}: {msg}"), StatusStyle::Dim)
);
}
pub fn pb_status(pb: &ProgressBar, verb: &str, msg: &str) {
if is_json_mode() {
return;
}
pb.println(format_status_line(verb, msg, StatusStyle::Ok));
}
pub fn pb_status_warn(pb: &ProgressBar, verb: &str, msg: &str) {
if is_json_mode() {
return;
}
pb.println(format_status_line(verb, msg, StatusStyle::Warn));
}
pub fn pb_status_err(pb: &ProgressBar, verb: &str, msg: &str) {
if is_json_mode() {
return;
}
pb.println(format_status_line(verb, msg, StatusStyle::Err));
}
pub fn pb_status_dim(pb: &ProgressBar, verb: &str, msg: &str) {
if is_json_mode() {
return;
}
pb.println(format_status_line(verb, msg, StatusStyle::Dim));
}
pub fn package_transition(package: &PackageInfo, language: Language) -> String {
let current = package
.current_version
.as_deref()
.unwrap_or(language.get_text("unknown"));
let latest = package
.latest_version
.as_deref()
.unwrap_or(language.get_text("unknown"));
let marker = package.source.marker();
let suffix = if marker.is_empty() {
String::new()
} else {
format!(" {}", marker.dimmed())
};
let prebuilt_suffix = match package.prebuilt {
Some(kind) => format!(" {}", prebuilt_marker(kind)),
None => String::new(),
};
format!(
"{} {} -> {}{}{}",
package.name.cyan(),
current.red(),
latest.green(),
suffix,
prebuilt_suffix
)
}
fn prebuilt_marker(kind: PrebuiltAvailability) -> String {
match kind {
PrebuiltAvailability::Prebuilt => kind.marker().green().to_string(),
PrebuiltAvailability::Source => kind.marker().yellow().to_string(),
PrebuiltAvailability::Unknown => kind.marker().dimmed().to_string(),
}
}
pub fn format_version_info(
old: &Option<String>,
new: &Option<String>,
language: Language,
) -> String {
match (old, new) {
(Some(old), Some(new)) if old != new => {
format!("{} -> {}", old.red(), new.green())
}
(Some(old), Some(_)) => {
format!(
"{} ({})",
old.yellow(),
language.get_text("version_unchanged").dimmed()
)
}
(Some(old), None) => {
format!("{} -> {}", old.red(), language.get_text("unknown_version").dimmed())
}
(None, Some(new)) => {
format!("{} -> {}", language.get_text("unknown_version").dimmed(), new.green())
}
_ => language.get_text("version_info_unknown").dimmed().to_string(),
}
}
fn package_with_source(package: &PackageInfo) -> String {
let marker = package.source.marker();
if marker.is_empty() {
package.name.cyan().to_string()
} else {
format!("{} {}", package.name.cyan(), marker.dimmed())
}
}
pub fn print_results(packages: &[PackageInfo], updates_only: bool, language: Language) {
if is_json_mode() {
return;
}
let mut has_updates = false;
let mut fresh_count = 0;
for package in packages {
if updates_only && !package.has_update() {
continue;
}
if package.has_update() {
has_updates = true;
status("Updating", &package_transition(package, language));
} else if !updates_only {
let version = package.current_version.as_deref().unwrap_or("?");
status_dim(
"Fresh",
&format!("{} {}", package_with_source(package), version.dimmed()),
);
fresh_count += 1;
} else {
fresh_count += 1;
}
}
if updates_only && !has_updates {
status("Finished", language.get_text("all_up_to_date"));
} else if !has_updates && fresh_count > 0 {
status("Finished", language.get_text("all_up_to_date"));
}
}
pub fn print_update_summary(update_results: &[UpdateResult], language: Language) {
if update_results.is_empty() {
return;
}
let mut success_updates = Vec::new();
let mut failed_updates = Vec::new();
for result in update_results {
if result.success {
success_updates.push(result);
} else {
failed_updates.push(result);
}
}
if is_json_mode() {
return;
}
anstream::eprintln!();
anstream::eprintln!("{}", language.get_text("update_summary").bold());
if !success_updates.is_empty() {
for result in &success_updates {
status(
"Updated",
&format!(
"{} {}",
result.package_name.cyan(),
format_version_info(&result.old_version, &result.new_version, language)
),
);
}
}
if !failed_updates.is_empty() {
for result in &failed_updates {
let detail = match &result.old_version {
Some(old) => format!(
"{} {} ({})",
result.package_name.cyan(),
old.red(),
language.get_text("update_failed")
),
None => format!(
"{} ({})",
result.package_name.cyan(),
language.get_text("update_failed")
),
};
status_err("Failed", &detail);
}
}
let prebuilt: Vec<&str> = success_updates
.iter()
.filter(|r| r.install_method == InstallMethod::Downloader)
.map(|r| r.package_name.as_str())
.collect();
let compiled: Vec<&str> = success_updates
.iter()
.filter(|r| r.install_method == InstallMethod::CargoInstall)
.map(|r| r.package_name.as_str())
.collect();
if !prebuilt.is_empty() {
status_dim(language.get_text("summary_prebuilt"), &prebuilt.join(", ").cyan().to_string());
}
if !compiled.is_empty() {
status_dim(language.get_text("summary_compiled"), &compiled.join(", ").yellow().to_string());
}
}
pub fn print_update_selection(
stable_updates: &[&PackageInfo],
prerelease_updates: &[&PackageInfo],
language: Language,
) -> Result<Vec<usize>, anyhow::Error> {
anstream::eprintln!();
anstream::eprintln!("{}", language.get_text("updates_detected").bold());
if !stable_updates.is_empty() {
anstream::eprintln!("{}", language.get_text("stable_updates").dimmed());
for package in stable_updates {
status("Updating", &package_transition(package, language));
}
}
if !prerelease_updates.is_empty() {
anstream::eprintln!("{}", language.get_text("prerelease_updates").dimmed());
for package in prerelease_updates {
status_warn(
"Prerelease",
&format!(
"{} {} -> {}",
package.name.cyan(),
package
.current_version
.as_deref()
.unwrap_or(language.get_text("unknown"))
.red(),
package
.latest_version
.as_deref()
.unwrap_or(language.get_text("unknown"))
.yellow(),
),
);
}
}
anstream::eprintln!();
let should_update = match Confirm::new()
.with_prompt(language.get_text("update_question"))
.default(true)
.show_default(false)
.interact()
{
Ok(choice) => choice,
Err(e) => {
if e.to_string().contains("not a terminal") {
status_warn("Note", language.get_text("no_interactive_mode"));
return Ok(vec![]);
}
return Err(e.into());
}
};
if !should_update {
return Ok(vec![]);
}
let mut packages_to_update = stable_updates.to_vec();
if !prerelease_updates.is_empty() {
let include_prerelease = match Confirm::new()
.with_prompt(language.get_text("include_prerelease_question"))
.default(false)
.show_default(false)
.interact()
{
Ok(choice) => choice,
Err(e) => {
if e.to_string().contains("not a terminal") {
status_warn("Note", language.get_text("no_interactive_mode"));
false
} else {
return Err(e.into());
}
}
};
if include_prerelease {
packages_to_update.extend(prerelease_updates);
}
}
let package_names: Vec<String> = packages_to_update.iter().map(|p| p.name.clone()).collect();
let selections = match MultiSelect::new()
.with_prompt(language.get_text("select_packages"))
.items(&package_names)
.interact()
{
Ok(choices) => choices,
Err(e) => {
if e.to_string().contains("not a terminal") {
anstream::eprintln!("{}", language.get_text("no_interactive_mode").yellow());
(0..package_names.len()).collect()
} else {
return Err(e.into());
}
}
};
Ok(selections)
}