#![doc = include_str!("../examples/fzf.rs")]
#![doc = include_str!("../examples/find.rs")]
#![deny(missing_docs)]
#![warn(rustdoc::unescaped_backticks)]
mod bind;
mod component;
mod injector;
pub mod render;
mod term;
use std::{
borrow::Cow,
io::{self, IsTerminal},
iter::Extend,
num::NonZero,
sync::Arc,
thread::{available_parallelism, sleep},
time::{Duration, Instant},
};
use crossterm::{
event::{DisableBracketedPaste, EnableBracketedPaste},
execute,
terminal::{
disable_raw_mode, enable_raw_mode, size, EnterAlternateScreen, LeaveAlternateScreen,
},
};
use nucleo::{
self as nc,
pattern::{CaseMatching, Normalization},
Nucleo,
};
pub use nucleo;
pub use crate::injector::Injector;
use crate::{
component::normalize_query_string,
term::{Compositor, CompositorBuffer, EventSummary, PickerConfig},
};
pub trait Render<T> {
type Str<'a>: AsRef<str>
where
T: 'a;
fn render<'a>(&self, item: &'a T) -> Self::Str<'a>;
}
impl<T, R: for<'a> Fn(&'a T) -> Cow<'a, str>> Render<T> for R {
type Str<'a>
= Cow<'a, str>
where
T: 'a;
fn render<'a>(&self, item: &'a T) -> Self::Str<'a> {
self(item)
}
}
pub struct PickerOptions {
config: nc::Config,
query: String,
threads: Option<NonZero<usize>>,
picker_config: PickerConfig,
}
impl Default for PickerOptions {
fn default() -> Self {
Self {
config: nc::Config::DEFAULT,
query: String::new(),
threads: None,
picker_config: PickerConfig::default(),
}
}
}
impl PickerOptions {
#[must_use]
#[inline]
pub fn new() -> Self {
Self::default()
}
#[must_use]
#[inline]
pub fn threads(mut self, threads: Option<NonZero<usize>>) -> Self {
self.threads = threads;
self
}
#[must_use]
#[inline]
pub fn config(mut self, config: nc::Config) -> Self {
self.config = config;
self
}
#[must_use]
#[inline]
pub fn highlight(mut self, highlight: bool) -> Self {
self.picker_config.highlight = highlight;
self
}
#[must_use]
#[inline]
pub fn right_highlight_padding(mut self, size: u16) -> Self {
self.picker_config.right_highlight_padding = size;
self
}
#[must_use]
#[inline]
pub fn scroll_padding(mut self, size: u16) -> Self {
self.picker_config.scroll_padding = size;
self
}
#[must_use]
#[inline]
pub fn case_matching(mut self, case_matching: CaseMatching) -> Self {
self.picker_config.case_matching = case_matching;
self
}
#[must_use]
#[inline]
pub fn normalization(mut self, normalization: Normalization) -> Self {
self.picker_config.normalization = normalization;
self
}
#[must_use]
#[inline]
pub fn query<Q: Into<String>>(mut self, query: Q) -> Self {
self.query = query.into();
normalize_query_string(&mut self.query);
self
}
#[must_use]
pub fn picker<T: Send + Sync + 'static, R>(self, render: R) -> Picker<T, R> {
let matcher = Nucleo::new(
self.config.clone(),
Arc::new(|| {}),
self.threads
.or_else(|| {
available_parallelism()
.ok()
.and_then(|it| it.get().checked_sub(2).and_then(NonZero::new))
})
.map(NonZero::get),
1,
);
Picker {
matcher,
render: render.into(),
picker_config: self.picker_config,
config: self.config,
query: self.query,
}
}
}
pub struct Picker<T: Send + Sync + 'static, R> {
matcher: Nucleo<T>,
render: Arc<R>,
picker_config: PickerConfig,
config: nc::Config,
query: String,
}
impl<T: Send + Sync + 'static, R: Render<T>> Extend<T> for Picker<T, R> {
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
let injector = self.injector();
for it in iter {
injector.push(it);
}
}
}
impl<T: Send + Sync + 'static, R: Render<T>> Picker<T, R> {
#[must_use]
pub fn new(render: R) -> Self {
PickerOptions::default().picker(render)
}
const fn default_frame_interval() -> Duration {
Duration::from_millis(16)
}
#[inline]
pub fn update_query<Q: Into<String>>(&mut self, query: Q) {
self.query = query.into();
normalize_query_string(&mut self.query);
}
#[inline]
pub fn update_config(&mut self, config: nc::Config) {
self.matcher.update_config(config);
}
pub fn restart(&mut self) {
self.matcher.restart(true);
}
pub fn reset_renderer(&mut self, render: R) {
self.restart();
self.render = render.into();
}
#[must_use]
pub fn injector(&self) -> Injector<T, R> {
Injector::new(self.matcher.injector(), self.render.clone())
}
#[inline]
pub fn render<'a>(&self, item: &'a T) -> <R as Render<T>>::Str<'a> {
self.render.render(item)
}
pub fn pick(&mut self) -> Result<Option<&T>, io::Error> {
let stderr = io::stderr().lock();
if stderr.is_terminal() {
self.pick_inner(Self::default_frame_interval(), stderr)
} else {
Err(io::Error::new(io::ErrorKind::Other, "is not interactive"))
}
}
fn pick_inner(
&mut self,
interval: Duration,
mut stderr: io::StderrLock<'_>,
) -> Result<Option<&T>, io::Error> {
let mut term = Compositor::new(size()?, &self.picker_config);
term.set_prompt(&self.query);
let mut buffer = CompositorBuffer::new();
let mut matcher = nucleo::Matcher::new(self.config.clone());
enable_raw_mode()?;
execute!(stderr, EnterAlternateScreen, EnableBracketedPaste)?;
let selection = loop {
let deadline = Instant::now() + interval;
match term.handle() {
Ok(summary) => match summary {
EventSummary::Continue => {}
EventSummary::UpdatePrompt(append) => {
self.matcher.pattern.reparse(
0,
&term.prompt_contents(),
self.picker_config.case_matching,
self.picker_config.normalization,
append,
);
}
EventSummary::Select => {
break Ok(term
.selection()
.try_into()
.ok()
.and_then(|idx| self.matcher.snapshot().get_matched_item(idx))
.map(|it| it.data));
}
EventSummary::Quit => {
break Ok(None);
}
},
Err(err) => break Err(err),
};
let status = self.matcher.tick(10);
term.update(status.changed, self.matcher.snapshot());
term.draw(
&mut stderr,
&mut matcher,
self.render.as_ref(),
self.matcher.snapshot(),
&mut buffer,
)?;
sleep(deadline - Instant::now());
};
disable_raw_mode()?;
execute!(stderr, DisableBracketedPaste, LeaveAlternateScreen)?;
selection
}
}