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}