use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct ListColors {
pub colors: HashMap<String, String>,
pub use_ls_colors: bool,
}
impl ListColors {
pub fn new() -> Self {
Self::default()
}
pub fn from_ls_colors(spec: &str) -> Self {
let mut colors = HashMap::new();
for entry in spec.split(':') {
if let Some((pattern, code)) = entry.split_once('=') {
colors.insert(pattern.to_string(), code.to_string());
}
}
ListColors {
colors,
use_ls_colors: true,
}
}
pub fn get_color(
&self,
name: &str,
is_dir: bool,
is_link: bool,
is_exec: bool,
) -> Option<String> {
if is_dir {
if let Some(c) = self.colors.get("di") {
return Some(format!("\x1b[{}m", c));
}
}
if is_link {
if let Some(c) = self.colors.get("ln") {
return Some(format!("\x1b[{}m", c));
}
}
if is_exec {
if let Some(c) = self.colors.get("ex") {
return Some(format!("\x1b[{}m", c));
}
}
if let Some(dot) = name.rfind('.') {
let ext = format!("*{}", &name[dot..]);
if let Some(c) = self.colors.get(&ext) {
return Some(format!("\x1b[{}m", c));
}
}
None
}
pub fn reset() -> &'static str {
"\x1b[0m"
}
}
#[derive(Debug, Clone)]
pub struct ListLayout {
pub columns: usize,
pub rows: usize,
pub col_widths: Vec<usize>,
pub total_width: usize,
}
pub fn calclist(
matches: &[String],
term_width: usize,
descriptions: &[Option<String>],
) -> ListLayout {
let max_len = matches
.iter()
.enumerate()
.map(|(i, m)| {
let desc_len = descriptions
.get(i)
.and_then(|d| d.as_ref())
.map(|d| d.len() + 4) .unwrap_or(0);
m.len() + desc_len
})
.max()
.unwrap_or(0);
let item_width = max_len + 2; let columns = (term_width / item_width.max(1)).max(1);
let rows = (matches.len() + columns - 1) / columns;
let mut col_widths = vec![item_width; columns];
if let Some(last) = col_widths.last_mut() {
*last = max_len;
}
let total_width = col_widths.iter().sum();
ListLayout {
columns,
rows,
col_widths,
total_width,
}
}
pub fn compprintlist(
matches: &[String],
descriptions: &[Option<String>],
groups: &[Option<String>],
layout: &ListLayout,
colors: &ListColors,
selected: Option<usize>,
) -> Vec<String> {
let mut lines = Vec::new();
let mut current_group: Option<&str> = None;
for row in 0..layout.rows {
let mut line = String::new();
for col in 0..layout.columns {
let idx = col * layout.rows + row;
if idx >= matches.len() {
break;
}
if let Some(Some(group)) = groups.get(idx) {
if current_group != Some(group.as_str()) {
current_group = Some(group);
lines.push(format!("\x1b[1m{}:\x1b[0m", group));
}
}
let m = &matches[idx];
let is_selected = selected == Some(idx);
let colored = if is_selected {
format!("\x1b[7m{}\x1b[0m", m) } else if let Some(color) = colors.get_color(m, false, false, false) {
format!("{}{}{}", color, m, ListColors::reset())
} else {
m.clone()
};
let desc = descriptions
.get(idx)
.and_then(|d| d.as_ref())
.map(|d| format!(" \x1b[2m-- {}\x1b[0m", d))
.unwrap_or_default();
let entry = format!("{}{}", colored, desc);
let visible_len = m.len()
+ descriptions
.get(idx)
.and_then(|d| d.as_ref())
.map(|d| d.len() + 4)
.unwrap_or(0);
line.push_str(&entry);
if col + 1 < layout.columns {
let padding = layout.col_widths[col].saturating_sub(visible_len);
for _ in 0..padding {
line.push(' ');
}
}
}
lines.push(line);
}
lines
}
pub fn asklistscroll(total: usize, shown: usize) -> String {
let remaining = total - shown;
format!("--More--({}/{})", shown, total)
}
pub fn compprintfmt(format: &str, matches_count: usize, group: &str) -> String {
format
.replace("%d", &matches_count.to_string())
.replace("%g", group)
.replace("%%", "%")
}
pub fn cleareol() -> &'static str {
"\x1b[K"
}
pub fn zcputs(s: &str, color: Option<&str>) -> String {
match color {
Some(c) => format!("\x1b[{}m{}\x1b[0m", c, s),
None => s.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ls_colors() {
let colors = ListColors::from_ls_colors("di=1;34:*.rs=0;32:*.c=0;33:ex=1;31");
assert!(colors.get_color("foo", true, false, false).is_some());
assert!(colors.get_color("main.rs", false, false, false).is_some());
assert!(colors.get_color("main.txt", false, false, false).is_none());
}
#[test]
fn test_calclist() {
let matches: Vec<String> = (0..20).map(|i| format!("item_{}", i)).collect();
let descs: Vec<Option<String>> = vec![None; 20];
let layout = calclist(&matches, 80, &descs);
assert!(layout.columns >= 1);
assert!(layout.rows >= 1);
assert_eq!(layout.columns * layout.rows >= matches.len(), true);
}
#[test]
fn test_compprintfmt() {
assert_eq!(
compprintfmt("Showing %d matches in %g", 42, "files"),
"Showing 42 matches in files"
);
}
}