Skip to content

Commit

Permalink
feat: add font list filter (#71)
Browse files Browse the repository at this point in the history
This PR adds a filter option -f/--filter in CLI, TUI mode also gets a input box to update the filter word.

The filter process is case-insenstive, and only cares about family name, not full name.

Fixes #64.
  • Loading branch information
7sDream authored May 10, 2024
1 parent f540360 commit be52327
Show file tree
Hide file tree
Showing 9 changed files with 491 additions and 161 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

## Unreleased

- Add `-f` option to filter font list by family name
- Add a search box in TUI mode to change filter word
- Remove `#[deny(warnings)]` in source code, add it in CI
- Fix build for comming Rust 1.79 new lints
- Fix build for upcomming Rust 1.79 new lints
- Update deps

## 0.4.2
Expand Down
60 changes: 57 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ clap = { version = "4.4", features = ["derive", "unicode", "wrap_help"] }
fontdb = "0.16"

# Font parser
ttf-parser = "0.20"
ttf-parser = "0.21"

# Filter fonts
range-set-blaze = "0.1.16"

# Font rasterizer
# see https://gist.github.com/7sDream/0bb194be42b8cb1f1926ca12151c8d76 for alternatives.
Expand All @@ -40,6 +43,9 @@ ratatui = "0.26"
# Terminal events
crossterm = "0.27"

# Input widget for filter in TUI
tui-input = "0.8.0"

# Home-made singel thread HTTP server for preview fonts in browser.
# Alternative: output a html file into temp dir and open it
httparse = "1.8"
Expand Down
15 changes: 13 additions & 2 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,18 @@

use std::path::PathBuf;

use clap::Parser;
use clap::{
builder::{NonEmptyStringValueParser, TypedValueParser},
Parser,
};

use super::one_char::OneChar;

#[derive(Debug, clap::Parser)]
fn no_newline_string_parser() -> impl TypedValueParser {
NonEmptyStringValueParser::new().map(|s| s.replace(['\r', '\n'], ""))
}

#[derive(Debug, Parser)]
#[command(author, version, about, arg_required_else_help(true))]
pub struct Args {
/// Verbose mode, -v show all font styles, -vv adds font file and face index
Expand All @@ -47,6 +54,10 @@ pub struct Args {
#[arg(short = 'I', long = "include", name = "PATH", action = clap::ArgAction::Append)]
pub custom_font_paths: Vec<PathBuf>,

/// Only show fonts whose family name contains the filter string
#[arg(short = 'f', long = "filter", name = "FILTER", value_parser = no_newline_string_parser())]
pub filter: Option<String>,

/// The character
#[arg(name = "CHAR")]
pub char: OneChar,
Expand Down
126 changes: 126 additions & 0 deletions src/family.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

use std::collections::HashMap;

use range_set_blaze::RangeSetBlaze;

use super::loader::FaceInfo;

pub struct Family<'a> {
Expand Down Expand Up @@ -65,3 +67,127 @@ pub fn group_by_family_sort_by_name(faces: &[FaceInfo]) -> Vec<Family<'_>> {

families
}

pub struct FilteredFamilies<'a> {
data: Vec<Family<'a>>,
names: Vec<String>,
keyword: String,
filtered: RangeSetBlaze<usize>,
}

#[derive(Clone)]
pub struct FilteredFamiliesIter<'f, 'a> {
data: &'f [Family<'a>],
range: range_set_blaze::Iter<usize, range_set_blaze::RangesIter<'f, usize>>,
}

impl<'f, 'a: 'f> FilteredFamiliesIter<'f, 'a> {
pub fn with_index(self) -> FilteredFamiliesWithIndexIter<'f, 'a> {
FilteredFamiliesWithIndexIter(self)
}
}

impl<'f, 'a: 'f> Iterator for FilteredFamiliesIter<'f, 'a> {
type Item = &'f Family<'a>;

fn next(&mut self) -> Option<Self::Item> {
self.range.next().map(|i| &self.data[i])
}
}

pub struct FilteredFamiliesWithIndexIter<'f, 'a>(FilteredFamiliesIter<'f, 'a>);

impl<'f, 'a: 'f> Iterator for FilteredFamiliesWithIndexIter<'f, 'a> {
type Item = (usize, &'f Family<'a>);

fn next(&mut self) -> Option<Self::Item> {
self.0.range.next().map(|i| (i, &self.0.data[i]))
}
}

impl<'a> FilteredFamilies<'a> {
pub fn new(families: Vec<Family<'a>>, keyword: String) -> Self {
let names = families.iter().map(|f| f.name.to_lowercase()).collect();
let mut ret = Self {
data: families,
names,
keyword: keyword.to_lowercase(),
filtered: RangeSetBlaze::new(),
};
ret.filter(true);
ret
}

pub fn is_empty(&self) -> bool {
self.data.is_empty()
}

fn full_indices(&self) -> RangeSetBlaze<usize> {
if !self.data.is_empty() {
RangeSetBlaze::from_iter(&[0..=self.data.len() - 1])
} else {
RangeSetBlaze::new()
}
}

pub fn matched_indices(&self) -> &RangeSetBlaze<usize> {
&self.filtered
}

pub fn data(&self) -> &[Family<'a>] {
&self.data
}

pub fn matched(&self) -> FilteredFamiliesIter<'_, 'a> {
FilteredFamiliesIter {
data: &self.data,
range: self.matched_indices().iter(),
}
}

fn unmatched_indices(&self) -> RangeSetBlaze<usize> {
&self.full_indices() - self.matched_indices()
}

fn retain(rs: &mut RangeSetBlaze<usize>, data: &[String], keyword: &str) {
if !keyword.is_empty() {
rs.retain(|i| data[*i].contains(keyword))
}
}

fn filter(&mut self, reset: bool) {
if reset {
self.filtered = self.full_indices()
}

Self::retain(&mut self.filtered, &self.names, &self.keyword)
}

pub fn keyword(&self) -> &str {
&self.keyword
}

pub fn change_keyword(&mut self, keyword: &str) {
let keyword = keyword.to_lowercase();

if self.keyword == keyword {
return;
}

if self.keyword.starts_with(&keyword) || self.keyword.ends_with(&keyword) {
// more loose, only search unmatched and append to matches
let mut unmatched = self.unmatched_indices();
self.keyword = keyword;
Self::retain(&mut unmatched, &self.names, &self.keyword);
self.filtered.append(&mut unmatched)
} else if keyword.starts_with(&keyword) || keyword.ends_with(&keyword) {
// more strict, search currents matches again
self.keyword = keyword;
self.filter(false);
} else {
// research all
self.keyword = keyword;
self.filter(true)
}
}
}
22 changes: 13 additions & 9 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use std::{
};

use args::Args;
use family::Family;
use family::{Family, FilteredFamilies};
use preview::{browser::ServerBuilder as PreviewServerBuilder, terminal::ui::UI};

fn init(arg: &Args) {
Expand All @@ -56,7 +56,6 @@ fn main() {

let font_set = loader::query(argument.char.0);
let families = family::group_by_family_sort_by_name(&font_set);

if families.is_empty() {
eprintln!(
"No font support this character {}.",
Expand All @@ -65,14 +64,16 @@ fn main() {
return;
}

let filtered = FilteredFamilies::new(families, argument.filter.unwrap_or_default());

if argument.tui {
let ui = UI::new(families).expect("family length checked before, must not empty");
let ui = UI::new(filtered).expect("family length checked before, must not empty");
if let Err(err) = ui.show() {
eprintln!("{:?}", err);
};
} else {
let builder = if argument.preview {
Some(PreviewServerBuilder::from_iter(families.iter()))
Some(PreviewServerBuilder::from_iter(filtered.matched()))
} else {
None
};
Expand All @@ -81,7 +82,7 @@ fn main() {
"Font(s) support the character {}:",
argument.char.description()
);
show_font_list(families, argument.verbose);
show_font_list(filtered.matched(), argument.verbose);

if let Some(builder) = builder {
builder
Expand All @@ -105,21 +106,24 @@ fn show_preview_addr_and_wait(addr: SocketAddr) {
.expect("read from stdout should not fail");
}

fn show_font_list(families: Vec<Family<'_>>, verbose: u8) {
fn show_font_list<'f, 'a: 'f, F>(families: F, verbose: u8)
where
F: Iterator<Item = &'f Family<'a>> + Clone,
{
let max_len = if verbose > 0 {
0
} else {
families
.iter()
.clone()
.map(|f| f.default_name_width)
.max()
.unwrap_or_default()
};

families.into_iter().for_each(|family| {
families.for_each(|family| {
if verbose > 0 {
println!("{}", family.name);
for face in family.faces {
for face in family.faces.iter() {
print!("\t{}", face.name);
if verbose > 1 {
print!("\t{}:{}", face.path.to_string_lossy(), face.index)
Expand Down
2 changes: 1 addition & 1 deletion src/preview/terminal/ui/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub static MONO_RENDER: Lazy<MonoRender> = Lazy::new(MonoRender::default);

#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct CacheKey {
pub index: usize,
pub index: (usize, usize),
pub rt: RenderType,
pub width: u32,
pub height: u32,
Expand Down
Loading

0 comments on commit be52327

Please sign in to comment.