use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use hjkl_buffer::Buffer;
use hjkl_form::{Input as EngineInput, Key as EngineKey, TextFieldEditor};
use crate::logic::{FilteredEntry, PickerAction, PickerEvent, PickerLogic, RequeryMode};
use crate::preview::PreviewSpans;
use crate::score::score;
const REQUERY_DEBOUNCE_MS: u64 = 150;
pub struct Picker {
pub query: TextFieldEditor,
source: Box<dyn PickerLogic>,
filtered: Vec<FilteredEntry>,
pub selected: usize,
last_query: String,
last_seen_count: usize,
cancel: Arc<AtomicBool>,
_scan: Option<JoinHandle<()>>,
requery_at: Option<Instant>,
preview_idx: Option<usize>,
preview_buffer: Buffer,
preview_status: String,
preview_label: Option<String>,
preview_spans: PreviewSpans,
preview_top_row: usize,
preview_match_row: Option<usize>,
preview_line_offset: usize,
}
impl Picker {
pub fn new(mut source: Box<dyn PickerLogic>) -> Self {
let cancel = Arc::new(AtomicBool::new(false));
let handle = source.enumerate(None, Arc::clone(&cancel));
let mut query = TextFieldEditor::new(true);
query.enter_insert_at_end();
let mut me = Self {
query,
source,
filtered: Vec::new(),
selected: 0,
last_query: String::new(),
last_seen_count: 0,
cancel,
_scan: handle,
requery_at: None,
preview_idx: None,
preview_buffer: Buffer::new(),
preview_status: String::new(),
preview_label: None,
preview_spans: PreviewSpans::default(),
preview_top_row: 0,
preview_match_row: None,
preview_line_offset: 0,
};
me.wait_for_items(Duration::from_millis(30));
me.refresh();
me.refresh_preview();
me
}
pub fn new_with_query(source: Box<dyn PickerLogic>, initial_query: &str) -> Self {
let mut me = Self::new(source);
me.query.set_text(initial_query);
me.refresh();
me.refresh_preview();
me
}
fn wait_for_items(&self, timeout: Duration) {
let deadline = Instant::now() + timeout;
loop {
if self.source.item_count() > 0 {
return;
}
if Instant::now() >= deadline {
return;
}
std::thread::sleep(Duration::from_millis(2));
}
}
pub fn title(&self) -> &str {
self.source.title()
}
pub fn has_preview(&self) -> bool {
self.source.has_preview()
}
pub fn scan_done(&self) -> bool {
self._scan.as_ref().map(|h| h.is_finished()).unwrap_or(true)
}
pub fn total(&self) -> usize {
self.source.item_count()
}
pub fn matched(&self) -> usize {
self.filtered.len()
}
pub fn tick(&mut self, now: Instant) {
if self.source.requery_mode() != RequeryMode::Spawn {
return;
}
let Some(at) = self.requery_at else { return };
if now < at {
return;
}
self.requery_at = None;
self.cancel.store(true, Ordering::Release);
let new_cancel = Arc::new(AtomicBool::new(false));
self.cancel = Arc::clone(&new_cancel);
let q = self.query.text();
let handle = self.source.enumerate(Some(&q), new_cancel);
self._scan = handle;
self.selected = 0;
self.preview_idx = None;
}
pub fn refresh(&mut self) -> bool {
let count = self.source.item_count();
let q = self.query.text();
let q_changed = q != self.last_query;
let count_changed = count != self.last_seen_count;
if !q_changed && !count_changed {
return false;
}
let spawn_mode = self.source.requery_mode() == RequeryMode::Spawn;
if spawn_mode && q_changed {
self.requery_at = Some(Instant::now() + Duration::from_millis(REQUERY_DEBOUNCE_MS));
}
self.last_query.clone_from(&q);
self.last_seen_count = count;
if spawn_mode {
self.filtered = (0..count)
.map(|idx| FilteredEntry {
idx,
matches: Vec::new(),
})
.collect();
if self.selected >= self.filtered.len() {
self.selected = self.filtered.len().saturating_sub(1);
}
return true;
}
let q_lower = q.to_lowercase();
let mut scored: Vec<(i64, usize, String, Vec<usize>)> = Vec::new();
for i in 0..count {
let m = self.source.match_text(i);
let m_lower = m.to_lowercase();
let (sc, positions) = if q.is_empty() {
(0i64, Vec::new())
} else {
match score(&m_lower, &q_lower) {
Some(v) => v,
None => continue,
}
};
scored.push((sc, i, m_lower, positions));
}
scored.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.2.cmp(&b.2)));
scored.truncate(500);
self.filtered = scored
.into_iter()
.map(|(_, idx, _, matches)| FilteredEntry { idx, matches })
.collect();
if self.selected >= self.filtered.len() {
self.selected = self.filtered.len().saturating_sub(1);
}
true
}
pub fn refresh_preview(&mut self) {
if !self.source.has_preview() {
return;
}
let target_idx = self.filtered.get(self.selected).map(|e| e.idx);
if target_idx == self.preview_idx {
return;
}
self.preview_idx = target_idx;
let Some(idx) = target_idx else {
self.preview_buffer = Buffer::new();
self.preview_status.clear();
self.preview_label = None;
self.preview_spans = PreviewSpans::default();
self.preview_top_row = 0;
self.preview_match_row = None;
self.preview_line_offset = 0;
return;
};
let label = self.source.label(idx);
let (buf, status, spans) = self.source.preview(idx);
self.preview_buffer = buf;
self.preview_status = status;
self.preview_label = Some(label);
self.preview_spans = spans;
self.preview_top_row = self.source.preview_top_row(idx);
self.preview_match_row = self.source.preview_match_row(idx);
self.preview_line_offset = self.source.preview_line_offset(idx);
}
pub fn preview_top_row(&self) -> usize {
self.preview_top_row
}
pub fn preview_match_row(&self) -> Option<usize> {
self.preview_match_row
}
pub fn preview_line_offset(&self) -> usize {
self.preview_line_offset
}
pub fn preview_spans(&self) -> &PreviewSpans {
&self.preview_spans
}
pub fn preview_buffer(&self) -> &Buffer {
&self.preview_buffer
}
pub fn preview_status(&self) -> &str {
&self.preview_status
}
pub fn preview_label(&self) -> Option<&str> {
self.preview_label.as_deref()
}
pub fn visible_entries(&self) -> Vec<(String, Vec<usize>)> {
let query = &self.last_query;
self.filtered
.iter()
.map(|e| {
let label = self.source.label(e.idx);
let positions = self
.source
.label_match_positions(e.idx, query, &label)
.unwrap_or_else(|| e.matches.clone());
(label, positions)
})
.collect()
}
fn selected_action(&self) -> Option<PickerAction> {
let idx = self.filtered.get(self.selected)?.idx;
Some(self.source.select(idx))
}
pub fn handle_key(&mut self, key: KeyEvent) -> PickerEvent {
if key.code == KeyCode::Esc {
return PickerEvent::Cancel;
}
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
return PickerEvent::Cancel;
}
if key.code == KeyCode::Enter {
return match self.selected_action() {
Some(a) => PickerEvent::Select(a),
None => PickerEvent::None,
};
}
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
match key.code {
KeyCode::Down => {
self.move_selection(1);
return PickerEvent::None;
}
KeyCode::Up => {
self.move_selection(-1);
return PickerEvent::None;
}
KeyCode::Char('n') if ctrl => {
self.move_selection(1);
return PickerEvent::None;
}
KeyCode::Char('p') if ctrl => {
self.move_selection(-1);
return PickerEvent::None;
}
_ => {}
}
let input: EngineInput = key.into();
if input.key == EngineKey::Enter || input.key == EngineKey::Esc {
return PickerEvent::None;
}
self.query.handle_input(input);
PickerEvent::None
}
fn move_selection(&mut self, delta: i32) {
if self.filtered.is_empty() {
self.selected = 0;
return;
}
let len = self.filtered.len() as i32;
let next = self.selected as i32 + delta;
let wrapped = next.rem_euclid(len);
self.selected = wrapped as usize;
}
}