#![doc = include_str!("../examples/fzf.rs")]
#![doc = include_str!("../examples/find.rs")]
#![deny(missing_docs)]
#![warn(rustdoc::unescaped_backticks)]
#![cfg_attr(docsrs, feature(doc_cfg))]
mod component;
pub mod error;
pub mod event;
mod incremental;
mod injector;
mod lazy;
mod match_list;
mod observer;
mod prompt;
pub mod render;
mod util;
use std::{
borrow::Cow,
io::{self, BufWriter, IsTerminal, Write},
iter::Extend,
num::NonZero,
panic::{set_hook, take_hook},
sync::Arc,
thread::available_parallelism,
time::{Duration, Instant},
};
use crossterm::{
ExecutableCommand, QueueableCommand,
cursor::MoveTo,
event::{DisableBracketedPaste, EnableBracketedPaste, KeyEvent},
execute,
terminal::{
BeginSynchronizedUpdate, EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen,
disable_raw_mode, enable_raw_mode, size,
},
};
use nucleo::{
self as nc, Nucleo,
pattern::{CaseMatching as NucleoCaseMatching, Normalization as NucleoNormalization},
};
use observer::{Notifier, Observer};
use crate::{
component::Status,
error::PickError,
event::{Event, EventSource, RecvError, StdinReader, keybind_default},
lazy::{LazyMatchList, LazyPrompt},
match_list::{MatchList, MatchListConfig, Queued, SelectedIndices},
prompt::{Prompt, PromptConfig},
};
pub use crate::injector::Injector;
pub use crate::match_list::Selection;
pub use nucleo;
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)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum CaseMatching {
Respect,
Ignore,
#[default]
Smart,
}
impl CaseMatching {
pub(crate) const fn convert(self) -> NucleoCaseMatching {
match self {
Self::Respect => NucleoCaseMatching::Respect,
Self::Ignore => NucleoCaseMatching::Ignore,
Self::Smart => NucleoCaseMatching::Smart,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Normalization {
Never,
#[default]
Smart,
}
impl Normalization {
pub(crate) const fn convert(self) -> NucleoNormalization {
match self {
Self::Never => NucleoNormalization::Never,
Self::Smart => NucleoNormalization::Smart,
}
}
}
pub struct PickerOptions {
config: nc::Config,
query: String,
threads: Option<NonZero<usize>>,
max_selection_count: Option<NonZero<u32>>,
interval: Duration,
match_list_config: MatchListConfig,
prompt_config: PromptConfig,
sort_results: bool,
reverse_items: bool,
}
impl Default for PickerOptions {
fn default() -> Self {
Self::new()
}
}
impl PickerOptions {
#[must_use]
#[inline]
pub const fn new() -> Self {
Self {
config: nc::Config::DEFAULT,
query: String::new(),
threads: None,
max_selection_count: None,
interval: Duration::from_millis(15),
match_list_config: MatchListConfig::new(),
prompt_config: PromptConfig::new(),
sort_results: true,
reverse_items: false,
}
}
#[must_use]
pub fn picker<T: Send + Sync + 'static, R>(self, render: R) -> Picker<T, R> {
let engine = Nucleo::with_match_list_config(
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,
nc::MatchListConfig {
sort_results: self.sort_results,
reverse_items: self.reverse_items,
},
);
let reversed = self.match_list_config.reversed;
let mut match_list =
MatchList::new(self.match_list_config, self.config, engine, render.into());
let mut prompt = Prompt::new(self.prompt_config);
match_list.reparse(&self.query);
prompt.set_query(self.query);
Picker {
match_list,
prompt,
interval: self.interval,
max_selection_count: self.max_selection_count,
reversed,
restart_notifier: None,
}
}
#[must_use]
#[inline]
pub const fn reversed(mut self, reversed: bool) -> Self {
self.match_list_config.reversed = reversed;
self
}
#[must_use]
#[inline]
pub const fn reverse_items(mut self, reversed: bool) -> Self {
self.reverse_items = reversed;
self
}
#[must_use]
#[inline]
pub const fn sort_results(mut self, sort: bool) -> Self {
self.sort_results = sort;
self
}
#[must_use]
#[inline]
pub const fn max_selection_count(mut self, maximum: Option<NonZero<u32>>) -> Self {
self.max_selection_count = maximum;
self
}
#[must_use]
#[inline]
pub const fn frame_interval(mut self, interval: Duration) -> Self {
self.interval = interval;
self
}
#[must_use]
#[inline]
pub const fn threads(mut self, threads: Option<NonZero<usize>>) -> Self {
self.threads = threads;
self
}
#[must_use]
#[inline]
#[deprecated(
since = "0.10.0",
note = "Use native methods `prefer_prefix` and `match_paths`. The `normalize` and `ignore_case` settings are never used; use `normalization` and `case_matching` instead."
)]
pub fn config(mut self, config: nc::Config) -> Self {
self.config = config;
self
}
#[must_use]
#[inline]
pub const fn normalization(mut self, normalization: Normalization) -> Self {
self.match_list_config.normalization = normalization.convert();
self
}
#[must_use]
#[inline]
pub const fn case_matching(mut self, case_matching: CaseMatching) -> Self {
self.match_list_config.case_matching = case_matching.convert();
self
}
#[must_use]
#[inline]
pub const fn match_paths(mut self) -> Self {
self.config = self.config.match_paths();
self
}
#[must_use]
#[inline]
pub const fn prefer_prefix(mut self, prefer_prefix: bool) -> Self {
self.config.prefer_prefix = prefer_prefix;
self
}
#[must_use]
#[inline]
pub const fn highlight(mut self, highlight: bool) -> Self {
self.match_list_config.highlight = highlight;
self
}
#[must_use]
#[inline]
pub const fn highlight_padding(mut self, size: u16) -> Self {
self.match_list_config.highlight_padding = size;
self
}
#[must_use]
#[inline]
pub const fn scroll_padding(mut self, size: u16) -> Self {
self.match_list_config.scroll_padding = size;
self
}
#[must_use]
#[inline]
pub const fn prompt_padding(mut self, size: u16) -> Self {
self.prompt_config.padding = size;
self
}
#[must_use]
#[inline]
pub fn query<Q: Into<String>>(mut self, query: Q) -> Self {
self.query = query.into();
self
}
}
#[doc = include_str!("../examples/custom_io.rs")]
pub struct Picker<T: Send + Sync + 'static, R> {
match_list: MatchList<T, R>,
max_selection_count: Option<NonZero<u32>>,
prompt: Prompt,
interval: Duration,
reversed: bool,
restart_notifier: Option<Notifier<Injector<T, R>>>,
}
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> Picker<T, R> {
#[must_use]
pub fn new(render: R) -> Self
where
R: Render<T>,
{
PickerOptions::default().picker(render)
}
#[inline]
pub fn update_query<Q: Into<String>>(&mut self, query: Q) {
self.prompt.set_query(query);
self.match_list.reparse(self.prompt.contents());
}
#[must_use]
pub fn query(&self) -> &str {
self.prompt.contents()
}
#[must_use]
pub fn injector_observer(&mut self, with_injector: bool) -> Observer<Injector<T, R>> {
let (notifier, observer) = if with_injector {
observer::occupied_channel(self.injector())
} else {
observer::channel()
};
self.restart_notifier = Some(notifier);
observer
}
#[inline]
pub fn update_config(&mut self, config: nc::Config) {
self.match_list.update_nucleo_config(config);
}
pub fn restart(&mut self) {
self.match_list.restart();
self.update_query("");
}
pub fn reset_renderer(&mut self, render: R) {
self.match_list.reset_renderer(render);
}
#[must_use]
pub fn injector(&self) -> Injector<T, R> {
self.match_list.injector()
}
pub fn extend_exact<I>(&self, iter: I)
where
R: Render<T>,
I: IntoIterator<Item = T>,
<I as IntoIterator>::IntoIter: ExactSizeIterator,
{
self.injector().extend_exact(iter);
}
#[inline]
pub fn render<'a>(&self, item: &'a T) -> <R as Render<T>>::Str<'a>
where
R: Render<T>,
{
self.match_list.render(item)
}
#[inline]
pub fn pick(&mut self) -> Result<Option<&T>, PickError>
where
R: Render<T>,
{
self.pick_with_keybind(keybind_default)
}
#[inline]
pub fn pick_multi(&mut self) -> Result<Selection<'_, T>, PickError>
where
R: Render<T>,
{
self.pick_multi_with_keybind(keybind_default)
}
#[inline]
pub fn pick_with_keybind<F: FnMut(KeyEvent) -> Option<Event>>(
&mut self,
keybind: F,
) -> Result<Option<&T>, PickError>
where
R: Render<T>,
{
let stderr = io::stderr().lock();
if stderr.is_terminal() {
self.pick_with_io(StdinReader::new(keybind), &mut BufWriter::new(stderr))
} else {
Err(PickError::NotInteractive)
}
}
#[inline]
pub fn pick_multi_with_keybind<F: FnMut(KeyEvent) -> Option<Event>>(
&mut self,
keybind: F,
) -> Result<Selection<'_, T>, PickError>
where
R: Render<T>,
{
let stderr = io::stderr().lock();
if stderr.is_terminal() {
self.pick_multi_with_io(StdinReader::new(keybind), &mut BufWriter::new(stderr))
} else {
Err(PickError::NotInteractive)
}
}
pub fn pick_with_io<E, W>(
&mut self,
event_source: E,
writer: &mut W,
) -> Result<Option<&T>, PickError<<E as EventSource>::AbortErr>>
where
R: Render<T>,
E: EventSource,
W: io::Write,
{
self.pick_impl::<_, _, ()>(event_source, writer)
}
pub fn pick_multi_with_io<E, W>(
&mut self,
event_source: E,
writer: &mut W,
) -> Result<Selection<'_, T>, PickError<<E as EventSource>::AbortErr>>
where
R: Render<T>,
E: EventSource,
W: io::Write,
{
self.pick_impl::<_, _, SelectedIndices>(event_source, writer)
}
#[inline]
fn init_screen<W: Write>(writer: &mut W) -> io::Result<()> {
enable_raw_mode()?;
execute!(writer, EnterAlternateScreen, EnableBracketedPaste)?;
Ok(())
}
#[inline]
fn cleanup_screen<W: Write>(writer: &mut W) -> io::Result<()> {
disable_raw_mode()?;
execute!(writer, DisableBracketedPaste, LeaveAlternateScreen)?;
Ok(())
}
#[inline]
fn render_frame<W: Write, Q: Queued>(
&mut self,
writer: &mut W,
redraw_prompt: bool,
redraw_match_list: bool,
queued_items: &Q,
) -> io::Result<()>
where
R: Render<T>,
{
let (width, height) = size()?;
let (prompt_row, match_list_row) = if self.reversed {
(0, 1)
} else {
(height - 1, 0)
};
if width >= 1 && (redraw_prompt || redraw_match_list) {
writer.execute(BeginSynchronizedUpdate)?;
if redraw_match_list && height >= 2 {
writer.queue(MoveTo(0, match_list_row))?;
self.match_list
.draw(width, height - 1, writer, |idx| queued_items.is_queued(idx))?;
}
if redraw_prompt && height >= 1 {
writer.queue(MoveTo(0, prompt_row))?;
self.prompt.draw(width, 1, writer)?;
}
writer.queue(MoveTo(self.prompt.screen_offset() + 2, prompt_row))?;
writer.flush()?;
writer.execute(EndSynchronizedUpdate)?;
};
Ok(())
}
fn pick_impl<E, W, Q: Queued>(
&mut self,
mut event_source: E,
writer: &mut W,
) -> Result<Q::Output<'_, T>, PickError<<E as EventSource>::AbortErr>>
where
R: Render<T>,
E: EventSource,
W: io::Write,
{
let original_hook = take_hook();
set_hook(Box::new(move |panic_info| {
let _ = Self::cleanup_screen(&mut io::stderr());
original_hook(panic_info);
}));
let mut queued_items = Q::init(self.max_selection_count);
Self::init_screen(writer)?;
let mut frame_start = Instant::now();
self.match_list.update(5);
self.render_frame(writer, true, true, &queued_items)?;
let mut redraw_prompt = false;
let mut redraw_match_list = false;
let selection = 'selection: loop {
let mut lazy_match_list = LazyMatchList::new(&mut self.match_list, &mut queued_items);
let mut lazy_prompt = LazyPrompt::new(&mut self.prompt);
'event: loop {
match event_source.recv_timeout(frame_start + self.interval - Instant::now()) {
Ok(event) => match event {
Event::Prompt(prompt_event) => {
lazy_prompt.handle(prompt_event);
}
Event::MatchList(match_list_event) => {
lazy_match_list.handle(match_list_event);
}
Event::Redraw => {
redraw_prompt = true;
redraw_match_list = true;
}
Event::Quit => {
break 'selection Ok(self.match_list.select_none(queued_items));
}
Event::QuitPromptEmpty => {
if lazy_prompt.is_empty() {
break 'selection Ok(self.match_list.select_none(queued_items));
}
}
Event::Select => {
if lazy_match_list.has_queued_items() {
break 'selection Ok(self.match_list.select_queued(queued_items));
}
if let Some(n) = lazy_match_list.selection() {
break 'selection Ok(self.match_list.select_one(queued_items, n));
}
}
Event::Restart => match self.restart_notifier {
Some(ref notifier) => {
if notifier.push(lazy_match_list.restart()).is_err() {
break 'selection Err(PickError::Disconnected);
} else {
redraw_match_list = true;
}
}
None => break 'selection Err(PickError::Disconnected),
},
Event::UserInterrupt => {
break 'selection Err(PickError::UserInterrupted);
}
Event::Abort(err) => {
break 'selection Err(PickError::Aborted(err));
}
},
Err(RecvError::Timeout) => break 'event,
Err(RecvError::Disconnected) => {
break 'selection Err(PickError::Disconnected);
}
Err(RecvError::IO(io_err)) => break 'selection Err(PickError::IO(io_err)),
}
}
frame_start = Instant::now();
let prompt_status = lazy_prompt.finish();
let match_list_status = lazy_match_list.finish();
redraw_prompt |= prompt_status.needs_redraw();
redraw_match_list |= match_list_status.needs_redraw();
if prompt_status.contents_changed {
self.match_list.reparse(self.prompt.contents());
redraw_match_list = true;
}
redraw_match_list |= self
.match_list
.update(2 * self.interval.as_millis() as u64 / 3)
.needs_redraw();
self.render_frame(writer, redraw_prompt, redraw_match_list, &queued_items)?;
redraw_prompt = false;
redraw_match_list = false;
};
Self::cleanup_screen(writer)?;
selection
}
}