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