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