broot/preview/
preview_state.rs

1use {
2    super::*,
3    crate::{
4        app::*,
5        command::{
6            Command,
7            ScrollCommand,
8            TriggerType,
9        },
10        display::{
11            Screen,
12            W,
13        },
14        errors::ProgramError,
15        flag::Flag,
16        pattern::InputPattern,
17        task_sync::Dam,
18        tree::TreeOptions,
19        verb::*,
20    },
21    crokey::crossterm::{
22        QueueableCommand,
23        cursor,
24    },
25    std::path::{
26        Path,
27        PathBuf,
28    },
29    termimad::{
30        Area,
31        CropWriter,
32        SPACE_FILLING,
33    },
34};
35
36/// an application state dedicated to previewing files.
37///
38/// It's usually the only state in its panel and is kept when the
39/// selection changes (other panels indirectly call `set_selected_path`).
40pub struct PreviewState {
41    pub preview_area: Area,
42    dirty: bool,          // true when background must be cleared
43    source_path: PathBuf, // path to the file whose preview is requested
44    transform: Option<PreviewTransform>,
45    preview: Preview,
46    pending_pattern: InputPattern, // a pattern (or not) which has not yet be applied
47    filtered_preview: Option<Preview>,
48    removed_pattern: InputPattern,
49    preferred_mode: Option<PreviewMode>,
50    tree_options: TreeOptions,
51    mode: Mode,
52}
53
54impl PreviewState {
55    pub fn new(
56        source_path: PathBuf,
57        pending_pattern: InputPattern,
58        preferred_mode: Option<PreviewMode>,
59        tree_options: TreeOptions,
60        con: &AppContext,
61    ) -> PreviewState {
62        let preview_area = Area::uninitialized(); // will be fixed at drawing time
63        let transform = con
64            .preview_transformers
65            .transform(&source_path, preferred_mode);
66        let preview_path = transform
67            .as_ref()
68            .map(|c| &c.output_path)
69            .unwrap_or(&source_path);
70        let preview = Preview::new(preview_path, preferred_mode, con);
71        PreviewState {
72            preview_area,
73            dirty: true,
74            source_path,
75            transform,
76            preview,
77            pending_pattern,
78            filtered_preview: None,
79            removed_pattern: InputPattern::none(),
80            preferred_mode,
81            tree_options,
82            mode: con.initial_mode(),
83        }
84    }
85    pub fn preview_path(&self) -> &Path {
86        self.transform
87            .as_ref()
88            .map(|c| &c.output_path)
89            .unwrap_or(&self.source_path)
90    }
91    fn vis_preview(&self) -> &Preview {
92        self.filtered_preview.as_ref().unwrap_or(&self.preview)
93    }
94    fn mut_preview(&mut self) -> &mut Preview {
95        self.filtered_preview.as_mut().unwrap_or(&mut self.preview)
96    }
97    fn set_mode(
98        &mut self,
99        mode: PreviewMode,
100        con: &AppContext,
101    ) -> Result<CmdResult, ProgramError> {
102        if self.preview.get_mode() == Some(mode) {
103            return Ok(CmdResult::Keep);
104        }
105        Ok(match Preview::with_mode(self.preview_path(), mode, con) {
106            Ok(preview) => {
107                self.preview = preview;
108                self.preferred_mode = Some(mode);
109                CmdResult::Keep
110            }
111            Err(e) => CmdResult::DisplayError(format!("Can't display as {mode:?} : {e:?}")),
112        })
113    }
114
115    fn no_opt_selection(&self) -> Selection<'_> {
116        match self.transform.as_ref() {
117            // When there's a transform, we can't assume the line number makes sense
118            Some(transform) => Selection {
119                path: &transform.output_path,
120                stype: SelectionType::File,
121                is_exe: false,
122                line: 0,
123            },
124            None => Selection {
125                path: &self.source_path,
126                stype: SelectionType::File,
127                is_exe: false,
128                line: self.vis_preview().get_selected_line_number().unwrap_or(0),
129            },
130        }
131    }
132
133    /// do the preview filtering if required and not yet done
134    fn do_pending_search(
135        &mut self,
136        con: &AppContext,
137        dam: &mut Dam,
138    ) -> Result<(), ProgramError> {
139        let old_selection = self
140            .filtered_preview
141            .as_ref()
142            .and_then(|p| p.get_selected_line_number())
143            .or_else(|| self.preview.get_selected_line_number());
144        let pattern = self.pending_pattern.take();
145        self.filtered_preview = time!(
146            Info,
147            "preview filtering",
148            self.preview
149                .filtered(self.preview_path(), pattern, dam, con),
150        ); // can be None if a cancellation was required
151        if let Some(ref mut filtered_preview) = self.filtered_preview {
152            if let Some(number) = old_selection {
153                filtered_preview.try_select_line_number(number);
154            }
155        }
156        Ok(())
157    }
158}
159
160impl PanelState for PreviewState {
161    fn get_type(&self) -> PanelStateType {
162        PanelStateType::Preview
163    }
164
165    fn set_mode(
166        &mut self,
167        mode: Mode,
168    ) {
169        self.mode = mode;
170    }
171
172    fn get_mode(&self) -> Mode {
173        self.mode
174    }
175
176    fn get_pending_task(&self) -> Option<&'static str> {
177        if self.preview.is_partial() {
178            Some("loading")
179        } else if self.pending_pattern.is_some() {
180            Some("searching")
181        } else {
182            None
183        }
184    }
185
186    fn on_pattern(
187        &mut self,
188        pat: InputPattern,
189        _app_state: &AppState,
190        _con: &AppContext,
191    ) -> Result<CmdResult, ProgramError> {
192        if pat.is_none() {
193            if let Some(filtered_preview) = self.filtered_preview.take() {
194                let old_selection = filtered_preview.get_selected_line_number();
195                if let Some(number) = old_selection {
196                    self.preview.try_select_line_number(number);
197                }
198                self.removed_pattern = filtered_preview.pattern();
199            }
200        } else if !self.preview.is_filterable() {
201            return Ok(CmdResult::error("this preview can't be searched"));
202        }
203        self.pending_pattern = pat;
204        Ok(CmdResult::Keep)
205    }
206
207    fn do_pending_task(
208        &mut self,
209        _app_state: &mut AppState,
210        _screen: Screen,
211        con: &AppContext,
212        dam: &mut Dam,
213    ) -> Result<(), ProgramError> {
214        if self.preview.is_partial() {
215            self.preview.complete_loading(con, dam)?;
216        } else if self.pending_pattern.is_some() {
217            self.do_pending_search(con, dam)?;
218        }
219        Ok(())
220    }
221
222    fn selected_path(&self) -> Option<&Path> {
223        Some(&self.source_path)
224    }
225
226    fn set_selected_path(
227        &mut self,
228        path: PathBuf,
229        con: &AppContext,
230    ) {
231        let selected_line_number = if self.preview_path() == path {
232            self.preview.get_selected_line_number()
233        } else {
234            None
235        };
236        if let Some(fp) = &self.filtered_preview {
237            self.pending_pattern = fp.pattern();
238        };
239        self.transform = con
240            .preview_transformers
241            .transform(&path, self.preferred_mode);
242        let preview_path = self.transform.as_ref().map_or(&path, |c| &c.output_path);
243        self.preview = Preview::new(preview_path, self.preferred_mode, con);
244        if let Some(number) = selected_line_number {
245            self.preview.try_select_line_number(number);
246        }
247        self.source_path = path;
248    }
249
250    fn selection(&self) -> Option<Selection<'_>> {
251        Some(self.no_opt_selection())
252    }
253
254    fn tree_options(&self) -> TreeOptions {
255        self.tree_options.clone()
256    }
257
258    fn with_new_options(
259        &mut self,
260        _screen: Screen,
261        change_options: &dyn Fn(&mut TreeOptions) -> &'static str,
262        _in_new_panel: bool, // TODO open tree if true
263        _con: &AppContext,
264    ) -> CmdResult {
265        change_options(&mut self.tree_options);
266        CmdResult::Keep
267    }
268
269    fn refresh(
270        &mut self,
271        _screen: Screen,
272        con: &AppContext,
273    ) -> Command {
274        self.dirty = true;
275        self.set_selected_path(self.source_path.clone(), con);
276        Command::empty()
277    }
278
279    fn on_click(
280        &mut self,
281        _x: u16,
282        y: u16,
283        _screen: Screen,
284        _con: &AppContext,
285    ) -> Result<CmdResult, ProgramError> {
286        if y >= self.preview_area.top && y < self.preview_area.top + self.preview_area.height {
287            let y = y - self.preview_area.top;
288            self.mut_preview().try_select_y(y);
289        }
290        Ok(CmdResult::Keep)
291    }
292
293    fn display(
294        &mut self,
295        w: &mut W,
296        disc: &DisplayContext,
297    ) -> Result<(), ProgramError> {
298        let state_area = &disc.state_area;
299        if state_area.height < 3 {
300            warn!("area too small for preview");
301            return Ok(());
302        }
303        let mut preview_area = state_area.clone();
304        preview_area.height -= 1;
305        preview_area.top += 1;
306        if preview_area != self.preview_area {
307            self.dirty = true;
308            self.preview_area = preview_area;
309        }
310        if self.dirty {
311            disc.panel_skin.styles.default.queue_bg(w)?;
312            disc.screen.clear_area_to_right(w, state_area)?;
313            self.dirty = false;
314        }
315        let styles = &disc.panel_skin.styles;
316        w.queue(cursor::MoveTo(state_area.left, 0))?;
317        let mut cw = CropWriter::new(w, state_area.width as usize);
318        let file_name = self
319            .source_path
320            .file_name()
321            .map(|n| n.to_string_lossy().to_string())
322            .unwrap_or_else(|| "???".to_string());
323        cw.queue_str(&styles.preview_title, &file_name)?;
324        let info_area = Area::new(
325            state_area.left + state_area.width - cw.allowed as u16,
326            state_area.top,
327            cw.allowed as u16,
328            1,
329        );
330        cw.fill(&styles.preview_title, &SPACE_FILLING)?;
331        let preview = self.filtered_preview.as_mut().unwrap_or(&mut self.preview);
332        preview.display_info(w, disc.screen, disc.panel_skin, &info_area)?;
333        if let Err(err) = preview.display(w, disc, &self.preview_area) {
334            warn!("error while displaying file: {:?}", &err);
335            if preview.get_mode().is_some() {
336                // means it's not an error already
337                if let ProgramError::Io { source } = err {
338                    // we mutate the preview to Preview::IOError
339                    self.preview = Preview::IoError(source);
340                    return self.display(w, disc);
341                }
342            }
343            return Err(err);
344        }
345        Ok(())
346    }
347
348    fn no_verb_status(
349        &self,
350        has_previous_state: bool,
351        con: &AppContext,
352        width: usize, // available width
353    ) -> Status {
354        let mut ssb =
355            con.standard_status
356                .builder(PanelStateType::Preview, self.no_opt_selection(), width);
357        ssb.has_previous_state = has_previous_state;
358        ssb.is_filtered = self.filtered_preview.is_some();
359        ssb.has_removed_pattern = self.removed_pattern.is_some();
360        ssb.status()
361    }
362
363    fn on_internal(
364        &mut self,
365        w: &mut W,
366        invocation_parser: Option<&InvocationParser>,
367        internal_exec: &InternalExecution,
368        input_invocation: Option<&VerbInvocation>,
369        trigger_type: TriggerType,
370        app_state: &mut AppState,
371        cc: &CmdContext,
372    ) -> Result<CmdResult, ProgramError> {
373        let con = &cc.app.con;
374        match internal_exec.internal {
375            Internal::back => {
376                if self.filtered_preview.is_some() {
377                    self.on_pattern(InputPattern::none(), app_state, con)
378                } else {
379                    Ok(CmdResult::PopState)
380                }
381            }
382            Internal::copy_line => {
383                #[cfg(not(feature = "clipboard"))]
384                {
385                    Ok(CmdResult::error(
386                        "Clipboard feature not enabled at compilation",
387                    ))
388                }
389                #[cfg(feature = "clipboard")]
390                {
391                    Ok(match self.mut_preview().get_selected_line() {
392                        Some(line) => match terminal_clipboard::set_string(line) {
393                            Ok(()) => CmdResult::Keep,
394                            Err(_) => CmdResult::error("Clipboard error while copying path"),
395                        },
396                        None => CmdResult::error("No selected line in preview"),
397                    })
398                }
399            }
400            Internal::line_down => {
401                let count = get_arg(input_invocation, internal_exec, 1);
402                self.mut_preview().move_selection(count, true);
403                Ok(CmdResult::Keep)
404            }
405            Internal::line_up => {
406                let count = get_arg(input_invocation, internal_exec, 1);
407                self.mut_preview().move_selection(-count, true);
408                Ok(CmdResult::Keep)
409            }
410            Internal::line_down_no_cycle => {
411                let count = get_arg(input_invocation, internal_exec, 1);
412                self.mut_preview().move_selection(count, false);
413                Ok(CmdResult::Keep)
414            }
415            Internal::line_up_no_cycle => {
416                let count = get_arg(input_invocation, internal_exec, 1);
417                self.mut_preview().move_selection(-count, false);
418                Ok(CmdResult::Keep)
419            }
420            Internal::page_down => {
421                self.mut_preview().try_scroll(ScrollCommand::Pages(1));
422                Ok(CmdResult::Keep)
423            }
424            Internal::page_up => {
425                self.mut_preview().try_scroll(ScrollCommand::Pages(-1));
426                Ok(CmdResult::Keep)
427            }
428            //Internal::restore_pattern => {
429            //    debug!("restore_pattern");
430            //    self.pending_pattern = self.removed_pattern.take();
431            //    Ok(CmdResult::Keep)
432            //}
433            Internal::panel_left if self.removed_pattern.is_some() => {
434                self.pending_pattern = self.removed_pattern.take();
435                Ok(CmdResult::Keep)
436            }
437            Internal::panel_left_no_open if self.removed_pattern.is_some() => {
438                self.pending_pattern = self.removed_pattern.take();
439                Ok(CmdResult::Keep)
440            }
441            Internal::panel_right if self.filtered_preview.is_some() => {
442                self.on_pattern(InputPattern::none(), app_state, con)
443            }
444            Internal::panel_right_no_open if self.filtered_preview.is_some() => {
445                self.on_pattern(InputPattern::none(), app_state, con)
446            }
447            Internal::select_first => {
448                self.mut_preview().select_first();
449                Ok(CmdResult::Keep)
450            }
451            Internal::select_last => {
452                self.mut_preview().select_last();
453                Ok(CmdResult::Keep)
454            }
455            Internal::previous_match => {
456                self.mut_preview().previous_match();
457                Ok(CmdResult::Keep)
458            }
459            Internal::next_match => {
460                self.mut_preview().next_match();
461                Ok(CmdResult::Keep)
462            }
463            Internal::preview_image => self.set_mode(PreviewMode::Image, con),
464            Internal::preview_text => self.set_mode(PreviewMode::Text, con),
465            Internal::preview_tty => self.set_mode(PreviewMode::Tty, con),
466            Internal::preview_binary => self.set_mode(PreviewMode::Hex, con),
467            _ => self.on_internal_generic(
468                w,
469                invocation_parser,
470                internal_exec,
471                input_invocation,
472                trigger_type,
473                app_state,
474                cc,
475            ),
476        }
477    }
478
479    fn get_flags(&self) -> Vec<Flag> {
480        vec![]
481    }
482
483    fn get_starting_input(&self) -> String {
484        if let Some(preview) = &self.filtered_preview {
485            preview.pattern().raw
486        } else {
487            self.pending_pattern.raw.clone()
488        }
489    }
490}