use super::{ filetree::{FileTreeItem, FileTreeItemKind}, statustree::{MoveSelection, StatusTree}, CommandBlocking, DrawableComponent, }; use crate::{ components::{CommandInfo, Component}, keys, queue::{InternalEvent, NeedsUpdate, Queue, ResetItem}, strings, ui, }; use asyncgit::{hash, sync, StatusItem, StatusItemType, CWD}; use crossterm::event::Event; use std::{borrow::Cow, convert::From, path::Path}; use strings::commands; use tui::{ backend::Backend, layout::Rect, style::{Color, Style}, widgets::Text, Frame, }; /// pub struct ChangesComponent { title: String, tree: StatusTree, current_hash: u64, focused: bool, show_selection: bool, is_working_dir: bool, queue: Queue, } impl ChangesComponent { /// pub fn new( title: &str, focus: bool, is_working_dir: bool, queue: Queue, ) -> Self { Self { title: title.to_string(), tree: StatusTree::default(), current_hash: 0, focused: focus, show_selection: focus, is_working_dir, queue, } } /// pub fn update(&mut self, list: &[StatusItem]) { let new_hash = hash(list); if self.current_hash != new_hash { self.tree.update(list); self.current_hash = new_hash; } } /// pub fn selection(&self) -> Option { self.tree.selected_item() } /// pub fn focus_select(&mut self, focus: bool) { self.focus(focus); self.show_selection = focus; } /// returns true if list is empty pub fn is_empty(&self) -> bool { self.tree.is_empty() } /// pub fn is_file_seleted(&self) -> bool { if let Some(item) = self.tree.selected_item() { match item.kind { FileTreeItemKind::File(_) => true, _ => false, } } else { false } } fn move_selection(&mut self, dir: MoveSelection) -> bool { let changed = self.tree.move_selection(dir); if changed { self.queue .borrow_mut() .push_back(InternalEvent::Update(NeedsUpdate::DIFF)); } changed } fn index_add_remove(&mut self) -> bool { if let Some(tree_item) = self.selection() { if self.is_working_dir { if let FileTreeItemKind::File(i) = tree_item.kind { if let Some(status) = i.status { let path = Path::new(i.path.as_str()); return match status { StatusItemType::Deleted => { sync::stage_addremoved(CWD, path) } _ => sync::stage_add_file(CWD, path), }; } } else { //TODO: check if we can handle the one file case with it aswell return sync::stage_add_all( CWD, tree_item.info.full_path.as_str(), ); } } else { let path = Path::new(tree_item.info.full_path.as_str()); return sync::reset_stage(CWD, path); } } false } fn dispatch_reset_workdir(&mut self) -> bool { if let Some(tree_item) = self.selection() { let is_folder = matches!(tree_item.kind, FileTreeItemKind::Path(_)); self.queue.borrow_mut().push_back( InternalEvent::ConfirmResetItem(ResetItem { path: tree_item.info.full_path, is_folder, }), ); return true; } false } fn item_to_text( item: &FileTreeItem, width: u16, selected: bool, ) -> Option { let select_color = Color::Rgb(0, 0, 100); let indent_str = if item.info.indent == 0 { String::from("") } else { format!("{:w$}", " ", w = (item.info.indent as usize) * 2) }; if !item.info.visible { return None; } match &item.kind { FileTreeItemKind::File(status_item) => { let status_char = Self::item_status_char(status_item.status); let file = Path::new(&status_item.path) .file_name() .unwrap() .to_str() .unwrap(); let txt = if selected { format!( "{} {}{:w$}", status_char, indent_str, file, w = width as usize ) } else { format!("{} {}{}", status_char, indent_str, file) }; let mut style = Style::default().fg(Self::item_color( status_item .status .unwrap_or(StatusItemType::Modified), )); if selected { style = style.bg(select_color); } Some(Text::Styled(Cow::from(txt), style)) } FileTreeItemKind::Path(path_collapsed) => { let collapse_char = if path_collapsed.0 { '▸' } else { '▾' }; let txt = if selected { format!( " {}{}{:w$}", indent_str, collapse_char, item.info.path, w = width as usize ) } else { format!( " {}{}{}", indent_str, collapse_char, item.info.path, ) }; let mut style = Style::default(); if selected { style = style.bg(select_color); } Some(Text::Styled(Cow::from(txt), style)) } } } fn item_color(item_type: StatusItemType) -> Color { match item_type { StatusItemType::Modified => Color::LightYellow, StatusItemType::New => Color::LightGreen, StatusItemType::Deleted => Color::LightRed, StatusItemType::Renamed => Color::LightMagenta, _ => Color::White, } } fn item_status_char(item_type: Option) -> char { if let Some(item_type) = item_type { match item_type { StatusItemType::Modified => 'M', StatusItemType::New => '+', StatusItemType::Deleted => '-', StatusItemType::Renamed => 'R', _ => ' ', } } else { ' ' } } } impl DrawableComponent for ChangesComponent { fn draw(&self, f: &mut Frame, r: Rect) { let selection_offset = self.tree.tree.items().iter().enumerate().fold( 0, |acc, (idx, e)| { let visible = e.info.visible; let index_above_select = idx < self.tree.selection.unwrap_or(0); if !visible && index_above_select { acc + 1 } else { acc } }, ); let items = self.tree.tree.items().iter().enumerate().filter_map( |(idx, e)| { Self::item_to_text( e, r.width, self.show_selection && self .tree .selection .map_or(false, |e| e == idx), ) }, ); ui::draw_list( f, r, &self.title.to_string(), items, self.tree.selection.map(|idx| idx - selection_offset), self.focused, ); } } impl Component for ChangesComponent { fn commands( &self, out: &mut Vec, _force_all: bool, ) -> CommandBlocking { let some_selection = self.selection().is_some(); if self.is_working_dir { out.push(CommandInfo::new( commands::STAGE_ITEM, some_selection, self.focused, )); out.push(CommandInfo::new( commands::RESET_ITEM, some_selection, self.focused, )); } else { out.push(CommandInfo::new( commands::UNSTAGE_ITEM, some_selection, self.focused, )); } out.push(CommandInfo::new( commands::NAVIGATE_TREE, !self.is_empty(), self.focused, )); CommandBlocking::PassingOn } fn event(&mut self, ev: Event) -> bool { if self.focused { if let Event::Key(e) = ev { return match e { keys::STATUS_STAGE_FILE => { if self.index_add_remove() { self.queue.borrow_mut().push_back( InternalEvent::Update( NeedsUpdate::ALL, ), ); } true } keys::STATUS_RESET_FILE => { self.is_working_dir && self.dispatch_reset_workdir() } keys::MOVE_DOWN => { self.move_selection(MoveSelection::Down) } keys::MOVE_UP => { self.move_selection(MoveSelection::Up) } keys::HOME => { self.move_selection(MoveSelection::Home) } keys::SHIFT_UP => { self.move_selection(MoveSelection::Home) } keys::MOVE_LEFT => { self.move_selection(MoveSelection::Left) } keys::MOVE_RIGHT => { self.move_selection(MoveSelection::Right) } _ => false, }; } } false } fn focused(&self) -> bool { self.focused } fn focus(&mut self, focus: bool) { self.focused = focus } }