mod bind;
mod editable;
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, 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},
editable::{EditableString, MovementType},
};
pub use nucleo;
pub enum EventSummary {
Continue,
UpdateQuery(bool),
Select,
Quit,
}
#[derive(Debug)]
struct PickerState {
width: u16,
height: u16,
selector_index: Option<u16>,
query: EditableString,
draw_count: u16,
item_count: u32,
matched_item_count: u32,
needs_redraw: bool,
}
impl PickerState {
pub fn new(dimensions: (u16, u16)) -> Self {
let (width, height) = dimensions;
Self {
width,
height,
selector_index: None,
query: EditableString::default(),
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.height - 2)
}
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 shift(&mut self, st: MovementType) {
self.needs_redraw |= self.query.shift(st);
}
pub fn paste(&mut self, contents: &str) {
self.needs_redraw |= self.query.paste(contents);
}
pub fn insert_char(&mut self, ch: char) {
self.needs_redraw |= self.query.insert(ch);
}
pub fn delete_char(&mut self) {
self.needs_redraw |= self.query.delete();
}
fn format_display(&self, display: &Utf32String) -> String {
display
.slice(..)
.chars()
.filter(|ch| !ch.is_control())
.take(self.width as usize - 2)
.map(|ch| match ch {
'\n' => ' ',
s => s,
})
.collect()
}
fn handle(&mut self) -> Result<EventSummary, io::Error> {
let mut update_query = 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.shift(MovementType::Start),
Event::MoveToEnd => self.shift(MovementType::End),
Event::Insert(ch) => {
update_query = true;
append &= self.query.cursor_at_end();
self.insert_char(ch);
}
Event::Select => return Ok(EventSummary::Select),
Event::MoveUp => self.incr_selection(),
Event::MoveDown => self.decr_selection(),
Event::MoveLeft => self.shift(MovementType::Left),
Event::MoveRight => self.shift(MovementType::Right),
Event::Delete => {
update_query = true;
append = false;
self.delete_char();
}
Event::Quit => return Ok(EventSummary::Quit),
Event::Resize(width, height) => {
self.resize(width, height);
}
Event::Paste(contents) => {
update_query = true;
append &= self.query.cursor_at_end();
self.paste(&contents);
}
}
}
}
Ok(if update_query {
EventSummary::UpdateQuery(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(Clear(ClearType::All))?
.queue(MoveTo(0, self.height - 2))?;
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)?;
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 usize) {
stdout
.queue(SetAttribute(Attribute::Bold))?
.queue(MoveUp(1))?
.queue(MoveToColumn(2))?
.queue(Print(render))?
.queue(SetAttribute(Attribute::Reset))?;
} else {
stdout
.queue(MoveUp(1))?
.queue(MoveToColumn(2))?
.queue(Print(render))?;
}
}
if let Some(position) = self.selector_index {
stdout
.queue(MoveTo(0, self.height - 3 - position))?
.queue(PrintStyledContent("▌".with(Color::Magenta)))?;
}
stdout
.queue(MoveTo(0, self.height - 1))?
.queue(Print("> "))?
.queue(Print(&self.query))?
.queue(MoveTo(self.query.position() as u16 + 2, self.height - 1))?;
stdout.flush()
} else {
Ok(())
}
}
pub fn resize(&mut self, width: u16, height: u16) {
self.needs_redraw = true;
self.width = width;
self.height = height;
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::suggested_threads(), 1)
}
}
impl<T: Send + Sync + 'static> Picker<T> {
fn suggested_threads() -> Option<usize> {
available_parallelism()
.map(|it| it.get().checked_sub(2).unwrap_or(1))
.ok()
}
const fn suggested_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::suggested_threads(), 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::suggested_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::UpdateQuery(append) => {
self.matcher.pattern.reparse(
0,
&term.query.to_string(),
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 u32))
.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)
}
}