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>, selection_idx: usize,
38}
39
40pub 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 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 fn move_line(
121 &mut self,
122 internal_exec: &InternalExecution,
123 input_invocation: Option<&VerbInvocation>,
124 dir: i32, 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, _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 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 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; if con.show_selection_mark {
279 wc_fs += 1;
280 }
281 let w_dsk = 5; 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; let w_use_share = 4;
292 let mut wc_use = w_use; 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; 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 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 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 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); 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 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 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 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 if let Some(stats) = mount.stats().filter(|s| s.size() > 0) {
468 let share_color = styles.good_to_bad_color(stats.use_share());
469 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 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 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 if e_use {
511 cw.repeat(txt_style, &SPACE_FILLING, wc_use)?;
512 cw.queue_char(border_style, '│')?;
513 }
514 cw.repeat(txt_style, &SPACE_FILLING, w_free)?;
516 cw.queue_char(border_style, '│')?;
517 cw.repeat(txt_style, &SPACE_FILLING, w_size)?;
519 cw.queue_char(border_style, '│')?;
520 }
521 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(); }
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 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 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 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 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}