#![doc = include_str!("../examples/fzf.rs")]
#![doc = include_str!("../examples/find.rs")]
#![deny(missing_docs)]
#![warn(rustdoc::unescaped_backticks)]
mod component;
pub mod error;
pub mod event;
mod incremental;
mod injector;
mod lazy;
mod match_list;
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::{
cursor::MoveTo,
event::{DisableBracketedPaste, EnableBracketedPaste, KeyEvent},
execute,
terminal::{
disable_raw_mode, enable_raw_mode, size, BeginSynchronizedUpdate, EndSynchronizedUpdate,
EnterAlternateScreen, LeaveAlternateScreen,
},
ExecutableCommand, QueueableCommand,
};
use nucleo::{
self as nc,
pattern::{CaseMatching, Normalization},
Nucleo,
};
use crate::{
component::{Component, Status},
error::PickError,
event::{keybind_default, Event, EventSource, RecvError, StdinReader},
lazy::{LazyMatchList, LazyPrompt},
match_list::{MatchList, MatchListConfig},
prompt::{Prompt, PromptConfig},
};
pub use crate::injector::Injector;
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)
}
}
pub struct PickerOptions {
config: nc::Config,
prompt: String,
threads: Option<NonZero<usize>>,
interval: Duration,
match_list_config: MatchListConfig,
prompt_config: PromptConfig,
}
impl Default for PickerOptions {
fn default() -> Self {
Self {
config: nc::Config::DEFAULT,
prompt: String::new(),
threads: None,
interval: Duration::from_millis(15),
match_list_config: MatchListConfig::default(),
prompt_config: PromptConfig::default(),
}
}
}
impl PickerOptions {
#[must_use]
#[inline]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn picker<T: Send + Sync + 'static, R: Render<T>>(self, render: R) -> Picker<T, R> {
let engine = 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,
);
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.prompt);
prompt.set_prompt(self.prompt);
Picker {
match_list,
prompt,
interval: self.interval,
reversed,
}
}
#[must_use]
#[inline]
pub fn reversed(mut self, reversed: bool) -> Self {
self.match_list_config.reversed = reversed;
self
}
#[must_use]
#[inline]
pub fn frame_interval(mut self, interval: Duration) -> Self {
self.interval = interval;
self
}
#[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.match_list_config.highlight = highlight;
self
}
#[must_use]
#[inline]
pub fn highlight_padding(mut self, size: u16) -> Self {
self.match_list_config.highlight_padding = size;
self
}
#[must_use]
#[inline]
pub fn scroll_padding(mut self, size: u16) -> Self {
self.match_list_config.scroll_padding = size;
self
}
#[must_use]
#[inline]
pub fn prompt_padding(mut self, size: u16) -> Self {
self.prompt_config.padding = size;
self
}
#[must_use]
#[inline]
pub fn case_matching(mut self, case_matching: CaseMatching) -> Self {
self.match_list_config.case_matching = case_matching;
self
}
#[must_use]
#[inline]
pub fn normalization(mut self, normalization: Normalization) -> Self {
self.match_list_config.normalization = normalization;
self
}
#[must_use]
#[inline]
pub fn prompt<Q: Into<String>>(mut self, prompt: Q) -> Self {
self.prompt = prompt.into();
self
}
#[must_use]
#[deprecated(since = "0.7.0", note = "method has been renamed to `prompt`")]
pub fn query<Q: Into<String>>(mut self, query: Q) -> Self {
self.prompt = query.into();
self
}
#[must_use]
#[deprecated(
since = "0.6.2",
note = "method has been renamed to `highlight_padding`"
)]
pub fn right_highlight_padding(mut self, size: u16) -> Self {
self.match_list_config.highlight_padding = size;
self
}
}
#[doc = include_str!("../examples/custom_io.rs")]
pub struct Picker<T: Send + Sync + 'static, R> {
match_list: MatchList<T, R>,
prompt: Prompt,
interval: Duration,
reversed: bool,
}
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)
}
#[inline]
pub fn update_prompt<Q: Into<String>>(&mut self, prompt: Q) {
self.prompt.set_prompt(prompt);
}
#[inline]
#[deprecated(since = "0.7.0", note = "method has been renamed to `update_prompt`")]
pub fn update_query<Q: Into<String>>(&mut self, query: Q) {
self.prompt.set_prompt(query);
}
#[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();
}
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()
}
#[inline]
pub fn render<'a>(&self, item: &'a T) -> <R as Render<T>>::Str<'a> {
self.match_list.render(item)
}
#[inline]
pub fn pick(&mut self) -> Result<Option<&T>, PickError> {
self.pick_with_keybind(keybind_default)
}
#[inline]
pub fn pick_with_keybind<F: Fn(KeyEvent) -> Option<Event<T, R>>>(
&mut self,
keybind: F,
) -> Result<Option<&T>, PickError> {
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]
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>(
&mut self,
writer: &mut W,
redraw_prompt: bool,
redraw_match_list: bool,
) -> io::Result<()> {
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)?;
}
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(())
}
pub fn pick_with_io<A, E, W>(
&mut self,
event_source: E,
writer: &mut W,
) -> Result<Option<&T>, PickError<A>>
where
E: EventSource<T, R, AbortErr = A>,
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);
}));
Self::init_screen(writer)?;
let mut frame_start = Instant::now();
self.match_list.update(5);
self.render_frame(writer, true, true)?;
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);
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(None);
}
Event::QuitPromptEmpty => {
if lazy_prompt.is_empty() {
break 'selection Ok(None);
}
}
Event::Select => {
if !lazy_match_list.is_empty() {
let n = lazy_match_list.selection();
let item = self.match_list.get_item(n).unwrap();
break 'selection Ok(Some(item.data));
}
}
Event::Restart(sender) => {
if sender.send(lazy_match_list.restart()).is_err() {
break 'selection Err(PickError::Disconnected);
}
redraw_match_list = true;
}
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)?;
redraw_prompt = false;
redraw_match_list = false;
};
Self::cleanup_screen(writer)?;
selection
}
}