Skip to main content

matchmaker/render/
state.rs

1use cba::{bait::TransformExt, broc::EnvVars, env_vars, unwrap};
2
3use crate::{
4    SSS, Selection, Selector,
5    action::ActionExt,
6    event::EventSender,
7    message::{Event, Interrupt},
8    nucleo::{Status, injector::WorkerInjector},
9    ui::{DisplayUI, OverlayUI, PickerUI, PreviewUI, Rect, UI},
10};
11
12// --------------------------------------------------------------------
13#[derive(Default, Debug)]
14pub struct State {
15    last_id: Option<u32>,
16    interrupt: Interrupt,
17    interrupt_payload: String,
18
19    // Stores "last" state to emit events on change
20    pub(crate) input: String,
21    pub(crate) col: Option<usize>,
22    pub(crate) iterations: u32,
23    pub(crate) preview_visible: bool,
24    pub(crate) layout: [Rect; 4], //preview, input, status, results
25    pub(crate) overlay_index: Option<usize>,
26    pub(crate) synced: [bool; 2], // ran, synced
27
28    pub(crate) events: Event,
29
30    /// The String passed to SetPreview
31    pub preview_set_payload: Option<String>,
32    /// The payload left by [`crate::action::Action::Preview`]
33    pub preview_payload: String,
34    /// The payload left by [`crate::action::Action::Store`]
35    pub store_payload: String,
36    /// A place to stash the preview visibility when overriding it
37    stashed_preview_visibility: Option<bool>,
38    /// Setting this to true finishes the picker with the contents of [`Selector`].
39    /// If [`Selector`] is disabled, the picker finishes with the current item.
40    /// If there are no items to finish with, the picker finishes with [`crate::errors::MatchError::Abort`]\(0).
41    pub should_quit: bool,
42    /// Setting this to true finishes the picker with [`crate::MatchError::NoMatch`].
43    pub should_quit_nomatch: bool,
44    pub filtering: bool,
45}
46
47impl State {
48    pub fn new() -> Self {
49        // this is the same as default
50        Self {
51            last_id: None,
52            interrupt: Interrupt::None,
53            interrupt_payload: String::new(),
54
55            preview_payload: String::new(),
56            store_payload: String::new(),
57            preview_set_payload: None,
58            preview_visible: false,
59            stashed_preview_visibility: None,
60            layout: [Rect::default(); 4],
61            overlay_index: None,
62            col: None,
63
64            input: String::new(),
65            iterations: 0,
66            synced: [false; 2],
67
68            events: Event::empty(),
69            should_quit: false,
70            should_quit_nomatch: false,
71            filtering: true,
72        }
73    }
74    // ------ properties -----------
75
76    pub fn contains(&self, event: Event) -> bool {
77        self.events.contains(event)
78    }
79
80    pub fn payload(&self) -> &String {
81        &self.interrupt_payload
82    }
83
84    pub fn interrupt(&self) -> Interrupt {
85        self.interrupt
86    }
87
88    pub fn set_interrupt(&mut self, interrupt: Interrupt, payload: String) {
89        self.interrupt = interrupt;
90        self.interrupt_payload = payload;
91    }
92
93    pub fn clear_interrupt(&mut self) {
94        self.interrupt = Interrupt::None;
95        self.interrupt_payload.clear();
96    }
97
98    pub fn insert(&mut self, event: Event) {
99        self.events.insert(event);
100    }
101
102    pub fn overlay_index(&self) -> Option<usize> {
103        self.overlay_index
104    }
105    pub fn preview_set_payload(&self) -> Option<String> {
106        self.preview_set_payload.clone()
107    }
108    pub fn preview_payload(&self) -> &String {
109        &self.preview_payload
110    }
111    pub fn stashed_preview_visibility(&self) -> Option<bool> {
112        self.stashed_preview_visibility
113    }
114
115    // ------- updates --------------
116    pub(crate) fn update_input(&mut self, new_input: &str) -> bool {
117        let changed = self.input.cmp_replace(new_input.to_string());
118        if changed {
119            self.insert(Event::QueryChange);
120        }
121        changed
122    }
123
124    pub(crate) fn update_preview(&mut self, context: &str) -> bool {
125        let changed = self.preview_payload.cmp_replace(context.into());
126        if changed {
127            self.insert(Event::PreviewChange);
128        }
129        changed
130    }
131
132    pub(crate) fn update_preview_set(&mut self, context: String) -> bool {
133        let next = Some(context);
134        let changed = self.preview_set_payload.cmp_replace(next);
135        if changed {
136            self.insert(Event::PreviewSet);
137        }
138        changed
139    }
140
141    pub(crate) fn update_preview_unset(&mut self) {
142        let changed = self.preview_set_payload.cmp_replace(None);
143        if changed {
144            self.insert(Event::PreviewSet);
145        }
146    }
147
148    pub(crate) fn update_layout(&mut self, new_layout: [Rect; 4]) -> bool {
149        let changed = self.layout.cmp_replace(new_layout);
150        if changed {
151            self.insert(Event::Resize);
152        }
153        changed
154    }
155
156    /// Emit PreviewChange event on change to visible
157    pub(crate) fn update_preview_visible(&mut self, preview_ui: &PreviewUI) -> bool {
158        let visible = preview_ui.visible();
159        let changed = self.preview_visible.cmp_replace(visible);
160        if changed && visible {
161            self.insert(Event::PreviewChange);
162        }
163        changed
164    }
165
166    pub(crate) fn update<'a, T: SSS, S: Selection, A: ActionExt>(
167        &'a mut self,
168        picker_ui: &'a PickerUI<T, S>,
169        overlay_ui: &'a Option<OverlayUI<A>>,
170    ) {
171        if self.iterations == 0 {
172            self.insert(Event::Start);
173        }
174        self.iterations += 1;
175
176        self.update_input(&picker_ui.input.input);
177        self.col = picker_ui.results.col();
178
179        let status = &picker_ui.results.status;
180        self.synced[1] |= status.running;
181        if status.changed {
182            // add a synced event when worker stops running
183            if !picker_ui.results.status.running {
184                if !self.synced[0] {
185                    // this is supposed to fire when all inputs have been loaded into nucleo although it clearly can't be race-free
186                    if picker_ui.results.status.item_count > 0 {
187                        self.insert(Event::Synced);
188                        self.synced[0] = true;
189                    }
190                } else {
191                    // this should be emitted every time input filter changes
192                    // note that this will never emit on empty input
193                    log::trace!("resynced on iteration {}", self.iterations);
194                    self.insert(Event::Resynced);
195                }
196            }
197        }
198
199        if let Some(o) = overlay_ui {
200            if self.overlay_index != o.index() {
201                self.insert(Event::OverlayChange);
202                self.overlay_index = o.index()
203            }
204            self.overlay_index = o.index()
205        }
206
207        let new_id = get_current(picker_ui).map(|x| x.0);
208        let changed = self.last_id != get_current(picker_ui).map(|x| x.0);
209        if changed {
210            self.last_id = new_id;
211            self.insert(Event::CursorChange);
212        }
213        // log::trace!("{self:?}");
214    }
215
216    // ---------- flush -----------
217    // public for tests only!
218    pub fn dispatcher<'a, 'b: 'a, T: SSS, S: Selection>(
219        &'a mut self,
220        ui: &'a mut UI,
221        picker_ui: &'a mut PickerUI<'b, T, S>,
222        footer_ui: &'a mut DisplayUI,
223        preview_ui: &'a mut Option<PreviewUI>,
224        event_controller: &'a EventSender,
225    ) -> MMState<'a, 'b, T, S> {
226        MMState {
227            state: self,
228            ui,
229            picker_ui,
230            footer_ui,
231            preview_ui,
232            event_controller,
233        }
234    }
235
236    fn reset(&mut self) {
237        // nothing
238    }
239
240    pub(crate) fn events(&mut self) -> Event {
241        self.reset();
242        std::mem::take(&mut self.events)
243    }
244}
245
246// ----------------------------------------------------------------------
247pub struct MMState<'a, 'b: 'a, T: SSS, S: Selection> {
248    // access through deref/mut
249    pub(crate) state: &'a mut State,
250
251    pub ui: &'a mut UI,
252    pub picker_ui: &'a mut PickerUI<'b, T, S>,
253    pub footer_ui: &'a mut DisplayUI,
254    pub preview_ui: &'a mut Option<PreviewUI>,
255    pub event_controller: &'a EventSender,
256}
257
258impl<'a, 'b: 'a, T: SSS, S: Selection> MMState<'a, 'b, T, S> {
259    pub fn previewer_area(&self) -> Option<&Rect> {
260        self.preview_ui.as_ref().map(|ui| &ui.area)
261    }
262
263    pub fn ui_area(&self) -> &Rect {
264        &self.ui.area
265    }
266    pub fn ui_size(&self) -> [u16; 2] {
267        let q = &self.ui.area;
268        [
269            q.width.saturating_sub(self.ui.config.border.width()),
270            q.height.saturating_sub(self.ui.config.border.width()),
271        ]
272    }
273
274    pub fn current_item(&self) -> Option<S> {
275        get_current(self.picker_ui).map(|s| s.1)
276    }
277
278    /// Same as current_item, but without applying the identifier.
279    pub fn current_raw(&self) -> Option<&T> {
280        self.picker_ui
281            .worker
282            .get_nth(self.picker_ui.results.index())
283    }
284    /// Runs f on selections if nonempty, otherwise, the current item
285    pub fn map_selected_to_vec<U>(&self, mut f: impl FnMut(&S) -> U) -> Vec<U> {
286        if !self.picker_ui.selector.is_empty() {
287            self.picker_ui.selector.map_to_vec(f)
288        } else {
289            get_current(self.picker_ui)
290                .iter()
291                .map(|s| f(&s.1))
292                .collect()
293        }
294    }
295
296    pub fn injector(&self) -> WorkerInjector<T> {
297        self.picker_ui.worker.injector()
298    }
299
300    pub fn widths(&self) -> &Vec<u16> {
301        self.picker_ui.results.widths()
302    }
303
304    pub fn status(&self) -> &Status {
305        // replace StatusType with the actual type
306        &self.picker_ui.results.status
307    }
308
309    pub fn selections(&self) -> &Selector<T, S> {
310        &self.picker_ui.selector
311    }
312
313    pub fn preview_visible(&self) -> bool {
314        self.preview_ui.as_ref().is_some_and(|s| s.visible())
315    }
316
317    pub fn get_content_and_index(&self) -> (String, u32) {
318        (
319            self.picker_ui.input.input.clone(),
320            self.picker_ui.results.index(),
321        )
322    }
323
324    pub fn restart_worker(&mut self) {
325        self.picker_ui.worker.restart(false);
326        self.state.synced = [false; 2];
327    }
328
329    pub fn make_env_vars(&self) -> EnvVars {
330        env_vars! {
331            "FZF_LINES" => self.ui_area().height.to_string(),
332            "FZF_COLUMNS" => self.ui_area().width.to_string(),
333            "FZF_TOTAL_COUNT" => self.status().item_count.to_string(),
334            "FZF_MATCH_COUNT" => self.status().matched_count.to_string(),
335            "FZF_SELECT_COUNT" => self.selections().len().to_string(),
336            "FZF_POS" => get_current(self.picker_ui).map_or("".to_string(), |x| format!("{}", x.0)),
337            "FZF_QUERY" => self.input.clone(),
338
339            "MM_LINES" => self.ui_area().height.to_string(),
340            "MM_COLUMNS" => self.ui_area().width.to_string(),
341            "MM_TOTAL_COUNT" => self.status().item_count.to_string(),
342            "MM_MATCH_COUNT" => self.status().matched_count.to_string(),
343            "MM_SELECT_COUNT" => self.selections().len().to_string(),
344            "MM_POS" => get_current(self.picker_ui).map_or("".to_string(), |x| format!("{}", x.0)),
345            "MM_QUERY" => self.input.clone(),
346
347            "MM_STORE" => if self.store_payload.is_empty() { "".into() } else { self.store_payload.clone() },
348        }
349    }
350
351    // -------- other
352
353    /// Some(s) -> Save current visibility, set visibility to s
354    /// None -> Restore saved visibility
355    pub fn stash_preview_visibility(&mut self, show: Option<bool>) {
356        log::trace!("Called stash_preview_visibility with {show:?}");
357        let p = unwrap!(self.preview_ui);
358        if let Some(s) = show {
359            self.state.stashed_preview_visibility = Some(p.visible());
360            p.show(s);
361        } else if let Some(s) = self.state.stashed_preview_visibility.take() {
362            p.show(s);
363        }
364    }
365}
366
367pub(crate) fn get_current<T: SSS, S: Selection>(picker_ui: &PickerUI<T, S>) -> Option<(u32, S)> {
368    let current_raw = picker_ui.worker.get_nth(picker_ui.results.index());
369    current_raw.map(picker_ui.selector.identifier)
370}
371
372// ----- BOILERPLATE -----------
373impl<'a, 'b: 'a, T: SSS, S: Selection> std::ops::Deref for MMState<'a, 'b, T, S> {
374    type Target = State;
375
376    fn deref(&self) -> &Self::Target {
377        self.state
378    }
379}
380
381impl<'a, 'b: 'a, T: SSS, S: Selection> std::ops::DerefMut for MMState<'a, 'b, T, S> {
382    fn deref_mut(&mut self) -> &mut Self::Target {
383        self.state
384    }
385}