use std::io::{self, BufRead, Write};
pub const RESET: &str = "\x1b[0m";
pub const BOLD: &str = "\x1b[1m";
pub const DIM: &str = "\x1b[2m";
pub const RED: &str = "\x1b[31m";
pub const GREEN: &str = "\x1b[32m";
pub const YELLOW: &str = "\x1b[33m";
pub const BLUE: &str = "\x1b[34m";
pub const MAGENTA: &str = "\x1b[35m";
pub const CYAN: &str = "\x1b[36m";
fn color_enabled() -> bool {
if std::env::var_os("NO_COLOR").is_some() {
return false;
}
#[cfg(unix)]
{
unsafe { libc_isatty(1) }
}
#[cfg(not(unix))]
{
true
}
}
#[cfg(unix)]
unsafe fn libc_isatty(fd: i32) -> bool {
unsafe { libc_isatty_inner(fd) != 0 }
}
#[cfg(unix)]
unsafe fn libc_isatty_inner(fd: i32) -> i32 {
unsafe extern "C" {
fn isatty(fd: i32) -> i32;
}
unsafe { isatty(fd) }
}
pub fn paint(color: &str, text: &str) -> String {
if color_enabled() {
format!("{color}{text}{RESET}")
} else {
text.to_string()
}
}
pub fn success(msg: &str) {
println!(" {} {msg}", paint(GREEN, "✓"));
}
pub fn error(msg: &str) {
eprintln!(" {} {msg}", paint(RED, "✗"));
}
pub fn warning(msg: &str) {
eprintln!(" {} {msg}", paint(YELLOW, "⚠"));
}
pub fn info(msg: &str) {
println!(" {} {msg}", paint(BLUE, "·"));
}
pub fn hint(msg: &str) {
println!(" {}", paint(DIM, msg));
}
pub fn header(title: &str) {
println!("\n{}", paint(BOLD, title));
}
pub fn dim(msg: &str) {
println!(" {}", paint(DIM, msg));
}
pub enum Status {
Ok,
Modified,
Missing,
Broken,
Encrypted,
Template,
Conflict,
}
impl Status {
fn symbol(&self) -> &str {
match self {
Self::Ok => "✓",
Self::Modified => "~",
Self::Missing => "✗",
Self::Broken => "!",
Self::Encrypted => "🔒",
Self::Template => "📄",
Self::Conflict => "⚡",
}
}
fn color(&self) -> &str {
match self {
Self::Ok | Self::Encrypted | Self::Template => GREEN,
Self::Modified => YELLOW,
Self::Missing | Self::Broken => RED,
Self::Conflict => MAGENTA,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncBadge {
InSync,
NeedsSync,
HasDiff,
}
impl SyncBadge {
fn render(self) -> String {
match self {
Self::InSync => paint(GREEN, "[in sync]"),
Self::NeedsSync => paint(YELLOW, "[needs sync]"),
Self::HasDiff => format!(
"{} {}",
paint(YELLOW, "[needs sync]"),
paint(MAGENTA, "[diff]")
),
}
}
}
pub fn status_line(status: &Status, source: &str, target: &str, badge: SyncBadge) {
let sym = paint(status.color(), status.symbol());
let src = paint(CYAN, source);
let arrow = paint(DIM, "→");
let badge_str = badge.render();
println!(" {sym} {src} {arrow} {target} {badge_str}");
}
pub fn summary(ok: usize, warnings: usize, errors: usize) {
let parts: Vec<String> = [
(ok, "ok", GREEN),
(warnings, "warning", YELLOW),
(errors, "error", RED),
]
.iter()
.filter(|(n, _, _)| *n > 0)
.map(|(n, label, color)| {
let plural = if *n == 1 { "" } else { "s" };
paint(color, &format!("{n} {label}{plural}"))
})
.collect();
if parts.is_empty() {
println!("\n {}", paint(DIM, "nothing to do"));
} else {
println!("\n {}", parts.join(", "));
}
}
pub fn confirm(question: &str) -> bool {
print!(" {} {question} [y/N] ", paint(YELLOW, "?"));
io::stdout().flush().ok();
let mut input = String::new();
if io::stdin().lock().read_line(&mut input).is_err() {
return false;
}
matches!(input.trim().to_ascii_lowercase().as_str(), "y" | "yes")
}
pub fn password(question: &str) -> String {
print!(" {} {question}: ", paint(YELLOW, "🔑"));
io::stdout().flush().ok();
#[cfg(unix)]
{
if let Some(pwd) = read_password_unix() {
println!(); return pwd;
}
}
let mut input = String::new();
io::stdin().lock().read_line(&mut input).ok();
input.trim().to_string()
}
#[cfg(unix)]
fn read_password_unix() -> Option<String> {
use std::os::unix::io::AsRawFd;
let stdin = io::stdin();
let fd = stdin.as_raw_fd();
unsafe {
let mut termios = std::mem::zeroed::<Termios>();
if tcgetattr(fd, &raw mut termios) != 0 {
return None;
}
let mut noecho = termios;
noecho.c_lflag &= !ECHO;
if tcsetattr(fd, 0, &raw const noecho) != 0 {
return None;
}
let mut input = String::new();
let result = stdin.lock().read_line(&mut input);
tcsetattr(fd, 0, &raw const termios);
result.ok()?;
Some(input.trim().to_string())
}
}
#[cfg(unix)]
const ECHO: u64 = 0x0000_0008;
#[cfg(unix)]
#[repr(C)]
#[derive(Clone, Copy)]
#[allow(clippy::struct_field_names)]
struct Termios {
c_iflag: u64,
c_oflag: u64,
c_cflag: u64,
c_lflag: u64,
c_cc: [u8; 20],
c_ispeed: u64,
c_ospeed: u64,
}
#[cfg(unix)]
unsafe extern "C" {
fn tcgetattr(fd: i32, termios: *mut Termios) -> i32;
fn tcsetattr(fd: i32, optional_actions: i32, termios: *const Termios) -> i32;
}
pub fn print_diff(source_label: &str, target_label: &str, source: &str, target: &str) {
let source_lines: Vec<&str> = source.lines().collect();
let target_lines: Vec<&str> = target.lines().collect();
println!("{} {source_label}", paint(RED, "---"));
println!("{} {target_label}", paint(GREEN, "+++"));
let max = source_lines.len().max(target_lines.len());
for i in 0..max {
match (source_lines.get(i), target_lines.get(i)) {
(Some(a), Some(b)) if a == b => {
println!(" {a}");
}
(Some(a), Some(b)) => {
println!("{}", paint(RED, &format!("-{a}")));
println!("{}", paint(GREEN, &format!("+{b}")));
}
(Some(a), None) => {
println!("{}", paint(RED, &format!("-{a}")));
}
(None, Some(b)) => {
println!("{}", paint(GREEN, &format!("+{b}")));
}
(None, None) => {}
}
}
}
pub fn conflict_header(origin_label: &str, source: &str, target: &str) {
println!("\n {} conflict ({origin_label}):", paint(MAGENTA, "⚡"));
println!(" {} {} {}", paint(CYAN, source), paint(DIM, "↔"), target);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConflictChoice {
KeepLocal,
UseRepo,
Merge,
Skip,
ShowDiff,
}
pub fn conflict_prompt(supports_merge: bool) -> ConflictChoice {
let merge_hint = if supports_merge { " [m]erge" } else { "" };
loop {
print!(
" {} [k]eep local [r]epo{merge_hint} [d]iff [s]kip > ",
paint(YELLOW, "?"),
);
io::stdout().flush().ok();
let mut input = String::new();
if io::stdin().lock().read_line(&mut input).is_err() || input.is_empty() {
return ConflictChoice::Skip;
}
match input.trim().to_ascii_lowercase().as_str() {
"k" | "keep" | "keep-local" | "keeplocal" => return ConflictChoice::KeepLocal,
"r" | "repo" => return ConflictChoice::UseRepo,
"m" | "merge" if supports_merge => return ConflictChoice::Merge,
"d" | "diff" => return ConflictChoice::ShowDiff,
"s" | "skip" | "" => return ConflictChoice::Skip,
_ => {
println!(" {}", paint(DIM, "unrecognised — type k, r, m, d, or s"));
}
}
}
}
pub fn merge_conflict_notice(conflict_count: usize, path: &std::path::Path) {
println!(
" {} {conflict_count} conflict hunk(s) need manual resolution in {}",
paint(YELLOW, "⚠"),
paint(CYAN, &path.display().to_string()),
);
}
pub fn merge_clean_notice(path: &std::path::Path) {
println!(
" {} merged cleanly → {}",
paint(GREEN, "✓"),
paint(CYAN, &path.display().to_string()),
);
}