Skip to main content

matchmaker/render/
state.rs

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