gitui/src/components/changes.rs

387 lines
11 KiB
Rust

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<FileTreeItem> {
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<Text> {
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<StatusItemType>) -> 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<B: Backend>(&self, f: &mut Frame<B>, 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<CommandInfo>,
_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
}
}