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