use std::io::{Write, stdout};
use is_terminal::IsTerminal;
use termimad::MadSkin;
pub fn should_render(enabled: bool) -> bool {
if !enabled {
return false;
}
if std::env::var_os("NO_COLOR").is_some() {
return false;
}
stdout().is_terminal()
}
pub fn default_skin() -> MadSkin {
MadSkin::default_dark()
}
fn visual_lines(text: &str, width: usize) -> usize {
if width == 0 {
return text.lines().count().max(1);
}
let mut lines = 0usize;
let segments: Vec<&str> = text.split('\n').collect();
for (i, seg) in segments.iter().enumerate() {
let cols = unicode_width_approx(seg);
if cols == 0 {
if i + 1 == segments.len() {
continue;
}
lines += 1;
} else {
lines += cols.div_ceil(width);
}
}
lines.max(1)
}
fn unicode_width_approx(s: &str) -> usize {
let mut w = 0usize;
for ch in s.chars() {
if ch == '\r' {
continue;
}
if (ch as u32) < 0x80 {
w += 1;
} else if ch.is_ascii() {
w += 1;
} else {
w += 2;
}
}
w
}
fn erase_lines_above(lines: usize) {
let mut out = stdout().lock();
let _ = write!(out, "\r");
let _ = write!(out, "\x1b[2K");
for _ in 1..lines {
let _ = write!(out, "\x1b[1A\x1b[2K");
}
let _ = out.flush();
}
pub fn rerender_over(raw: &str, skin: &MadSkin, term_width: usize) {
if raw.is_empty() {
return;
}
let lines = visual_lines(raw, term_width.max(1));
erase_lines_above(lines);
let rendered = skin.text(raw, Some(term_width));
print!("{}", rendered);
let _ = stdout().flush();
}
pub fn term_width() -> usize {
termimad::terminal_size().0 as usize
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn visual_lines_single_short() {
assert_eq!(visual_lines("hello", 80), 1);
}
#[test]
fn visual_lines_wrapping() {
assert_eq!(visual_lines("abcdefghij", 5), 2);
}
#[test]
fn visual_lines_multiple_newlines() {
assert_eq!(visual_lines("a\nb\nc", 80), 3);
}
#[test]
fn visual_lines_trailing_newline_ignored() {
assert_eq!(visual_lines("a\n", 80), 1);
}
#[test]
fn visual_lines_cjk_wide() {
assert_eq!(visual_lines("中文字", 4), 2);
}
}