Skip to main content

broot/filesystems/
filesystems_state.rs

1use {
2    super::*,
3    crate::{
4        app::*,
5        browser::BrowserState,
6        command::*,
7        display::*,
8        errors::ProgramError,
9        pattern::*,
10        task_sync::Dam,
11        tree::TreeOptions,
12        verb::*,
13    },
14    crokey::crossterm::{
15        QueueableCommand,
16        cursor,
17        style::Color,
18    },
19    lfs_core::{
20        DeviceId,
21        Mount,
22    },
23    std::{
24        convert::TryInto,
25        path::Path,
26    },
27    strict::NonEmptyVec,
28    termimad::{
29        minimad::Alignment,
30        *,
31    },
32};
33
34struct FilteredContent {
35    pattern: Pattern,
36    mounts: Vec<Mount>, // may be empty
37    selection_idx: usize,
38}
39
40/// an application state showing the currently mounted filesystems
41pub struct FilesystemState {
42    mounts: NonEmptyVec<Mount>,
43    selection_idx: usize,
44    scroll: usize,
45    page_height: usize,
46    tree_options: TreeOptions,
47    filtered: Option<FilteredContent>,
48    mode: Mode,
49}
50
51impl FilesystemState {
52    /// create a state listing the filesystem, trying to select
53    /// the one containing the path given in argument.
54    /// Not finding any filesystem is considered an error and prevents
55    /// the opening of this state.
56    pub fn new(
57        path: Option<&Path>,
58        tree_options: TreeOptions,
59        con: &AppContext,
60    ) -> Result<FilesystemState, ProgramError> {
61        let mut mount_list = MOUNTS.lock().unwrap();
62        let show_only_disks = false;
63        let mounts = mount_list
64            .load()?
65            .iter()
66            .filter(|mount| {
67                if show_only_disks {
68                    mount.disk.is_some()
69                } else {
70                    mount.stats().is_some()
71                }
72            })
73            .cloned()
74            .collect::<Vec<Mount>>();
75        let mounts: NonEmptyVec<Mount> = match mounts.try_into() {
76            Ok(nev) => nev,
77            _ => {
78                return Err(ProgramError::Lfs {
79                    details: "no disk in lfs-core list".to_string(),
80                });
81            }
82        };
83        let selection_idx = path
84            .and_then(|path| DeviceId::of_path(path).ok())
85            .and_then(|device_id| {
86                mounts.iter().position(|m| m.info.dev == device_id)
87            })
88            .unwrap_or(0);
89        Ok(FilesystemState {
90            mounts,
91            selection_idx,
92            scroll: 0,
93            page_height: 0,
94            tree_options,
95            filtered: None,
96            mode: con.initial_mode(),
97        })
98    }
99    pub fn count(&self) -> usize {
100        self.filtered
101            .as_ref()
102            .map(|f| f.mounts.len())
103            .unwrap_or_else(|| self.mounts.len().into())
104    }
105    pub fn try_scroll(
106        &mut self,
107        cmd: ScrollCommand,
108    ) -> bool {
109        let old_scroll = self.scroll;
110        self.scroll = cmd.apply(self.scroll, self.count(), self.page_height);
111        if self.selection_idx < self.scroll {
112            self.selection_idx = self.scroll;
113        } else if self.selection_idx >= self.scroll + self.page_height {
114            self.selection_idx = self.scroll + self.page_height - 1;
115        }
116        self.scroll != old_scroll
117    }
118
119    /// change the selection
120    fn move_line(
121        &mut self,
122        internal_exec: &InternalExecution,
123        input_invocation: Option<&VerbInvocation>,
124        dir: i32, // -1 for up, 1 for down
125        cycle: bool,
126    ) -> CmdResult {
127        let count = get_arg(input_invocation, internal_exec, 1);
128        let dir = dir * count;
129        if let Some(f) = self.filtered.as_mut() {
130            f.selection_idx = move_sel(f.selection_idx, f.mounts.len(), dir, cycle);
131        } else {
132            self.selection_idx = move_sel(self.selection_idx, self.mounts.len().get(), dir, cycle);
133        }
134        if self.selection_idx < self.scroll {
135            self.scroll = self.selection_idx;
136        } else if self.selection_idx >= self.scroll + self.page_height {
137            self.scroll = self.selection_idx + 1 - self.page_height;
138        }
139        CmdResult::Keep
140    }
141
142    fn no_opt_selected_path(&self) -> &Path {
143        &self.mounts[self.selection_idx].info.mount_point
144    }
145
146    fn no_opt_selection(&self) -> Selection<'_> {
147        Selection {
148            path: self.no_opt_selected_path(),
149            stype: SelectionType::Directory,
150            is_exe: false,
151            line: 0,
152        }
153    }
154}
155
156impl PanelState for FilesystemState {
157    fn get_type(&self) -> PanelStateType {
158        PanelStateType::Fs
159    }
160
161    fn set_mode(
162        &mut self,
163        mode: Mode,
164    ) {
165        self.mode = mode;
166    }
167
168    fn get_mode(&self) -> Mode {
169        self.mode
170    }
171
172    fn selected_path(&self) -> Option<&Path> {
173        Some(self.no_opt_selected_path())
174    }
175
176    fn tree_options(&self) -> TreeOptions {
177        self.tree_options.clone()
178    }
179
180    fn with_new_options(
181        &mut self,
182        _screen: Screen,
183        change_options: &dyn Fn(&mut TreeOptions) -> &'static str,
184        _in_new_panel: bool, // TODO open tree if true
185        _con: &AppContext,
186    ) -> CmdResult {
187        change_options(&mut self.tree_options);
188        CmdResult::Keep
189    }
190
191    fn selection(&self) -> Option<Selection<'_>> {
192        Some(self.no_opt_selection())
193    }
194
195    fn refresh(
196        &mut self,
197        _screen: Screen,
198        _con: &AppContext,
199    ) -> Command {
200        Command::empty()
201    }
202
203    fn on_pattern(
204        &mut self,
205        pattern: InputPattern,
206        _app_state: &AppState,
207        _con: &AppContext,
208    ) -> Result<CmdResult, ProgramError> {
209        if pattern.is_none() {
210            self.filtered = None;
211        } else {
212            let mut selection_idx = 0;
213            let mut mounts = Vec::new();
214            let pattern = pattern.pattern;
215            for (idx, mount) in self.mounts.iter().enumerate() {
216                if pattern.score_of_string(&mount.info.fs).is_none()
217                    && mount
218                        .disk
219                        .as_ref()
220                        .and_then(|d| pattern.score_of_string(d.disk_type()))
221                        .is_none()
222                    && pattern.score_of_string(&mount.info.fs_type).is_none()
223                    && pattern
224                        .score_of_string(&mount.info.mount_point.to_string_lossy())
225                        .is_none()
226                {
227                    continue;
228                }
229                if idx <= self.selection_idx {
230                    selection_idx = mounts.len();
231                }
232                mounts.push(mount.clone());
233            }
234            self.filtered = Some(FilteredContent {
235                pattern,
236                mounts,
237                selection_idx,
238            });
239        }
240        Ok(CmdResult::Keep)
241    }
242
243    fn display(
244        &mut self,
245        w: &mut W,
246        disc: &DisplayContext,
247    ) -> Result<(), ProgramError> {
248        let area = &disc.state_area;
249        let con = &disc.con;
250        self.page_height = area.height as usize - 2;
251        let (mounts, selection_idx) = if let Some(filtered) = &self.filtered {
252            (filtered.mounts.as_slice(), filtered.selection_idx)
253        } else {
254            (self.mounts.as_slice(), self.selection_idx)
255        };
256        let scrollbar = area.scrollbar(self.scroll, mounts.len());
257        //- style preparation
258        let styles = &disc.panel_skin.styles;
259        let selection_bg = styles
260            .selected_line
261            .get_bg()
262            .unwrap_or(Color::AnsiValue(240));
263        let match_style = &styles.char_match;
264        let mut selected_match_style = styles.char_match.clone();
265        selected_match_style.set_bg(selection_bg);
266        let border_style = &styles.help_table_border;
267        let mut selected_border_style = styles.help_table_border.clone();
268        selected_border_style.set_bg(selection_bg);
269        //- width computations and selection of columns to display
270        let width = area.width as usize;
271        let w_fs = mounts
272            .iter()
273            .map(|m| m.info.fs.chars().count())
274            .max()
275            .unwrap_or(0)
276            .max("filesystem".len());
277        let mut wc_fs = w_fs; // width of the column (may include selection mark)
278        if con.show_selection_mark {
279            wc_fs += 1;
280        }
281        let w_dsk = 5; // max width of a lfs-core disk type
282        let w_type = mounts
283            .iter()
284            .map(|m| m.info.fs_type.chars().count())
285            .max()
286            .unwrap_or(0)
287            .max("type".len());
288        let w_size = 4;
289        let w_use = 4;
290        let mut w_use_bar = 1; // min size, may grow if space available
291        let w_use_share = 4;
292        let mut wc_use = w_use; // sum of all the parts of the usage column
293        let w_free = 4;
294        let w_mount_point = mounts
295            .iter()
296            .map(|m| m.info.mount_point.to_string_lossy().chars().count())
297            .max()
298            .unwrap_or(0)
299            .max("mount point".len());
300        let w_mandatory = wc_fs + 1 + w_size + 1 + w_free + 1 + w_mount_point;
301        let mut e_dsk = false;
302        let mut e_type = false;
303        let mut e_use_bar = false;
304        let mut e_use_share = false;
305        let mut e_use = false;
306        if w_mandatory + 1 < width {
307            let mut rem = width - w_mandatory - 1;
308            if rem > w_use {
309                rem -= w_use + 1;
310                e_use = true;
311            }
312            if e_use && rem > w_use_share {
313                rem -= w_use_share; // no separation with use
314                e_use_share = true;
315                wc_use += w_use_share;
316            }
317            if rem > w_dsk {
318                rem -= w_dsk + 1;
319                e_dsk = true;
320            }
321            if e_use && rem > w_use_bar {
322                rem -= w_use_bar + 1;
323                e_use_bar = true;
324                wc_use += w_use_bar + 1;
325            }
326            if rem > w_type {
327                rem -= w_type + 1;
328                e_type = true;
329            }
330            if e_use_bar && rem > 0 {
331                let incr = rem.min(9);
332                w_use_bar += incr;
333                wc_use += incr;
334            }
335        }
336        //- titles
337        w.queue(cursor::MoveTo(area.left, area.top))?;
338        let mut cw = CropWriter::new(w, width);
339        cw.queue_g_string(&styles.default, format!("{:wc_fs$}", "filesystem"))?;
340        cw.queue_char(border_style, '│')?;
341        if e_dsk {
342            cw.queue_g_string(&styles.default, "disk ".to_string())?;
343            cw.queue_char(border_style, '│')?;
344        }
345        if e_type {
346            cw.queue_g_string(&styles.default, format!("{:^w_type$}", "type"))?;
347            cw.queue_char(border_style, '│')?;
348        }
349        if e_use {
350            cw.queue_g_string(
351                &styles.default,
352                format!(
353                    "{:^width$}",
354                    if wc_use > 4 { "usage" } else { "use" },
355                    width = wc_use
356                ),
357            )?;
358            cw.queue_char(border_style, '│')?;
359        }
360        cw.queue_g_string(&styles.default, "free".to_string())?;
361        cw.queue_char(border_style, '│')?;
362        cw.queue_g_string(&styles.default, "size".to_string())?;
363        cw.queue_char(border_style, '│')?;
364        cw.queue_g_string(&styles.default, "mount point".to_string())?;
365        cw.fill(border_style, &SPACE_FILLING)?;
366        //- horizontal line
367        w.queue(cursor::MoveTo(area.left, 1 + area.top))?;
368        let mut cw = CropWriter::new(w, width);
369        cw.queue_g_string(border_style, format!("{:─>width$}", '┼', width = wc_fs + 1))?;
370        if e_dsk {
371            cw.queue_g_string(border_style, format!("{:─>width$}", '┼', width = w_dsk + 1))?;
372        }
373        if e_type {
374            cw.queue_g_string(
375                border_style,
376                format!("{:─>width$}", '┼', width = w_type + 1),
377            )?;
378        }
379        cw.queue_g_string(
380            border_style,
381            format!("{:─>width$}", '┼', width = w_size + 1),
382        )?;
383        if e_use {
384            cw.queue_g_string(
385                border_style,
386                format!("{:─>width$}", '┼', width = wc_use + 1),
387            )?;
388        }
389        cw.queue_g_string(
390            border_style,
391            format!("{:─>width$}", '┼', width = w_free + 1),
392        )?;
393        cw.fill(border_style, &BRANCH_FILLING)?;
394        //- content
395        let mut idx = self.scroll;
396        for y in 2..area.height {
397            w.queue(cursor::MoveTo(area.left, y + area.top))?;
398            let selected = selection_idx == idx;
399            let mut cw = CropWriter::new(w, width - 1); // -1 for scrollbar
400            let txt_style = if selected {
401                &styles.selected_line
402            } else {
403                &styles.default
404            };
405            if let Some(mount) = mounts.get(idx) {
406                let match_style = if selected {
407                    &selected_match_style
408                } else {
409                    match_style
410                };
411                let border_style = if selected {
412                    &selected_border_style
413                } else {
414                    border_style
415                };
416                if con.show_selection_mark {
417                    cw.queue_char(txt_style, if selected { '▶' } else { ' ' })?;
418                }
419                // fs
420                let s = &mount.info.fs;
421                let mut matched_string = MatchedString::new(
422                    self.filtered
423                        .as_ref()
424                        .and_then(|f| f.pattern.search_string(s)),
425                    s,
426                    txt_style,
427                    match_style,
428                );
429                matched_string.fill(w_fs, Alignment::Left);
430                matched_string.queue_on(&mut cw)?;
431                cw.queue_char(border_style, '│')?;
432                // dsk
433                if e_dsk {
434                    if let Some(disk) = mount.disk.as_ref() {
435                        let s = disk.disk_type();
436                        let mut matched_string = MatchedString::new(
437                            self.filtered
438                                .as_ref()
439                                .and_then(|f| f.pattern.search_string(s)),
440                            s,
441                            txt_style,
442                            match_style,
443                        );
444                        matched_string.fill(5, Alignment::Center);
445                        matched_string.queue_on(&mut cw)?;
446                    } else {
447                        cw.queue_g_string(txt_style, "     ".to_string())?;
448                    }
449                    cw.queue_char(border_style, '│')?;
450                }
451                // type
452                if e_type {
453                    let s = &mount.info.fs_type;
454                    let mut matched_string = MatchedString::new(
455                        self.filtered
456                            .as_ref()
457                            .and_then(|f| f.pattern.search_string(s)),
458                        s,
459                        txt_style,
460                        match_style,
461                    );
462                    matched_string.fill(w_type, Alignment::Center);
463                    matched_string.queue_on(&mut cw)?;
464                    cw.queue_char(border_style, '│')?;
465                }
466                // size, used, free
467                if let Some(stats) = mount.stats().filter(|s| s.size() > 0) {
468                    let share_color = styles.good_to_bad_color(stats.use_share());
469                    // used
470                    if e_use {
471                        cw.queue_g_string(
472                            txt_style,
473                            format!("{:>4}", file_size::fit_4(stats.used())),
474                        )?;
475                        if e_use_share {
476                            cw.queue_g_string(
477                                txt_style,
478                                format!("{:>3.0}%", 100.0 * stats.use_share()),
479                            )?;
480                        }
481                        if e_use_bar {
482                            cw.queue_char(txt_style, ' ')?;
483                            let pb = ProgressBar::new(stats.use_share() as f32, w_use_bar);
484                            let mut bar_style = styles.default.clone();
485                            bar_style.set_bg(share_color);
486                            cw.queue_g_string(&bar_style, format!("{pb:<w_use_bar$}"))?;
487                        }
488                        cw.queue_char(border_style, '│')?;
489                    }
490                    // free
491                    let mut share_style = txt_style.clone();
492                    share_style.set_fg(share_color);
493                    cw.queue_g_string(
494                        &share_style,
495                        format!("{:>4}", file_size::fit_4(stats.available())),
496                    )?;
497                    cw.queue_char(border_style, '│')?;
498                    // size
499                    if let Some(stats) = mount.stats() {
500                        cw.queue_g_string(
501                            txt_style,
502                            format!("{:>4}", file_size::fit_4(stats.size())),
503                        )?;
504                    } else {
505                        cw.repeat(txt_style, &SPACE_FILLING, 4)?;
506                    }
507                    cw.queue_char(border_style, '│')?;
508                } else {
509                    // used
510                    if e_use {
511                        cw.repeat(txt_style, &SPACE_FILLING, wc_use)?;
512                        cw.queue_char(border_style, '│')?;
513                    }
514                    // free
515                    cw.repeat(txt_style, &SPACE_FILLING, w_free)?;
516                    cw.queue_char(border_style, '│')?;
517                    // size
518                    cw.repeat(txt_style, &SPACE_FILLING, w_size)?;
519                    cw.queue_char(border_style, '│')?;
520                }
521                // mount point
522                let s = &mount.info.mount_point.to_string_lossy();
523                let matched_string = MatchedString::new(
524                    self.filtered
525                        .as_ref()
526                        .and_then(|f| f.pattern.search_string(s)),
527                    s,
528                    txt_style,
529                    match_style,
530                );
531                matched_string.queue_on(&mut cw)?;
532                idx += 1;
533            }
534            cw.fill(txt_style, &SPACE_FILLING)?;
535            let scrollbar_style = if ScrollCommand::is_thumb(y, scrollbar) {
536                &styles.scrollbar_thumb
537            } else {
538                &styles.scrollbar_track
539            };
540            scrollbar_style.queue_str(w, "▐")?;
541        }
542        Ok(())
543    }
544
545    fn on_internal(
546        &mut self,
547        w: &mut W,
548        invocation_parser: Option<&InvocationParser>,
549        internal_exec: &InternalExecution,
550        input_invocation: Option<&VerbInvocation>,
551        trigger_type: TriggerType,
552        app_state: &mut AppState,
553        cc: &CmdContext,
554    ) -> Result<CmdResult, ProgramError> {
555        let screen = cc.app.screen;
556        let con = &cc.app.con;
557        use Internal::*;
558        Ok(match internal_exec.internal {
559            Internal::back => {
560                if let Some(f) = self.filtered.take() {
561                    if !f.mounts.is_empty() {
562                        self.selection_idx = self
563                            .mounts
564                            .iter()
565                            .position(|m| m.info.id == f.mounts[f.selection_idx].info.id)
566                            .unwrap(); // all filtered mounts come from self.mounts
567                    }
568                    CmdResult::Keep
569                } else {
570                    CmdResult::PopState
571                }
572            }
573            Internal::line_down => self.move_line(internal_exec, input_invocation, 1, true),
574            Internal::line_up => self.move_line(internal_exec, input_invocation, -1, true),
575            Internal::line_down_no_cycle => {
576                self.move_line(internal_exec, input_invocation, 1, false)
577            }
578            Internal::line_up_no_cycle => {
579                self.move_line(internal_exec, input_invocation, -1, false)
580            }
581            Internal::open_stay => {
582                let in_new_panel = input_invocation
583                    .map(|inv| inv.bang)
584                    .unwrap_or(internal_exec.bang);
585                let dam = Dam::unlimited();
586                let mut tree_options = self.tree_options();
587                tree_options.show_root_fs = true;
588                CmdResult::from_optional_browser_state(
589                    BrowserState::new(
590                        self.no_opt_selected_path().to_path_buf(),
591                        tree_options,
592                        screen,
593                        con,
594                        &dam,
595                    ),
596                    None,
597                    in_new_panel,
598                )
599            }
600            Internal::panel_left => {
601                let areas = &cc.panel.areas;
602                if areas.is_first() && areas.nb_pos < con.max_panels_count {
603                    // we ask for the creation of a panel to the left
604                    internal_focus::new_panel_on_path(
605                        self.no_opt_selected_path().to_path_buf(),
606                        screen,
607                        self.tree_options(),
608                        PanelPurpose::None,
609                        con,
610                        HDir::Left,
611                    )
612                } else {
613                    // we ask the app to focus the panel to the left
614                    CmdResult::HandleInApp(Internal::panel_left_no_open)
615                }
616            }
617            Internal::panel_left_no_open => CmdResult::HandleInApp(Internal::panel_left_no_open),
618            Internal::panel_right => {
619                let areas = &cc.panel.areas;
620                if areas.is_last() && areas.nb_pos < con.max_panels_count {
621                    // we ask for the creation of a panel to the right
622                    internal_focus::new_panel_on_path(
623                        self.no_opt_selected_path().to_path_buf(),
624                        screen,
625                        self.tree_options(),
626                        PanelPurpose::None,
627                        con,
628                        HDir::Right,
629                    )
630                } else {
631                    // we ask the app to focus the panel to the right
632                    CmdResult::HandleInApp(Internal::panel_right_no_open)
633                }
634            }
635            Internal::panel_right_no_open => CmdResult::HandleInApp(Internal::panel_right_no_open),
636            Internal::page_down => {
637                if !self.try_scroll(ScrollCommand::Pages(1)) {
638                    self.selection_idx = self.count() - 1;
639                }
640                CmdResult::Keep
641            }
642            Internal::page_up => {
643                if !self.try_scroll(ScrollCommand::Pages(-1)) {
644                    self.selection_idx = 0;
645                }
646                CmdResult::Keep
647            }
648            open_leave => CmdResult::PopStateAndReapply,
649            _ => self.on_internal_generic(
650                w,
651                invocation_parser,
652                internal_exec,
653                input_invocation,
654                trigger_type,
655                app_state,
656                cc,
657            )?,
658        })
659    }
660
661    fn on_click(
662        &mut self,
663        _x: u16,
664        y: u16,
665        _screen: Screen,
666        _con: &AppContext,
667    ) -> Result<CmdResult, ProgramError> {
668        if y >= 2 {
669            let y = y as usize - 2 + self.scroll;
670            let len: usize = self.mounts.len().into();
671            if y < len {
672                self.selection_idx = y;
673            }
674        }
675        Ok(CmdResult::Keep)
676    }
677}