Skip to main content

hjkl_picker/
logic.rs

1use std::any::Any;
2use std::path::PathBuf;
3use std::sync::Arc;
4use std::sync::atomic::AtomicBool;
5use std::thread::JoinHandle;
6
7use hjkl_buffer::Buffer;
8
9/// Action emitted when the user picks an item. The App dispatches each
10/// variant to the right machinery.
11pub enum PickerAction {
12    /// User picked an item — payload is app-defined. Downcast to your
13    /// app's action type via `Any`.
14    Custom(Box<dyn Any + Send>),
15    /// No-op action (used for error sentinel items).
16    None,
17}
18
19/// How the picker reacts when the query string changes.
20#[derive(Clone, Copy, PartialEq, Eq)]
21pub enum RequeryMode {
22    /// Filter the existing in-memory item vec. `enumerate` is called once
23    /// at open with `query = None`; subsequent query changes just re-score.
24    FilterInMemory,
25    /// Re-spawn the source for every debounced query change. `enumerate` is
26    /// called with `query = Some(q)` after each debounce interval; the
27    /// source resets its item vec each time.
28    Spawn,
29}
30
31/// Fully-erased source for one kind of picker. The picker only talks to
32/// the source via opaque `usize` indices into the source's internal item vec.
33pub trait PickerLogic: Send + 'static {
34    /// Title shown above the input row (e.g. "files", "buffers", "grep").
35    fn title(&self) -> &str;
36
37    /// Number of items currently available (grows as enumeration progresses).
38    fn item_count(&self) -> usize;
39
40    /// Display label for the row at `idx`.
41    fn label(&self, idx: usize) -> String;
42
43    /// Text the fuzzy scorer scores against. May equal `label`.
44    fn match_text(&self, idx: usize) -> String;
45
46    /// Whether this source wants the preview pane.
47    fn has_preview(&self) -> bool {
48        true
49    }
50
51    /// Build the preview pane for the row. Default: empty buffer.
52    ///
53    /// Returns `(buffer, status_text)`. The picker is bonsai-agnostic —
54    /// it never produces syntax spans itself. Hosts that want syntax
55    /// highlighting in the preview pane read [`Self::preview_path`] and
56    /// run the buffer's bytes through their own highlighter at render
57    /// time.
58    fn preview(&self, idx: usize) -> (Buffer, String) {
59        let _ = idx;
60        (Buffer::new(), String::new())
61    }
62
63    /// File-system path the preview's content was loaded from, when one
64    /// exists. Used by the host to drive language-aware preview rendering
65    /// (e.g. tree-sitter syntax highlighting). Default `None` for sources
66    /// whose preview has no on-disk path (an in-memory snapshot, a help
67    /// message, etc.).
68    fn preview_path(&self, idx: usize) -> Option<PathBuf> {
69        let _ = idx;
70        None
71    }
72
73    /// Initial scroll position (top row) for the preview viewport.
74    /// Sources that show a windowed preview around a specific line override
75    /// this so the gutter line numbers reflect the actual file line. Default 0.
76    fn preview_top_row(&self, idx: usize) -> usize {
77        let _ = idx;
78        0
79    }
80
81    /// 0-based row to visually mark in the preview (e.g. grep match line).
82    /// Default `None` → no highlight. Returning `Some(row)` tells the
83    /// renderer to paint a `cursor_line_bg` across that row.
84    fn preview_match_row(&self, idx: usize) -> Option<usize> {
85        let _ = idx;
86        None
87    }
88
89    /// Added to the gutter line numbers in the preview. Sources that
90    /// snapshot a window of a larger document (e.g. buffer picker
91    /// snapshotting ±N lines around the cursor) use this so the gutter
92    /// shows the original document line numbers rather than restarting
93    /// at 1. Default 0.
94    fn preview_line_offset(&self, idx: usize) -> usize {
95        let _ = idx;
96        0
97    }
98
99    /// Translate the picked row into an action.
100    fn select(&self, idx: usize) -> PickerAction;
101
102    /// Handle a key before the picker's default handling. Return `Some(action)`
103    /// to short-circuit and emit that action immediately. Return `None` to let
104    /// the picker handle the key normally. Default: `None`.
105    fn handle_key(&self, idx: usize, key: crossterm::event::KeyEvent) -> Option<PickerAction> {
106        let _ = (idx, key);
107        None
108    }
109
110    /// How the picker should react when the query changes.
111    fn requery_mode(&self) -> RequeryMode {
112        RequeryMode::FilterInMemory
113    }
114
115    /// Override the highlight positions for the row at `idx`.
116    ///
117    /// Default (`None`) means the picker uses fuzzy-scorer match positions.
118    /// Sources whose query has its own match semantics (regex grep, exact
119    /// match) implement this to return positions in the LABEL string
120    /// (char indices, same convention as fuzzy positions).
121    fn label_match_positions(&self, idx: usize, query: &str, label: &str) -> Option<Vec<usize>> {
122        let _ = (idx, query, label);
123        None
124    }
125
126    /// Whether the picker should keep the source's enumeration order when
127    /// the query is empty. Default `false` means empty-query rows are sorted
128    /// by match-text ascending. Sources that pre-sort meaningfully (e.g. by
129    /// recency, by HEAD-first) override to `true`.
130    fn preserve_source_order(&self) -> bool {
131        false
132    }
133
134    /// Optional per-row semantic styling for the label. Char-index ranges into
135    /// the label with a base style. Fuzzy-match positions overlay these.
136    /// Default `None` means no extra styling.
137    fn label_styles(
138        &self,
139        idx: usize,
140        label: &str,
141    ) -> Option<Vec<(std::ops::Range<usize>, ratatui::style::Style)>> {
142        let _ = (idx, label);
143        None
144    }
145
146    /// Re-enumerate items.
147    ///
148    /// - `FilterInMemory` sources: called once at open with `query = None`.
149    /// - `Spawn` sources: called on every debounced query change with
150    ///   `query = Some(q)`. Must reset the internal item vec before pushing
151    ///   new items.
152    ///
153    /// `cancel` is set to `true` by the picker when a newer requery
154    /// supersedes this one — long-running threads should poll it and bail.
155    fn enumerate(&mut self, query: Option<&str>, cancel: Arc<AtomicBool>)
156    -> Option<JoinHandle<()>>;
157}
158
159/// Outcome of routing one key event into the picker.
160pub enum PickerEvent {
161    /// Key consumed; picker stays open.
162    None,
163    /// User dismissed the picker.
164    Cancel,
165    /// User picked an item — dispatch this action.
166    Select(PickerAction),
167}
168
169/// One entry in the filtered/ranked list. Stores the index into the
170/// source item vec together with the char positions that satisfied the
171/// fuzzy match (used by the renderer to highlight matched chars).
172pub(crate) struct FilteredEntry {
173    /// Index into the source's internal item vec.
174    pub idx: usize,
175    /// Char-indices in the item's label where needle chars matched.
176    /// Empty when the query is empty (no highlight needed).
177    pub matches: Vec<usize>,
178}