use std::io;
use std::io::Write;
use clap::{CommandFactory, Parser};
use glyf::{
FIELD_GLYPH, Frecency, Idx, Sort, block, category, codepoint, entry_block, entry_category,
entry_icon_set, entry_name, entry_source, entry_str, icon_set_description, list_icon_sets,
list_sources, lookup_str, num_entries, output::print_entry, parse_cp_str, search_all,
search_in,
};
#[derive(Parser)]
#[command(
name = "glyf",
about = "Look up Unicode glyphs by name or codepoint",
version,
after_help = "If the bare argument is a hex codepoint (279F, U+279F, 0x279F), looks it up.\n\
Otherwise, searches by name. Use -s to force name search."
)]
struct Args {
#[arg(short = 's', long = "search", conflicts_with_all = &["codepoint", "pick", "list"], help = "Search by name (fuzzy)")]
search: Option<String>,
#[arg(short = 'c', long = "codepoint", conflicts_with_all = &["search", "pick", "list"], help = "Lookup by codepoint: 279f, U+279F, 0x279F")]
codepoint: Option<String>,
#[arg(short = 'p', long = "pick", conflicts_with_all = &["search", "codepoint", "list"], help = "Record a pick in frecency db")]
pick: Option<String>,
#[arg(short = 'l', long = "list", conflicts_with_all = &["search", "codepoint", "pick"], help = "List all entries")]
list: bool,
#[arg(short = 'b', long = "block", help = "Search within a Unicode block")]
block: Option<String>,
#[arg(long = "list-blocks", help = "List all Unicode blocks")]
list_blocks: bool,
#[arg(
short = 'C',
long = "category",
help = "Filter by Unicode General Category (e.g. Lu, Nd, L* for all letters, P* for punctuation)"
)]
category: Vec<String>,
#[arg(long = "list-categories", help = "List all Unicode General Categories")]
list_categories: bool,
#[arg(
short = 'S',
long = "source",
help = "Filter by source (e.g. unicode, nerdfonts)"
)]
source: Vec<String>,
#[arg(long = "list-sources", help = "List all sources")]
list_sources: bool,
#[arg(
short = 'I',
long = "icon-set",
help = "Filter by Nerd Fonts icon set (e.g. dev, fa, cod)"
)]
icon_set: Vec<String>,
#[arg(long = "list-icon-sets", help = "List all icon sets")]
list_icon_sets: bool,
#[arg(
short = 'f',
long = "format",
default_value = "pretty",
help = "Output format: pretty (default), plain, tsv"
)]
format: String,
#[arg(
short = 'n',
long = "limit",
default_value_t = 50,
help = "Max results (default: 50)"
)]
limit: usize,
#[arg(
short = 't',
long = "typos",
default_value_t = 2,
help = "Typo tolerance (default: 2, 0 = exact)"
)]
typos: u16,
#[arg(
long = "sort",
default_value = "relevance",
help = "Sort by: relevance (default, alias: score), name, codepoint"
)]
sort: String,
#[arg(help = "Query: hex codepoint or name to search")]
query: Option<String>,
}
fn print_help() -> ! {
let mut out = io::stdout();
let _ = Args::command().write_help(&mut out);
let _ = writeln!(&mut out);
std::process::exit(0);
}
fn exit_ok<T>(r: Result<T, impl std::fmt::Display>) -> T {
match r {
Ok(v) => v,
Err(e) => {
eprintln!("error: {e}");
std::process::exit(1);
}
}
}
struct Filter {
ranges: Vec<std::ops::Range<u32>>,
sources: Vec<&'static str>,
icon_sets: Vec<&'static str>,
categories: Vec<&'static str>,
}
fn entry_in_bounds(idx: Idx, f: &Filter) -> bool {
let cp = codepoint(idx);
(f.ranges.is_empty() || f.ranges.iter().any(|r| r.contains(&cp)))
&& (f.sources.is_empty() || f.sources.contains(&entry_source(idx)))
&& (f.icon_sets.is_empty() || f.icon_sets.contains(&entry_icon_set(idx)))
&& (f.categories.is_empty() || f.categories.contains(&entry_category(idx)))
}
fn no_matches(query: &str) -> ! {
eprintln!("no matches for {:?}", query);
std::process::exit(1);
}
fn print_entries_pretty(out: &mut impl io::Write, indices: &[Idx]) {
let names: Vec<&str> = indices.iter().map(|&idx| entry_name(idx)).collect();
let max_name = names.iter().map(|n| n.len()).max().unwrap_or(0);
for (&idx, &name) in indices.iter().zip(names.iter()) {
let _ = writeln!(
out,
"{}\tU+{:04X}\t{:max_name$}\t{}",
entry_str(idx, FIELD_GLYPH),
codepoint(idx),
name,
entry_block(idx),
);
}
}
fn print_results(out: &mut impl io::Write, results: &[glyf::Match], fmt: &glyf::Format) -> usize {
if matches!(fmt, glyf::Format::Pretty) {
let indices: Vec<Idx> = results.iter().map(|m| m.idx).collect();
print_entries_pretty(out, &indices);
return results.len();
}
for m in results {
let _ = print_entry(out, m.idx, fmt);
}
results.len()
}
fn do_search(query: &str, limit: usize, typos: u16, sort: Sort, fmt: &glyf::Format, f: &Filter) {
let frecency = Frecency::load();
if f.ranges.is_empty()
&& f.sources.is_empty()
&& f.icon_sets.is_empty()
&& f.categories.is_empty()
{
let results = search_all(query, &frecency, limit, Some(typos), sort);
if results.is_empty() {
no_matches(query);
}
print_results(&mut io::stdout().lock(), &results, fmt);
return;
}
let base: Box<dyn Iterator<Item = Idx>> = if f.ranges.is_empty() {
Box::new((0..num_entries()).map(|i| Idx(i as u32)))
} else {
Box::new(
f.ranges
.iter()
.flat_map(|r| block::entry_range(r).map(|i| Idx(i as u32))),
)
};
let pool: Vec<Idx> = base.filter(|&idx| entry_in_bounds(idx, f)).collect();
if pool.is_empty() {
no_matches(query);
}
let names: Vec<&str> = pool.iter().map(|&idx| entry_name(idx)).collect();
let results = search_in(query, &pool, &names, &frecency, limit, Some(typos), sort);
if results.is_empty() {
no_matches(query);
}
print_results(&mut io::stdout().lock(), &results, fmt);
}
fn validate_str_list<'a>(
known: &[&'a str],
input: &[String],
vec: &mut Vec<&'a str>,
label: &str,
) -> Result<(), String> {
for s in input {
match known.iter().find(|k| *k == s) {
Some(&k) => {
if !vec.contains(&k) {
vec.push(k);
}
}
None => return Err(format!("unknown {label} '{s}'")),
}
}
Ok(())
}
fn resolve_filter(args: &Args) -> Result<Filter, String> {
let mut f = Filter {
ranges: vec![],
sources: vec![],
icon_sets: vec![],
categories: vec![],
};
if let Some(ref name) = args.block {
f.ranges.push(
block::by_name(name)
.map(|b| b.range.clone())
.ok_or_else(|| format!("unknown block '{}'", name))?,
);
}
for pat in &args.category {
f.categories.extend(category::resolve(pat)?);
}
validate_str_list(list_sources(), &args.source, &mut f.sources, "source")?;
validate_str_list(
list_icon_sets(),
&args.icon_set,
&mut f.icon_sets,
"icon set",
)?;
Ok(f)
}
fn main() {
let args = Args::parse();
let fmt = exit_ok(args.format.parse::<glyf::Format>());
let sort = match args.sort.as_str() {
"relevance" | "score" => Sort::Relevance,
"name" => Sort::Name,
"codepoint" => Sort::Codepoint,
_ => {
eprintln!(
"error: unknown sort '{}'; use relevance, name, or codepoint",
args.sort
);
std::process::exit(1);
}
};
if args.list_blocks {
let blks = block::all();
let pad = blks.iter().map(|b| b.name.len()).max().unwrap_or(0);
for b in blks {
println!(
"{name:<pad$} U+{start:04X}..U+{end:04X}",
name = b.name,
pad = pad,
start = b.range.start,
end = b.range.end - 1,
);
}
return;
}
if args.list_categories {
for c in category::list() {
println!("{}\t{}", c.code, c.desc);
}
return;
}
if args.list_sources {
for s in list_sources() {
println!("{}", s);
}
return;
}
if args.list_icon_sets {
for s in list_icon_sets() {
let desc = icon_set_description(s);
println!("{s:12} {desc}");
}
return;
}
let filter = exit_ok(resolve_filter(&args));
if let Some(cp_str) = args.codepoint {
let entry = lookup_str(&cp_str);
match entry {
Some(idx) if entry_in_bounds(idx, &filter) => {
drop(print_entry(&mut io::stdout(), idx, &fmt))
}
_ => {
eprintln!("not found: {cp_str}");
std::process::exit(1);
}
}
} else if let Some(query) = args.search {
do_search(&query, args.limit, args.typos, sort, &fmt, &filter);
} else if let Some(cp_str) = args.pick {
let Some(cp) = parse_cp_str(&cp_str) else {
eprintln!("bad codepoint: {cp_str}");
std::process::exit(1);
};
let mut frecency = Frecency::load();
frecency.record(cp);
if let Err(e) = frecency.flush() {
eprintln!("error: failed to save frecency: {e}");
std::process::exit(1);
}
} else if args.list {
let mut sorted: Vec<Idx> = (0..num_entries())
.map(|i| Idx(i as u32))
.filter(|&idx| entry_in_bounds(idx, &filter))
.collect();
match sort {
Sort::Name => sorted.sort_unstable_by(|&a, &b| entry_name(a).cmp(entry_name(b))),
Sort::Codepoint => sorted.sort_unstable_by(|&a, &b| codepoint(a).cmp(&codepoint(b))),
Sort::Relevance => {}
}
let stdout = io::stdout();
let mut out = stdout.lock();
if matches!(fmt, glyf::Format::Pretty) {
print_entries_pretty(&mut out, &sorted);
} else {
for &idx in &sorted {
let _ = print_entry(&mut out, idx, &fmt);
}
}
} else if let Some(q) = args.query {
let entry = lookup_str(&q);
if let Some(idx) = entry
&& entry_in_bounds(idx, &filter)
{
drop(print_entry(&mut io::stdout(), idx, &fmt));
} else {
do_search(&q, args.limit, args.typos, sort, &fmt, &filter);
}
} else {
print_help();
}
}