gitui 0.28.1

blazing fast terminal-ui for git
use crate::components::{
	visibility_blocking, CommandBlocking, CommandInfo, Component,
	DrawableComponent, EventState,
};
use crate::{
	app::Environment,
	keys::{key_match, SharedKeyConfig},
	queue::Queue,
	strings, try_or_popup,
	ui::{self, style::SharedTheme},
};
use anyhow::Result;
use asyncgit::{
	cached,
	sync::{CommitId, RepoPath, ResetType},
};
use crossterm::event::Event;
use ratatui::{
	layout::{Alignment, Rect},
	text::{Line, Span},
	widgets::{Block, Borders, Clear, Paragraph},
	Frame,
};

const fn type_to_string(
	kind: ResetType,
) -> (&'static str, &'static str) {
	const RESET_TYPE_DESC_SOFT: &str =
		"  🟢 Keep all changes. Stage differences";
	const RESET_TYPE_DESC_MIXED: &str =
		" 🟡 Keep all changes. Unstage differences";
	const RESET_TYPE_DESC_HARD: &str =
		"  🔴 Discard all local changes";

	match kind {
		ResetType::Soft => ("Soft", RESET_TYPE_DESC_SOFT),
		ResetType::Mixed => ("Mixed", RESET_TYPE_DESC_MIXED),
		ResetType::Hard => ("Hard", RESET_TYPE_DESC_HARD),
	}
}

pub struct ResetPopup {
	queue: Queue,
	repo: RepoPath,
	commit: Option<CommitId>,
	kind: ResetType,
	git_branch_name: cached::BranchName,
	visible: bool,
	key_config: SharedKeyConfig,
	theme: SharedTheme,
}

impl ResetPopup {
	///
	pub fn new(env: &Environment) -> Self {
		Self {
			queue: env.queue.clone(),
			repo: env.repo.borrow().clone(),
			commit: None,
			kind: ResetType::Soft,
			git_branch_name: cached::BranchName::new(
				env.repo.clone(),
			),
			visible: false,
			key_config: env.key_config.clone(),
			theme: env.theme.clone(),
		}
	}

	fn get_text(&self, _width: u16) -> Vec<Line<'_>> {
		let mut txt: Vec<Line> = Vec::with_capacity(10);

		txt.push(Line::from(vec![
			Span::styled(
				String::from("Branch: "),
				self.theme.text(true, false),
			),
			Span::styled(
				self.git_branch_name.last().unwrap_or_default(),
				self.theme.branch(false, true),
			),
		]));

		txt.push(Line::from(vec![
			Span::styled(
				String::from("Reset to: "),
				self.theme.text(true, false),
			),
			Span::styled(
				self.commit
					.map(|c| c.to_string())
					.unwrap_or_default(),
				self.theme.commit_hash(false),
			),
		]));

		let (kind_name, kind_desc) = type_to_string(self.kind);

		txt.push(Line::from(vec![
			Span::styled(
				String::from("How: "),
				self.theme.text(true, false),
			),
			Span::styled(kind_name, self.theme.text(true, true)),
			Span::styled(kind_desc, self.theme.text(true, false)),
		]));

		txt
	}

	///
	pub fn open(&mut self, id: CommitId) -> Result<()> {
		self.show()?;

		self.commit = Some(id);

		Ok(())
	}

	///
	#[allow(clippy::unnecessary_wraps)]
	pub fn update(&mut self) -> Result<()> {
		self.git_branch_name.lookup().ok();

		Ok(())
	}

	fn reset(&mut self) {
		if let Some(id) = self.commit {
			try_or_popup!(
				self,
				"reset:",
				asyncgit::sync::reset_repo(&self.repo, id, self.kind)
			);
		}

		self.hide();
	}

	const fn change_kind(&mut self, incr: bool) {
		self.kind = if incr {
			match self.kind {
				ResetType::Soft => ResetType::Mixed,
				ResetType::Mixed => ResetType::Hard,
				ResetType::Hard => ResetType::Soft,
			}
		} else {
			match self.kind {
				ResetType::Soft => ResetType::Hard,
				ResetType::Mixed => ResetType::Soft,
				ResetType::Hard => ResetType::Mixed,
			}
		};
	}
}

impl DrawableComponent for ResetPopup {
	fn draw(&self, f: &mut Frame, area: Rect) -> Result<()> {
		if self.is_visible() {
			const SIZE: (u16, u16) = (55, 5);
			let area =
				ui::centered_rect_absolute(SIZE.0, SIZE.1, area);

			let width = area.width;

			f.render_widget(Clear, area);
			f.render_widget(
				Paragraph::new(self.get_text(width))
					.block(
						Block::default()
							.borders(Borders::ALL)
							.title(Span::styled(
								"Reset",
								self.theme.title(true),
							))
							.border_style(self.theme.block(true)),
					)
					.alignment(Alignment::Left),
				area,
			);
		}

		Ok(())
	}
}

impl Component for ResetPopup {
	fn commands(
		&self,
		out: &mut Vec<CommandInfo>,
		force_all: bool,
	) -> CommandBlocking {
		if self.is_visible() || force_all {
			out.push(
				CommandInfo::new(
					strings::commands::close_popup(&self.key_config),
					true,
					true,
				)
				.order(1),
			);

			out.push(
				CommandInfo::new(
					strings::commands::reset_commit(&self.key_config),
					true,
					true,
				)
				.order(1),
			);

			out.push(
				CommandInfo::new(
					strings::commands::reset_type(&self.key_config),
					true,
					true,
				)
				.order(1),
			);
		}

		visibility_blocking(self)
	}

	fn event(
		&mut self,
		event: &crossterm::event::Event,
	) -> Result<EventState> {
		if self.is_visible() {
			if let Event::Key(key) = &event {
				if key_match(key, self.key_config.keys.exit_popup) {
					self.hide();
				} else if key_match(
					key,
					self.key_config.keys.move_down,
				) {
					self.change_kind(true);
				} else if key_match(key, self.key_config.keys.move_up)
				{
					self.change_kind(false);
				} else if key_match(key, self.key_config.keys.enter) {
					self.reset();
				}
			}

			return Ok(EventState::Consumed);
		}

		Ok(EventState::NotConsumed)
	}

	fn is_visible(&self) -> bool {
		self.visible
	}

	fn hide(&mut self) {
		self.visible = false;
	}

	fn show(&mut self) -> Result<()> {
		self.visible = true;

		Ok(())
	}
}