use super::{
status_tree::StatusTreeComponent,
utils::filetree::{FileTreeItem, FileTreeItemKind},
CommandBlocking, DrawableComponent,
};
use crate::{
components::{CommandInfo, Component, EventState},
keys::{key_match, SharedKeyConfig},
options::SharedOptions,
queue::{Action, InternalEvent, NeedsUpdate, Queue, ResetItem},
strings, try_or_popup,
ui::style::SharedTheme,
};
use anyhow::Result;
use asyncgit::{
sync::{self, RepoPathRef},
StatusItem, StatusItemType,
};
use crossterm::event::Event;
use std::path::Path;
use tui::{backend::Backend, layout::Rect, Frame};
pub struct ChangesComponent {
repo: RepoPathRef,
files: StatusTreeComponent,
is_working_dir: bool,
queue: Queue,
key_config: SharedKeyConfig,
options: SharedOptions,
}
impl ChangesComponent {
#[allow(clippy::too_many_arguments)]
pub fn new(
repo: RepoPathRef,
title: &str,
focus: bool,
is_working_dir: bool,
queue: Queue,
theme: SharedTheme,
key_config: SharedKeyConfig,
options: SharedOptions,
) -> Self {
Self {
files: StatusTreeComponent::new(
title,
focus,
Some(queue.clone()),
theme,
key_config.clone(),
),
is_working_dir,
queue,
key_config,
options,
repo,
}
}
pub fn set_items(&mut self, list: &[StatusItem]) -> Result<()> {
self.files.show()?;
self.files.update(list)?;
Ok(())
}
pub fn selection(&self) -> Option<FileTreeItem> {
self.files.selection()
}
pub fn focus_select(&mut self, focus: bool) {
self.files.focus(focus);
self.files.show_selection(focus);
}
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
pub fn is_file_seleted(&self) -> bool {
self.files.is_file_seleted()
}
fn index_add_remove(&mut self) -> Result<bool> {
if let Some(tree_item) = self.selection() {
if self.is_working_dir {
if let FileTreeItemKind::File(i) = tree_item.kind {
let path = Path::new(i.path.as_str());
match i.status {
StatusItemType::Deleted => {
sync::stage_addremoved(
&self.repo.borrow(),
path,
)?;
}
_ => sync::stage_add_file(
&self.repo.borrow(),
path,
)?,
};
} else {
let config =
self.options.borrow().status_show_untracked();
sync::stage_add_all(
&self.repo.borrow(),
tree_item.info.full_path.as_str(),
config,
)?;
}
if sync::is_workdir_clean(
&self.repo.borrow(),
self.options.borrow().status_show_untracked(),
)? {
self.queue
.push(InternalEvent::StatusLastFileMoved);
}
} else {
let path = tree_item.info.full_path.as_str();
sync::reset_stage(&self.repo.borrow(), path)?;
}
return Ok(true);
}
Ok(false)
}
fn index_add_all(&mut self) -> Result<()> {
let config = self.options.borrow().status_show_untracked();
sync::stage_add_all(&self.repo.borrow(), "*", config)?;
self.queue.push(InternalEvent::Update(NeedsUpdate::ALL));
Ok(())
}
fn stage_remove_all(&mut self) -> Result<()> {
sync::reset_stage(&self.repo.borrow(), "*")?;
self.queue.push(InternalEvent::Update(NeedsUpdate::ALL));
Ok(())
}
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.push(InternalEvent::ConfirmAction(
Action::Reset(ResetItem {
path: tree_item.info.full_path,
is_folder,
}),
));
return true;
}
false
}
fn add_to_ignore(&mut self) -> bool {
if let Some(tree_item) = self.selection() {
if let Err(e) = sync::add_to_ignore(
&self.repo.borrow(),
&tree_item.info.full_path,
) {
self.queue.push(InternalEvent::ShowErrorMsg(
format!(
"ignore error:\n{}\nfile:\n{:?}",
e, tree_item.info.full_path
),
));
} else {
self.queue
.push(InternalEvent::Update(NeedsUpdate::ALL));
return true;
}
}
false
}
}
impl DrawableComponent for ChangesComponent {
fn draw<B: Backend>(
&self,
f: &mut Frame<B>,
r: Rect,
) -> Result<()> {
self.files.draw(f, r)?;
Ok(())
}
}
impl Component for ChangesComponent {
fn commands(
&self,
out: &mut Vec<CommandInfo>,
force_all: bool,
) -> CommandBlocking {
self.files.commands(out, force_all);
let some_selection = self.selection().is_some();
if self.is_working_dir {
out.push(CommandInfo::new(
strings::commands::stage_all(&self.key_config),
true,
some_selection && self.focused(),
));
out.push(CommandInfo::new(
strings::commands::stage_item(&self.key_config),
true,
some_selection && self.focused(),
));
out.push(CommandInfo::new(
strings::commands::reset_item(&self.key_config),
true,
some_selection && self.focused(),
));
out.push(CommandInfo::new(
strings::commands::ignore_item(&self.key_config),
true,
some_selection && self.focused(),
));
} else {
out.push(CommandInfo::new(
strings::commands::unstage_item(&self.key_config),
true,
some_selection && self.focused(),
));
out.push(CommandInfo::new(
strings::commands::unstage_all(&self.key_config),
true,
some_selection && self.focused(),
));
}
CommandBlocking::PassingOn
}
fn event(&mut self, ev: &Event) -> Result<EventState> {
if self.files.event(ev)?.is_consumed() {
return Ok(EventState::Consumed);
}
if self.focused() {
if let Event::Key(e) = ev {
return if key_match(
e,
self.key_config.keys.stage_unstage_item,
) {
try_or_popup!(
self,
"staging error:",
self.index_add_remove()
);
self.queue.push(InternalEvent::Update(
NeedsUpdate::ALL,
));
Ok(EventState::Consumed)
} else if key_match(
e,
self.key_config.keys.status_stage_all,
) && !self.is_empty()
{
if self.is_working_dir {
try_or_popup!(
self,
"staging all error:",
self.index_add_all()
);
} else {
self.stage_remove_all()?;
}
self.queue
.push(InternalEvent::StatusLastFileMoved);
Ok(EventState::Consumed)
} else if key_match(
e,
self.key_config.keys.status_reset_item,
) && self.is_working_dir
{
Ok(self.dispatch_reset_workdir().into())
} else if key_match(
e,
self.key_config.keys.status_ignore_file,
) && self.is_working_dir
&& !self.is_empty()
{
Ok(self.add_to_ignore().into())
} else {
Ok(EventState::NotConsumed)
};
}
}
Ok(EventState::NotConsumed)
}
fn focused(&self) -> bool {
self.files.focused()
}
fn focus(&mut self, focus: bool) {
self.files.focus(focus);
}
fn is_visible(&self) -> bool {
self.files.is_visible()
}
fn hide(&mut self) {
self.files.hide();
}
fn show(&mut self) -> Result<()> {
self.files.show()?;
Ok(())
}
}