1 use { 2 super::*, 3 crate::{ 4 app::*, 5 command::*, 6 display::{MatchedString, Screen, W}, 7 errors::ProgramError, 8 pattern::*, 9 skin::*, 10 task_sync::Dam, 11 tree::*, 12 verb::*, 13 }, 14 crossterm::{ 15 cursor, 16 QueueableCommand, 17 }, 18 std::path::{Path}, 19 termimad::{Area, CropWriter, SPACE_FILLING}, 20 unicode_width::{UnicodeWidthChar, UnicodeWidthStr}, 21 }; 22 23 static TITLE: &str = "Staging Area"; // no wide char allowed here 24 static COUNT_LABEL: &str = " count: "; 25 static SIZE_LABEL: &str = " size: "; 26 static ELLIPSIS: char = '…'; 27 28 pub struct StageState { 29 30 filtered_stage: FilteredStage, 31 32 scroll: usize, 33 34 tree_options: TreeOptions, 35 36 /// the 'modal' mode 37 mode: Mode, 38 39 page_height: usize, 40 41 stage_sum: StageSum, 42 43 } 44 45 impl StageState { 46 new( app_state: &AppState, tree_options: TreeOptions, con: &AppContext, ) -> StageState47 pub fn new( 48 app_state: &AppState, 49 tree_options: TreeOptions, 50 con: &AppContext, 51 ) -> StageState { 52 let filtered_stage = FilteredStage::filtered( 53 &app_state.stage, 54 tree_options.pattern.clone(), 55 ); 56 Self { 57 filtered_stage, 58 scroll: 0, 59 tree_options, 60 mode: initial_mode(con), 61 page_height: 0, 62 stage_sum: StageSum::default(), 63 } 64 } 65 need_sum_computation(&self) -> bool66 fn need_sum_computation(&self) -> bool { 67 self.tree_options.show_sizes && !self.stage_sum.is_up_to_date() 68 } 69 70 try_scroll( &mut self, cmd: ScrollCommand, ) -> bool71 pub fn try_scroll( 72 &mut self, 73 cmd: ScrollCommand, 74 ) -> bool { 75 let old_scroll = self.scroll; 76 self.scroll = cmd.apply(self.scroll, self.filtered_stage.len(), self.page_height); 77 self.scroll != old_scroll 78 } 79 fix_scroll(&mut self)80 pub fn fix_scroll(&mut self) { 81 let len = self.filtered_stage.len(); 82 if self.scroll + self.page_height > len { 83 self.scroll = if len > self.page_height { 84 len - self.page_height 85 } else { 86 0 87 }; 88 } 89 } 90 write_title_line( &self, stage: &Stage, cw: &mut CropWriter<'_, W>, styles: &StyleMap, ) -> Result<(), ProgramError>91 fn write_title_line( 92 &self, 93 stage: &Stage, 94 cw: &mut CropWriter<'_, W>, 95 styles: &StyleMap, 96 ) -> Result<(), ProgramError> { 97 let total_count = format!("{}", stage.len()); 98 let mut count_len = total_count.len(); 99 if self.filtered_stage.pattern().is_some() { 100 count_len += total_count.len() + 1; // 1 for '/' 101 } 102 if cw.allowed < count_len { 103 return Ok(()); 104 } 105 if TITLE.len() + 1 + count_len <= cw.allowed { 106 cw.queue_str( 107 &styles.staging_area_title, 108 TITLE, 109 )?; 110 } 111 let mut show_count_label = false; 112 let mut rem = cw.allowed - count_len; 113 if COUNT_LABEL.len() < rem { 114 rem -= COUNT_LABEL.len(); 115 show_count_label = true; 116 if self.tree_options.show_sizes { 117 if let Some(sum) = self.stage_sum.computed() { 118 let size = file_size::fit_4(sum.to_size()); 119 let size_len = SIZE_LABEL.len() + size.len(); 120 if size_len < rem { 121 rem -= size_len; 122 // we display the size in the middle, so we cut rem in two 123 let left_rem = rem / 2; 124 rem -= left_rem; 125 cw.repeat(&styles.staging_area_title, &SPACE_FILLING, left_rem)?; 126 cw.queue_g_string( 127 &styles.staging_area_title, 128 SIZE_LABEL.to_string(), 129 )?; 130 cw.queue_g_string( 131 &styles.staging_area_title, 132 size, 133 )?; 134 } 135 } 136 } 137 } 138 cw.repeat(&styles.staging_area_title, &SPACE_FILLING, rem)?; 139 if show_count_label { 140 cw.queue_g_string( 141 &styles.staging_area_title, 142 COUNT_LABEL.to_string(), 143 )?; 144 } 145 if self.filtered_stage.pattern().is_some() { 146 cw.queue_g_string( 147 &styles.char_match, 148 format!("{}", self.filtered_stage.len()), 149 )?; 150 cw.queue_char( 151 &styles.staging_area_title, 152 '/', 153 )?; 154 } 155 cw.queue_g_string( 156 &styles.staging_area_title, 157 total_count, 158 )?; 159 cw.fill(&styles.staging_area_title, &SPACE_FILLING)?; 160 Ok(()) 161 } 162 move_selection(&mut self, dy: i32, cycle: bool) -> CmdResult163 fn move_selection(&mut self, dy: i32, cycle: bool) -> CmdResult { 164 self.filtered_stage.move_selection(dy, cycle); 165 if let Some(sel) = self.filtered_stage.selection() { 166 if sel < self.scroll + 5 { 167 self.scroll = (sel as i32 -5).max(0) as usize; 168 } else if sel > self.scroll + self.page_height - 5 { 169 self.scroll = (sel + 5 - self.page_height) 170 .min(self.filtered_stage.len() - self.page_height); 171 } 172 } 173 CmdResult::Keep 174 } 175 176 } 177 178 impl PanelState for StageState { 179 get_type(&self) -> PanelStateType180 fn get_type(&self) -> PanelStateType { 181 PanelStateType::Stage 182 } 183 selected_path(&self) -> Option<&Path>184 fn selected_path(&self) -> Option<&Path> { 185 None 186 } 187 selection(&self) -> Option<Selection<'_>>188 fn selection(&self) -> Option<Selection<'_>> { 189 None 190 } 191 clear_pending(&mut self)192 fn clear_pending(&mut self) { 193 self.stage_sum.clear(); 194 } do_pending_task( &mut self, stage: &Stage, _screen: Screen, con: &AppContext, dam: &mut Dam, )195 fn do_pending_task( 196 &mut self, 197 stage: &Stage, 198 _screen: Screen, 199 con: &AppContext, 200 dam: &mut Dam, 201 // need the stage here 202 ) { 203 if self.need_sum_computation() { 204 self.stage_sum.compute(stage, dam, con); 205 } 206 } get_pending_task(&self) -> Option<&'static str>207 fn get_pending_task(&self) -> Option<&'static str> { 208 if self.need_sum_computation() { 209 Some("stage size summing") 210 } else { 211 None 212 } 213 } 214 sel_info<'c>(&'c self, app_state: &'c AppState) -> SelInfo<'c>215 fn sel_info<'c>(&'c self, app_state: &'c AppState) -> SelInfo<'c> { 216 match app_state.stage.len() { 217 0 => SelInfo::None, 218 1 => SelInfo::One(Selection { 219 path: &app_state.stage.paths()[0], 220 stype: SelectionType::File, 221 is_exe: false, 222 line: 0, 223 }), 224 _ => SelInfo::More(&app_state.stage), 225 } 226 } 227 has_at_least_one_selection(&self, app_state: &AppState) -> bool228 fn has_at_least_one_selection(&self, app_state: &AppState) -> bool { 229 !app_state.stage.is_empty() 230 } 231 tree_options(&self) -> TreeOptions232 fn tree_options(&self) -> TreeOptions { 233 self.tree_options.clone() 234 } 235 236 /// option changing is unlikely to be done on this state, but 237 /// we'll still do it in case a future scenario makes it possible 238 /// to open a different state from this state with_new_options( &mut self, _screen: Screen, change_options: &dyn Fn(&mut TreeOptions), in_new_panel: bool, con: &AppContext, ) -> CmdResult239 fn with_new_options( 240 &mut self, 241 _screen: Screen, 242 change_options: &dyn Fn(&mut TreeOptions), 243 in_new_panel: bool, 244 con: &AppContext, 245 ) -> CmdResult { 246 if in_new_panel { 247 CmdResult::error("stage can't be displayed in two panels") 248 } else { 249 let mut new_options= self.tree_options(); 250 change_options(&mut new_options); 251 CmdResult::NewState(Box::new(StageState { 252 filtered_stage: self.filtered_stage.clone(), 253 scroll: self.scroll, 254 mode: initial_mode(con), 255 tree_options: new_options, 256 page_height: self.page_height, 257 stage_sum: self.stage_sum, 258 })) 259 } 260 } 261 on_click( &mut self, _x: u16, y: u16, _screen: Screen, _con: &AppContext, ) -> Result<CmdResult, ProgramError>262 fn on_click( 263 &mut self, 264 _x: u16, 265 y: u16, 266 _screen: Screen, 267 _con: &AppContext, 268 ) -> Result<CmdResult, ProgramError> { 269 if y > 0 { 270 // the list starts on the second row 271 self.filtered_stage.try_select_idx(y as usize - 1 + self.scroll); 272 } 273 Ok(CmdResult::Keep) 274 } 275 on_pattern( &mut self, pat: InputPattern, app_state: &AppState, _con: &AppContext, ) -> Result<CmdResult, ProgramError>276 fn on_pattern( 277 &mut self, 278 pat: InputPattern, 279 app_state: &AppState, 280 _con: &AppContext, 281 ) -> Result<CmdResult, ProgramError> { 282 self.filtered_stage.set_pattern(&app_state.stage, pat); 283 self.fix_scroll(); 284 Ok(CmdResult::Keep) 285 } 286 display( &mut self, w: &mut W, disc: &DisplayContext, ) -> Result<(), ProgramError>287 fn display( 288 &mut self, 289 w: &mut W, 290 disc: &DisplayContext, 291 ) -> Result<(), ProgramError> { 292 let stage = &disc.app_state.stage; 293 self.stage_sum.see_stage(stage); // this may invalidate the sum 294 if self.filtered_stage.update(stage) { 295 self.fix_scroll(); 296 } 297 let area = &disc.state_area; 298 let styles = &disc.panel_skin.styles; 299 let width = area.width as usize; 300 w.queue(cursor::MoveTo(area.left, 0))?; 301 let mut cw = CropWriter::new(w, width); 302 self.write_title_line(stage, &mut cw, styles)?; 303 let list_area = Area::new(area.left, area.top + 1, area.width, area.height - 1); 304 self.page_height = list_area.height as usize; 305 let pattern = &self.filtered_stage.pattern().pattern; 306 let pattern_object = pattern.object(); 307 let scrollbar = list_area.scrollbar(self.scroll, self.filtered_stage.len()); 308 for idx in 0..self.page_height { 309 let y = list_area.top + idx as u16; 310 let stage_idx = idx + self.scroll; 311 w.queue(cursor::MoveTo(area.left, y))?; 312 let mut cw = CropWriter::new(w, width - 1); 313 let cw = &mut cw; 314 if let Some((path, selected)) = self.filtered_stage.path_sel(stage, stage_idx) { 315 let mut style = if path.is_dir() { 316 &styles.directory 317 } else { 318 &styles.file 319 }; 320 let mut bg_style; 321 if selected { 322 bg_style = style.clone(); 323 if let Some(c) = styles.selected_line.get_bg() { 324 bg_style.set_bg(c); 325 } 326 style = &bg_style; 327 } 328 let mut bg_style_match; 329 let mut style_match = &styles.char_match; 330 if selected { 331 bg_style_match = style_match.clone(); 332 if let Some(c) = styles.selected_line.get_bg() { 333 bg_style_match.set_bg(c); 334 } 335 style_match = &bg_style_match; 336 } 337 if disc.con.show_selection_mark && self.filtered_stage.has_selection() { 338 cw.queue_char(style, if selected { '▶' } else { ' ' })?; 339 } 340 if pattern_object.subpath { 341 let label = path.to_string_lossy(); 342 // we must display the matching on the whole path 343 // (subpath is the path for the staging area) 344 let name_match = pattern.search_string(&label); 345 let matched_string = MatchedString::new( 346 name_match, 347 &label, 348 style, 349 style_match, 350 ); 351 matched_string.queue_on(cw)?; 352 } else if let Some(file_name) = path.file_name() { 353 let label = file_name.to_string_lossy(); 354 let label_cols = label.width(); 355 if label_cols + 2 < cw.allowed { 356 if let Some(parent_path) = path.parent() { 357 let mut parent_style = &styles.parent; 358 let mut bg_style; 359 if selected { 360 bg_style = parent_style.clone(); 361 if let Some(c) = styles.selected_line.get_bg() { 362 bg_style.set_bg(c); 363 } 364 parent_style = &bg_style; 365 } 366 let cols_max = cw.allowed - label_cols - 3; 367 let parent_path = parent_path.to_string_lossy(); 368 let parent_cols = parent_path.width(); 369 if parent_cols <= cols_max { 370 cw.queue_str( 371 parent_style, 372 &parent_path, 373 )?; 374 } else { 375 // TODO move to (crop_writer ? termimad ?) 376 // we'll compute the size of the tail fitting 377 // the width minus one (for the ellipsis) 378 let mut bytes_count = 0; 379 let mut cols_count = 0; 380 for c in parent_path.chars().rev() { 381 let char_width = UnicodeWidthChar::width(c).unwrap_or(0); 382 let next_str_width = cols_count + char_width; 383 if next_str_width > cols_max { 384 break; 385 } 386 cols_count = next_str_width; 387 bytes_count += c.len_utf8(); 388 } 389 cw.queue_char( 390 parent_style, 391 ELLIPSIS, 392 )?; 393 cw.queue_str( 394 parent_style, 395 &parent_path[parent_path.len()-bytes_count..], 396 )?; 397 } 398 cw.queue_char( 399 parent_style, 400 '/', 401 )?; 402 } 403 } 404 let name_match = pattern.search_string(&label); 405 let matched_string = MatchedString::new( 406 name_match, 407 &label, 408 style, 409 style_match, 410 ); 411 matched_string.queue_on(cw)?; 412 } else { 413 // this should not happen 414 warn!("how did we fall on a path without filename?"); 415 } 416 cw.fill(style, &SPACE_FILLING)?; 417 } 418 cw.fill(&styles.default, &SPACE_FILLING)?; 419 let scrollbar_style = if ScrollCommand::is_thumb(y, scrollbar) { 420 &styles.scrollbar_thumb 421 } else { 422 &styles.scrollbar_track 423 }; 424 scrollbar_style.queue_str(w, "▐")?; 425 } 426 Ok(()) 427 } 428 refresh(&mut self, _screen: Screen, _con: &AppContext) -> Command429 fn refresh(&mut self, _screen: Screen, _con: &AppContext) -> Command { 430 Command::empty() 431 } 432 set_mode(&mut self, mode: Mode)433 fn set_mode(&mut self, mode: Mode) { 434 self.mode = mode; 435 } 436 get_mode(&self) -> Mode437 fn get_mode(&self) -> Mode { 438 self.mode 439 } 440 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>441 fn on_internal( 442 &mut self, 443 w: &mut W, 444 internal_exec: &InternalExecution, 445 input_invocation: Option<&VerbInvocation>, 446 trigger_type: TriggerType, 447 app_state: &mut AppState, 448 cc: &CmdContext, 449 ) -> Result<CmdResult, ProgramError> { 450 Ok(match internal_exec.internal { 451 Internal::back if self.filtered_stage.pattern().is_some() => { 452 self.filtered_stage = FilteredStage::unfiltered(&app_state.stage); 453 CmdResult::Keep 454 } 455 Internal::back if self.filtered_stage.has_selection() => { 456 self.filtered_stage.unselect(); 457 CmdResult::Keep 458 } 459 Internal::line_down => { 460 let count = get_arg(input_invocation, internal_exec, 1); 461 self.move_selection(count, true) 462 } 463 Internal::line_up => { 464 let count = get_arg(input_invocation, internal_exec, 1); 465 self.move_selection(-count, true) 466 } 467 Internal::line_down_no_cycle => { 468 let count = get_arg(input_invocation, internal_exec, 1); 469 self.move_selection(count, false) 470 } 471 Internal::line_up_no_cycle => { 472 let count = get_arg(input_invocation, internal_exec, 1); 473 self.move_selection(-count, false) 474 } 475 Internal::page_down => { 476 self.try_scroll(ScrollCommand::Pages(1)); 477 CmdResult::Keep 478 } 479 Internal::page_up => { 480 self.try_scroll(ScrollCommand::Pages(-1)); 481 CmdResult::Keep 482 } 483 Internal::stage => { 484 // shall we restage what we just unstaged ? 485 CmdResult::error("nothing to stage here") 486 } 487 Internal::unstage | Internal::toggle_stage => { 488 if self.filtered_stage.unstage_selection(&mut app_state.stage) { 489 CmdResult::Keep 490 } else { 491 CmdResult::error("you must select a path to unstage") 492 } 493 } 494 _ => self.on_internal_generic( 495 w, 496 internal_exec, 497 input_invocation, 498 trigger_type, 499 app_state, 500 cc, 501 )?, 502 }) 503 } 504 } 505 506