pub(crate) mod detail;
pub(crate) mod table;
use std::fmt::Write as _;
use anstyle::{AnsiColor, Color, Style};
use reserve_core::{Catalog, Extension, Family, Page, Sort, SortDirection, SortKey};
use crate::output::table::{Column, Table};
pub(crate) fn scrub(raw: &str) -> String {
raw.chars()
.filter(|c| !c.is_control() && !reserve_core::reorders_text(*c))
.take(200)
.collect()
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Palette {
enabled: bool,
}
impl Palette {
#[must_use]
pub(crate) const fn new(enabled: bool) -> Self {
Self { enabled }
}
fn paint(self, text: &str, style: Style) -> String {
if self.enabled {
format!("{style}{text}{style:#}")
} else {
text.to_owned()
}
}
#[must_use]
pub(crate) fn heading(self, text: &str) -> String {
self.paint(text, Style::new().bold())
}
#[must_use]
pub(crate) fn dim(self, text: &str) -> String {
self.paint(text, Style::new().dimmed())
}
#[must_use]
pub(crate) fn error(self, text: &str) -> String {
self.paint(
text,
Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red))),
)
}
#[must_use]
pub(crate) fn success(self, text: &str) -> String {
self.paint(
text,
Style::new()
.fg_color(Some(Color::Ansi(AnsiColor::Green)))
.bold(),
)
}
#[must_use]
pub(crate) fn warning(self, text: &str) -> String {
self.paint(
text,
Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow))),
)
}
#[must_use]
pub(crate) fn accent(self, text: &str) -> String {
self.paint(
text,
Style::new().fg_color(Some(Color::Ansi(AnsiColor::Cyan))),
)
}
}
#[must_use]
pub(crate) fn format_thousands(value: u64) -> String {
let digits = value.to_string();
let mut out = String::with_capacity(digits.len().saturating_add(digits.len().div_ceil(3)));
for (index, ch) in digits.chars().enumerate() {
let remaining = digits.len() - index;
if index > 0 && remaining.is_multiple_of(3) {
out.push(',');
}
out.push(ch);
}
out
}
#[must_use]
pub(crate) fn groups(
catalog: &Catalog,
only: Option<Family>,
palette: Palette,
width: usize,
) -> String {
let families = only.map_or_else(|| Family::all().to_vec(), |family| vec![family]);
let mut out = String::new();
for (index, family) in families.iter().enumerate() {
let family_groups = catalog.groups_in(*family);
if family_groups.is_empty() {
continue;
}
if index > 0 {
out.push('\n');
}
let _ = writeln!(out, "{}", palette.heading(family.title()));
let mut table = Table::new(vec![
Column::left("GROUP").shrinks_to(8),
Column::right("EXTENSIONS").drop_order(2).shrinks_to(4),
Column::left("WHAT IT COVERS").drop_order(1).shrinks_to(14),
]);
for group in family_groups {
table.push(vec![
palette.accent(&group.key),
catalog.group_size(group).to_string(),
palette.dim(&group.summary),
]);
}
out.push_str(&table.render(false, width));
}
if out.is_empty() {
return "No groups matched.\n".to_owned();
}
out
}
#[must_use]
pub(crate) fn extensions(
items: &[&Extension],
page: Page,
sort: Sort,
palette: Palette,
show_footer: bool,
width: usize,
) -> String {
let visible = page.slice(items);
let mut table = Table::new(vec![
Column::left("EXTENSION").shrinks_to(10),
Column::right("RANK").drop_order(3).shrinks_to(4),
Column::left("KIND").drop_order(2).shrinks_to(7),
Column::left("USED FOR")
.drop_order(1)
.shrinks_to(12)
.caps_at(44),
]);
for ext in visible {
table.push(vec![
palette.accent(&ext.suffix.human()),
ext.rank
.map_or_else(|| "-".to_owned(), |rank| rank.to_string()),
ext.kind.label().to_owned(),
palette.dim(&describe_use(ext)),
]);
}
let mut out = table.render(true, width);
if show_footer {
out.push('\n');
for line in footer(items.len(), page, sort, palette).lines() {
out.push_str(&fit_footer(line, width));
out.push('\n');
}
}
out
}
fn describe_use(ext: &Extension) -> String {
let mut parts: Vec<String> = Vec::new();
if let Some(country) = &ext.country {
parts.push(country.clone());
}
if !ext.industries.is_empty() {
parts.push(ext.industries.join(", "));
}
if ext.repurposed {
parts.push("often used off-label".to_owned());
}
if !ext.registrable {
parts.push("not publicly registrable".to_owned());
}
parts.join(" · ")
}
#[must_use]
fn fit_footer(line: &str, width: usize) -> String {
use crate::output::table::{display_width, truncate};
if width == 0 || display_width(line) <= width {
return line.to_owned();
}
let Some((head, flags)) = line.split_once(": ") else {
return truncate(line, width);
};
let mut kept: Vec<&str> = Vec::new();
for flag in flags.split(" | ") {
let candidate = format!(
"{head}: {}",
[kept.as_slice(), &[flag]].concat().join(" | ")
);
if display_width(&candidate) > width {
break;
}
kept.push(flag);
}
if kept.is_empty() {
return truncate(line, width);
}
format!("{head}: {}", kept.join(" | "))
}
pub(crate) fn footer(total: usize, page: Page, sort: Sort, palette: Palette) -> String {
let pages = page.pages_for(total);
let page = Page::new(page.number.min(pages), page.size);
let shown = total.saturating_sub(page.offset()).min(page.size);
let first = if total == 0 { 0 } else { page.offset() + 1 };
let last = page.offset() + shown;
let mut line = format!(
"{} of {} extensions · page {} of {} · sorted by {} {}",
palette.heading(&format!("{first}-{last}")),
format_thousands(total as u64),
page.number.min(pages),
pages,
sort.key,
describe_direction(sort),
);
if pages > 1 {
let mut page_flags: Vec<String> = Vec::new();
if page.previous().is_some() {
page_flags.push(format!("--page {}", page.number.saturating_sub(1)));
}
if page.next(total).is_some() {
page_flags.push(format!("--page {}", page.number.saturating_add(1)));
}
page_flags.push("--all-pages".to_owned());
let _ = write!(
line,
"\n{}",
palette.dim(&format!("next: {}", page_flags.join(" | ")))
);
}
line.push('\n');
line
}
const fn describe_direction(sort: Sort) -> &'static str {
match (sort.key, sort.direction) {
(SortKey::Popularity, SortDirection::Descending) => "(most used first)",
(SortKey::Popularity, SortDirection::Ascending) => "(least used first)",
(SortKey::Length, SortDirection::Ascending) => "(shortest first)",
(SortKey::Length, SortDirection::Descending) => "(longest first)",
(SortKey::Name, SortDirection::Ascending) => "(a to z)",
(SortKey::Name, SortDirection::Descending) => "(z to a)",
}
}
#[must_use]
pub(crate) fn findings(
items: &[&reserve_core::Finding],
tally: reserve_core::Tally,
palette: Palette,
width: usize,
) -> String {
let mut table = Table::new(vec![
Column::left("").shrinks_to(1),
Column::left("DOMAIN").shrinks_to(12),
Column::left("STATUS").shrinks_to(7),
Column::left("SOURCE").drop_order(2).shrinks_to(6),
Column::right("TIME").drop_order(3).shrinks_to(4),
Column::left("NOTE").drop_order(1).shrinks_to(10),
]);
for finding in items {
let mark = match finding.status {
reserve_core::Status::Available => palette.success("+"),
reserve_core::Status::Taken => palette.error("-"),
reserve_core::Status::Unknown(_) => palette.warning("?"),
};
let status = match &finding.status {
reserve_core::Status::Available => palette.success("AVAILABLE"),
reserve_core::Status::Taken => palette.error("TAKEN"),
reserve_core::Status::Unknown(_) => palette.warning("UNKNOWN"),
};
let note = match &finding.status {
reserve_core::Status::Unknown(reason) => reason.to_string(),
_ => String::new(),
};
table.push(vec![
mark,
palette.accent(&format!("{}.{}", finding.name, finding.suffix.human())),
status,
finding
.source
.map_or_else(String::new, |source| source.label().to_owned()),
format!("{}ms", finding.elapsed.as_millis()),
palette.dim(¬e),
]);
}
let mut out = table.render(true, width);
if items.is_empty() {
out.push_str(&palette.dim(
"Nothing to show. Widen the group, or drop --available-only to see what is taken.\n",
));
}
out.push('\n');
out.push_str(&format!(
"{} available {} taken {} unknown ({} checked)\n",
palette.success(&tally.available.to_string()),
tally.taken,
tally.unknown,
tally.checked(),
));
if tally.unknown > 0 {
out.push_str(
&palette.dim("unknown means no registry would answer, never that the name is free\n"),
);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_result_table_says_what_it_found_and_never_calls_an_unanswered_lookup_free() {
use reserve_core::{Reason, Status};
let mut items = Vec::new();
for (name, status) in [
("free.example", Status::Available),
("held.example", Status::Taken),
("quiet.example", Status::Unknown(Reason::NoService)),
] {
let mut found = reserve_core::Finding::unrecognized(name);
found.status = status;
items.push(found);
}
let borrowed: Vec<&reserve_core::Finding> = items.iter().collect();
let tally = reserve_core::Tally::of(&items);
let rendered = findings(&borrowed, tally, palette(), 100);
assert!(rendered.contains("AVAILABLE"), "{rendered}");
assert!(rendered.contains("TAKEN"), "{rendered}");
assert!(rendered.contains("UNKNOWN"), "{rendered}");
assert!(
rendered.contains("1 available 1 taken 1 unknown"),
"the tally has to count what the rows say:\n{rendered}"
);
assert!(
rendered.contains("never that the name is free"),
"an unknown row has to carry its warning:\n{rendered}"
);
}
#[test]
fn a_narrow_paging_hint_drops_whole_flags_so_it_can_still_be_copied() {
let line = "next: --page 2 | --page 4 | --all-pages";
assert_eq!(fit_footer(line, 100), line, "a wide line is left alone");
for width in [14_usize, 20, 24, 30, 36, 40] {
let fitted = fit_footer(line, width);
assert!(
crate::output::table::display_width(&fitted) <= width,
"{fitted:?} ran past {width}"
);
for flag in fitted.trim_start_matches("next: ").split(" | ") {
if flag.is_empty() || flag.contains('\u{2026}') {
continue;
}
assert!(
line.contains(flag),
"at {width} the hint offered `{flag}`, which is not a flag it was given"
);
}
}
}
#[test]
fn an_empty_result_table_says_why_it_is_empty() {
let tally = reserve_core::Tally::of(&[]);
let rendered = findings(&[], tally, palette(), 100);
assert!(
rendered.contains("Nothing to show"),
"a bare heading reads as a broken run:\n{rendered}"
);
assert!(rendered.contains("0 available"), "{rendered}");
}
#[test]
fn text_from_a_stranger_cannot_move_the_cursor_or_reverse_the_line() {
let hostile = "name\u{1b}[2Kcut\u{202e}reversed\u{feff}\u{200b}\r\n";
let clean = scrub(hostile);
assert!(!clean.contains('\u{1b}'), "{clean:?}");
assert!(
!clean.contains('\u{202e}'),
"a reversing mark survived: {clean:?}"
);
assert!(!clean.contains('\u{feff}'), "{clean:?}");
assert!(!clean.contains('\u{200b}'), "{clean:?}");
assert!(!clean.contains('\r') && !clean.contains('\n'), "{clean:?}");
let long = "x".repeat(500);
assert!(
scrub(&long).chars().count() <= 200,
"a remote party must not decide how much of the screen it takes"
);
}
fn palette() -> Palette {
Palette::new(false)
}
#[test]
fn counts_get_thousands_separators() {
assert_eq!(format_thousands(0), "0");
assert_eq!(format_thousands(999), "999");
assert_eq!(format_thousands(1000), "1,000");
assert_eq!(format_thousands(1234567), "1,234,567");
}
#[test]
fn the_footer_reports_the_visible_range_and_the_total() {
let sort = Sort::default();
let text = footer(120, Page::new(2, 25), sort, palette());
assert!(text.contains("26-50"), "{text}");
assert!(text.contains("120 extensions"), "{text}");
assert!(text.contains("page 2 of 5"), "{text}");
}
#[test]
fn the_footer_offers_only_the_moves_that_exist() {
let sort = Sort::default();
let first = footer(60, Page::new(1, 25), sort, palette());
assert!(!first.contains("--page 0"), "{first}");
assert!(first.contains("--page 2"), "{first}");
let last = footer(60, Page::new(3, 25), sort, palette());
assert!(last.contains("--page 2"), "{last}");
assert!(!last.contains("--page 4"), "{last}");
}
#[test]
fn a_single_page_gets_no_navigation_line() {
let text = footer(10, Page::new(1, 25), Sort::default(), palette());
assert!(!text.contains("next:"), "{text}");
}
#[test]
fn an_empty_list_reports_zero_rather_than_a_broken_range() {
let text = footer(0, Page::default(), Sort::default(), palette());
assert!(text.contains("0-0"), "{text}");
}
#[test]
fn a_disabled_palette_adds_no_escape_codes() {
let plain = Palette::new(false);
assert_eq!(plain.error("taken"), "taken");
assert_eq!(plain.heading("Groups"), "Groups");
}
#[test]
fn an_enabled_palette_wraps_the_text() {
let colored = Palette::new(true);
let painted = colored.error("taken");
assert!(painted.contains("taken"));
assert!(painted.len() > "taken".len());
}
#[test]
fn the_group_listing_names_every_family_present() {
let catalog = Catalog::bundled().expect("bundled catalog");
let text = groups(&catalog, None, palette(), 0);
for family in Family::all() {
assert!(text.contains(family.title()), "{} missing", family.title());
}
}
#[test]
fn filtering_the_group_listing_shows_one_family_only() {
let catalog = Catalog::bundled().expect("bundled catalog");
let text = groups(&catalog, Some(Family::Region), palette(), 0);
assert!(text.contains(Family::Region.title()));
assert!(!text.contains(Family::Industry.title()));
}
#[test]
fn a_name_from_a_list_file_cannot_move_the_cursor_when_it_is_echoed_back() {
let hostile = "\u{1b}[2J\u{7f}evil\rname";
let shown = scrub(hostile);
assert!(!shown.contains('\u{1b}'));
assert!(!shown.contains('\r'));
assert_eq!(shown, "[2Jevilname");
}
#[test]
fn the_summary_line_is_cut_to_the_width_the_rows_are_cut_to() {
let catalog = reserve_core::Catalog::bundled().expect("the bundled catalog parses");
let plan = reserve_core::SweepPlan {
group_keys: vec!["everything".to_owned()],
..reserve_core::SweepPlan::default()
};
let all = catalog
.extensions_for(&plan)
.expect("the everything group selects something");
for width in [30_usize, 40, 60] {
let rendered = extensions(
&all,
Page::new(1, 10),
Sort::default(),
palette(),
true,
width,
);
for line in rendered.lines() {
assert!(
table::display_width(line) <= width,
"a {} column line ran past --width {width}: {line}",
table::display_width(line)
);
}
}
}
}