mod bind;
pub mod component;
pub mod fill;
use std::{
cmp::min,
io::{self, Stdout, Write},
process::exit,
sync::Arc,
thread::{available_parallelism, sleep},
time::{Duration, Instant},
};
use crossterm::{
cursor::{MoveTo, MoveToColumn, MoveToPreviousLine, MoveUp},
event::{poll, read, DisableBracketedPaste, EnableBracketedPaste},
execute,
style::{
Attribute, Color, Print, PrintStyledContent, ResetColor, SetAttribute, SetForegroundColor,
Stylize,
},
terminal::{
disable_raw_mode, enable_raw_mode, size, Clear, ClearType, EnterAlternateScreen,
LeaveAlternateScreen,
},
tty::IsTty,
QueueableCommand,
};
use nucleo::{Config, Injector, Nucleo, Utf32String};
use crate::{
bind::{convert, Event},
component::{Edit, EditableString},
};
pub use nucleo;
enum EventSummary {
Continue,
UpdatePrompt(bool),
Select,
Quit,
}
#[derive(Debug)]
struct Dimensions {
width: u16,
height: u16,
prompt_left_padding: u16,
prompt_right_padding: u16,
}
impl Dimensions {
pub fn from_screen(width: u16, height: u16) -> Self {
Self {
width,
height,
prompt_left_padding: width / 8,
prompt_right_padding: width / 12,
}
}
pub fn move_to_end_of_line(&self) -> MoveToColumn {
MoveToColumn(self.width - 1)
}
pub fn move_to_results_start(&self) -> MoveTo {
MoveTo(0, self.max_draw_count())
}
pub fn prompt_max_width(&self) -> usize {
self.width
.saturating_sub(self.prompt_left_padding)
.saturating_sub(self.prompt_right_padding)
.saturating_sub(2) as _
}
pub fn max_draw_count(&self) -> u16 {
self.height.saturating_sub(2)
}
pub fn max_draw_length(&self) -> u16 {
self.width.saturating_sub(2)
}
fn prompt_y(&self) -> u16 {
self.height.saturating_sub(1)
}
pub fn move_to_prompt(&self) -> MoveTo {
MoveTo(0, self.prompt_y())
}
pub fn move_to_cursor(&self, view_position: usize) -> MoveTo {
MoveTo((view_position + 2) as _, self.prompt_y())
}
}
#[derive(Debug)]
struct PickerState {
dimensions: Dimensions,
selector_index: Option<u16>,
prompt: EditableString,
draw_count: u16,
item_count: u32,
matched_item_count: u32,
needs_redraw: bool,
}
impl PickerState {
pub fn new(screen: (u16, u16)) -> Self {
let dimensions = Dimensions::from_screen(screen.0, screen.1);
let prompt = EditableString::new(dimensions.prompt_max_width());
Self {
dimensions,
selector_index: None,
prompt,
draw_count: 0,
matched_item_count: 0,
item_count: 0,
needs_redraw: true,
}
}
pub fn incr_selection(&mut self) {
self.needs_redraw = true;
self.selector_index = self.selector_index.map(|i| i.saturating_add(1));
self.clamp_selector_index();
}
pub fn decr_selection(&mut self) {
self.needs_redraw = true;
self.selector_index = self.selector_index.map(|i| i.saturating_sub(1));
self.clamp_selector_index();
}
pub fn update<T: Send + Sync + 'static>(
&mut self,
changed: bool,
snapshot: &nucleo::Snapshot<T>,
) {
if changed {
self.needs_redraw = true;
self.item_count = snapshot.item_count();
self.matched_item_count = snapshot.matched_item_count();
self.draw_count = self.matched_item_count.try_into().unwrap_or(u16::MAX);
self.clamp_draw_count();
self.clamp_selector_index();
}
}
fn clamp_draw_count(&mut self) {
self.draw_count = min(self.draw_count, self.dimensions.max_draw_count())
}
fn clamp_selector_index(&mut self) {
if self.draw_count == 0 {
self.selector_index = None;
} else {
let position = min(self.selector_index.unwrap_or(0), self.draw_count - 1);
self.selector_index = Some(position);
}
}
pub fn edit_prompt(&mut self, st: Edit) {
self.needs_redraw |= self.prompt.edit(st);
}
fn format_display(&self, display: &Utf32String) -> String {
display
.slice(..)
.chars()
.filter(|ch| !ch.is_control())
.take(self.dimensions.max_draw_length() as _)
.map(|ch| match ch {
'\n' => ' ',
s => s,
})
.collect()
}
fn handle(&mut self) -> Result<EventSummary, io::Error> {
let mut update_prompt = false;
let mut append = true;
while poll(Duration::from_millis(5))? {
if let Some(event) = convert(read()?) {
match event {
Event::Abort => exit(1),
Event::MoveToStart => self.edit_prompt(Edit::ToStart),
Event::MoveToEnd => self.edit_prompt(Edit::ToEnd),
Event::Insert(ch) => {
update_prompt = true;
append &= self.prompt.cursor_at_end();
self.edit_prompt(Edit::Insert(ch));
}
Event::Select => return Ok(EventSummary::Select),
Event::MoveUp => self.incr_selection(),
Event::MoveDown => self.decr_selection(),
Event::MoveLeft => self.edit_prompt(Edit::Left),
Event::MoveRight => self.edit_prompt(Edit::Right),
Event::Delete => {
update_prompt = true;
append = false;
self.edit_prompt(Edit::Delete);
}
Event::Quit => return Ok(EventSummary::Quit),
Event::Resize(width, height) => {
self.resize(width, height);
}
Event::Paste(contents) => {
update_prompt = true;
append &= self.prompt.cursor_at_end();
self.edit_prompt(Edit::Paste(contents));
}
}
}
}
Ok(if update_prompt {
EventSummary::UpdatePrompt(append)
} else {
EventSummary::Continue
})
}
pub fn draw<T: Send + Sync + 'static>(
&mut self,
stdout: &mut Stdout,
snapshot: &nucleo::Snapshot<T>,
) -> Result<(), io::Error> {
if self.needs_redraw {
self.needs_redraw = false;
stdout.queue(self.dimensions.move_to_results_start())?;
stdout
.queue(SetAttribute(Attribute::Italic))?
.queue(SetForegroundColor(Color::Green))?
.queue(Print(" "))?
.queue(Print(self.matched_item_count))?
.queue(Print("/"))?
.queue(Print(self.item_count))?
.queue(SetAttribute(Attribute::Reset))?
.queue(ResetColor)?
.queue(Clear(ClearType::UntilNewLine))?;
for (idx, it) in snapshot.matched_items(..self.draw_count as u32).enumerate() {
let render = self.format_display(&it.matcher_columns[0]);
if Some(idx) == self.selector_index.map(|i| i as _) {
stdout
.queue(MoveToPreviousLine(1))?
.queue(SetAttribute(Attribute::Bold))?
.queue(PrintStyledContent("▌ ".with(Color::Magenta)))? .queue(Print(render))?
.queue(SetAttribute(Attribute::Reset))?
.queue(Clear(ClearType::UntilNewLine))?;
} else {
stdout
.queue(MoveToPreviousLine(1))?
.queue(Print(" "))?
.queue(Print(render))?
.queue(Clear(ClearType::UntilNewLine))?;
}
}
if self.draw_count != self.dimensions.max_draw_count() {
stdout
.queue(MoveUp(1))?
.queue(self.dimensions.move_to_end_of_line())?
.queue(Clear(ClearType::FromCursorUp))?;
}
let view = self.prompt.view_padded(
self.dimensions.prompt_left_padding as _,
self.dimensions.prompt_right_padding as _,
);
stdout
.queue(self.dimensions.move_to_prompt())?
.queue(Print("> "))?
.queue(Print(&view))?
.queue(Clear(ClearType::UntilNewLine))?
.queue(self.dimensions.move_to_cursor(view.index()))?;
stdout.flush()
} else {
Ok(())
}
}
pub fn resize(&mut self, width: u16, height: u16) {
self.needs_redraw = true;
self.dimensions = Dimensions::from_screen(width, height);
self.prompt.resize(self.dimensions.prompt_max_width());
self.clamp_draw_count();
self.clamp_selector_index();
}
}
pub struct Picker<T: Send + Sync + 'static> {
matcher: Nucleo<T>,
}
impl<T: Send + Sync + 'static> Default for Picker<T> {
fn default() -> Self {
Self::new(Config::DEFAULT, Self::default_thread_count(), 1)
}
}
impl<T: Send + Sync + 'static> Picker<T> {
fn default_thread_count() -> Option<usize> {
available_parallelism()
.map(|it| it.get().checked_sub(2).unwrap_or(1))
.ok()
}
const fn default_frame_interval() -> Duration {
Duration::from_millis(16)
}
pub fn new(config: Config, num_threads: Option<usize>, columns: u32) -> Self {
Self {
matcher: Nucleo::new(config, Arc::new(|| {}), num_threads, columns),
}
}
pub fn with_config(config: Config) -> Self {
Self {
matcher: Nucleo::new(config, Arc::new(|| {}), Self::default_thread_count(), 1),
}
}
pub fn injector(&self) -> Injector<T> {
self.matcher.injector()
}
pub fn pick(&mut self) -> Result<Option<&T>, io::Error> {
if !std::io::stdin().is_tty() {
return Err(io::Error::new(io::ErrorKind::Other, "is not interactive"));
}
self.pick_inner(Self::default_frame_interval())
}
fn pick_inner(&mut self, interval: Duration) -> Result<Option<&T>, io::Error> {
let mut stdout = io::stdout();
let mut term = PickerState::new(size()?);
enable_raw_mode()?;
execute!(stdout, EnterAlternateScreen, EnableBracketedPaste)?;
let selection = loop {
let deadline = Instant::now() + interval;
match term.handle()? {
EventSummary::Continue => {}
EventSummary::UpdatePrompt(append) => {
self.matcher.pattern.reparse(
0,
&term.prompt.full_contents(),
nucleo::pattern::CaseMatching::Smart,
nucleo::pattern::Normalization::Smart,
append,
);
}
EventSummary::Select => {
break term
.selector_index
.and_then(|idx| self.matcher.snapshot().get_matched_item(idx as _))
.map(|it| it.data);
}
EventSummary::Quit => {
break None;
}
};
let status = self.matcher.tick(10);
term.update(status.changed, self.matcher.snapshot());
term.draw(&mut stdout, self.matcher.snapshot())?;
sleep(deadline - Instant::now());
};
disable_raw_mode()?;
execute!(stdout, DisableBracketedPaste, LeaveAlternateScreen)?;
Ok(selection)
}
}