Skip to main content

matchmaker/render/
state.rs

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