use std::{
io::{self, Write},
sync::OnceLock,
thread,
time::Duration,
};
use owo_colors::OwoColorize;
static COLOR_ENABLED: OnceLock<bool> = OnceLock::new();
static PIPE_MODE: OnceLock<bool> = OnceLock::new();
pub fn colors_enabled() -> bool {
*COLOR_ENABLED.get_or_init(|| {
if std::env::var("NO_COLOR").is_ok() {
return false;
}
supports_color::on(supports_color::Stream::Stdout).is_some_and(|level| level.has_basic)
})
}
pub fn pipe_mode() -> bool {
*PIPE_MODE.get_or_init(|| {
use std::io::IsTerminal;
!std::io::stdout().is_terminal()
})
}
pub fn success(s: &str) -> String {
if colors_enabled() {
s.green().bold().to_string()
} else {
s.to_string()
}
}
pub fn warning(s: &str) -> String {
if colors_enabled() {
s.yellow().to_string()
} else {
s.to_string()
}
}
pub fn error(s: &str) -> String {
if colors_enabled() {
s.red().bold().to_string()
} else {
s.to_string()
}
}
pub fn info(s: &str) -> String {
if colors_enabled() {
s.cyan().to_string()
} else {
s.to_string()
}
}
pub fn warn(msg: &str) {
if !pipe_mode() {
print!("\r\x1b[K");
io::stdout().flush().ok();
}
eprintln!("{} {}", warning(icons::WARNING), warning(msg));
}
pub fn dim(s: &str) -> String {
if colors_enabled() {
s.dimmed().to_string()
} else {
s.to_string()
}
}
pub fn bold(s: &str) -> String {
if colors_enabled() {
s.bold().to_string()
} else {
s.to_string()
}
}
pub fn model(s: &str) -> String {
if colors_enabled() {
s.magenta().to_string()
} else {
s.to_string()
}
}
pub fn commit_type(s: &str) -> String {
if colors_enabled() {
s.blue().bold().to_string()
} else {
s.to_string()
}
}
pub fn scope(s: &str) -> String {
if colors_enabled() {
s.cyan().to_string()
} else {
s.to_string()
}
}
pub fn term_width() -> usize {
terminal_size::terminal_size()
.map_or(80, |(w, _)| w.0 as usize)
.min(120)
}
pub mod box_chars {
pub const TOP_LEFT: char = '\u{256D}';
pub const TOP_RIGHT: char = '\u{256E}';
pub const BOTTOM_LEFT: char = '\u{2570}';
pub const BOTTOM_RIGHT: char = '\u{256F}';
pub const HORIZONTAL: char = '\u{2500}';
pub const VERTICAL: char = '\u{2502}';
}
fn wrap_line(line: &str, max_width: usize) -> Vec<String> {
if line.is_empty() {
return vec![String::new()];
}
let mut lines = Vec::new();
let mut current = String::new();
for word in line.split_whitespace() {
let word_len = word.chars().count();
let current_len = current.chars().count();
if current.is_empty() {
current = word.to_string();
} else if current_len + 1 + word_len <= max_width {
current.push(' ');
current.push_str(word);
} else {
lines.push(current);
current = word.to_string();
}
}
if !current.is_empty() {
lines.push(current);
}
lines
}
pub fn boxed_message(title: &str, content: &str, width: usize) -> String {
use box_chars::*;
let mut out = String::new();
let inner_width = width.saturating_sub(4);
let title_len = title.chars().count();
let border_width = width.saturating_sub(2);
let padding = border_width.saturating_sub(title_len + 2);
let left_pad = padding / 2;
let right_pad = padding - left_pad;
out.push(TOP_LEFT);
out.push_str(&HORIZONTAL.to_string().repeat(left_pad));
out.push(' ');
out.push_str(&if colors_enabled() {
bold(title)
} else {
title.to_string()
});
out.push(' ');
out.push_str(&HORIZONTAL.to_string().repeat(right_pad));
out.push(TOP_RIGHT);
out.push('\n');
for line in content.lines() {
let wrapped = wrap_line(line, inner_width);
for wrapped_line in wrapped {
out.push(VERTICAL);
out.push(' ');
let line_chars = wrapped_line.chars().count();
out.push_str(&wrapped_line);
let pad = inner_width.saturating_sub(line_chars);
out.push_str(&" ".repeat(pad));
out.push(' ');
out.push(VERTICAL);
out.push('\n');
}
}
out.push(BOTTOM_LEFT);
out.push_str(&HORIZONTAL.to_string().repeat(border_width));
out.push(BOTTOM_RIGHT);
out
}
pub fn print_info(msg: &str) {
use std::io::IsTerminal;
if std::io::stderr().is_terminal() && colors_enabled() {
eprintln!("\r\x1b[K{} {msg}", icons::INFO.cyan());
} else {
eprintln!("{} {msg}", icons::INFO);
}
}
pub fn separator(width: usize) -> String {
let line = box_chars::HORIZONTAL.to_string().repeat(width);
if colors_enabled() { dim(&line) } else { line }
}
pub fn section_header(title: &str, width: usize) -> String {
let title_len = title.chars().count();
let line_len = (width.saturating_sub(title_len + 2)) / 2;
let line = box_chars::HORIZONTAL.to_string().repeat(line_len);
if colors_enabled() {
format!("{} {} {}", dim(&line), bold(title), dim(&line))
} else {
format!("{line} {title} {line}")
}
}
pub mod icons {
pub const SUCCESS: &str = "\u{2713}";
pub const WARNING: &str = "\u{26A0}";
pub const ERROR: &str = "\u{2717}";
pub const INFO: &str = "\u{2139}";
pub const ARROW: &str = "\u{2192}";
pub const BULLET: &str = "\u{2022}";
pub const CLIPBOARD: &str = "\u{1F4CB}";
pub const SEARCH: &str = "\u{1F50D}";
pub const ROBOT: &str = "\u{1F916}";
pub const SAVE: &str = "\u{1F4BE}";
}
const SPINNER_FRAMES: &[char] = &[
'\u{280B}', '\u{2819}', '\u{2839}', '\u{2838}', '\u{283C}', '\u{2834}', '\u{2826}', '\u{2827}',
'\u{2807}', '\u{280F}',
];
pub async fn with_spinner<F: Future<Output = T>, T>(message: &str, f: F) -> T {
if !colors_enabled() {
eprintln!("{message}");
return f.await;
}
let (tx, rx) = std::sync::mpsc::channel::<()>();
let msg = message.to_string();
let spinner = thread::spawn(move || {
let mut idx = 0;
loop {
if rx.try_recv().is_ok() {
print!("\r\x1b[K{} {}\n", icons::SUCCESS.green(), msg);
io::stdout().flush().ok();
break;
}
print!("\r{} {}", SPINNER_FRAMES[idx].cyan(), msg);
io::stdout().flush().ok();
idx = (idx + 1) % SPINNER_FRAMES.len();
thread::sleep(Duration::from_millis(80));
}
});
let result = f.await;
tx.send(()).ok();
spinner.join().ok();
result
}
pub async fn with_spinner_result<F: Future<Output = Result<T, E>>, T, E>(
message: &str,
f: F,
) -> Result<T, E> {
if !colors_enabled() {
eprintln!("{message}");
return f.await;
}
let (tx, rx) = std::sync::mpsc::channel::<bool>();
let msg = message.to_string();
let spinner = thread::spawn(move || {
let mut idx = 0;
loop {
match rx.try_recv() {
Ok(success) => {
let icon = if success {
icons::SUCCESS.green().to_string()
} else {
icons::ERROR.red().to_string()
};
print!("\r\x1b[K{icon} {msg}\n");
io::stdout().flush().ok();
break;
},
Err(std::sync::mpsc::TryRecvError::Disconnected) => break,
Err(std::sync::mpsc::TryRecvError::Empty) => {},
}
print!("\r{} {}", SPINNER_FRAMES[idx].cyan(), msg);
io::stdout().flush().ok();
idx = (idx + 1) % SPINNER_FRAMES.len();
thread::sleep(Duration::from_millis(80));
}
});
let result = f.await;
tx.send(result.is_ok()).ok();
spinner.join().ok();
result
}