gitui 0.22.1

blazing fast terminal-ui for git
use super::{
	utils::scroll_vertical::VerticalScroll, visibility_blocking,
	CommandBlocking, CommandInfo, Component, DrawableComponent,
	EventState, ScrollType,
};
use crate::{
	keys::{key_match, SharedKeyConfig},
	queue::{InternalEvent, NeedsUpdate, Queue},
	strings, try_or_popup,
	ui::{self, Size},
};
use anyhow::Result;
use asyncgit::sync::{
	get_submodules, repo_dir, submodule_parent_info,
	update_submodule, RepoPathRef, SubmoduleInfo,
	SubmoduleParentInfo,
};
use crossterm::event::Event;
use std::{cell::Cell, convert::TryInto};
use tui::{
	backend::Backend,
	layout::{
		Alignment, Constraint, Direction, Layout, Margin, Rect,
	},
	text::{Span, Spans, Text},
	widgets::{Block, Borders, Clear, Paragraph},
	Frame,
};
use ui::style::SharedTheme;
use unicode_truncate::UnicodeTruncateStr;

///
pub struct SubmodulesListComponent {
	repo: RepoPathRef,
	repo_path: String,
	queue: Queue,
	submodules: Vec<SubmoduleInfo>,
	submodule_parent: Option<SubmoduleParentInfo>,
	visible: bool,
	current_height: Cell<u16>,
	selection: u16,
	scroll: VerticalScroll,
	theme: SharedTheme,
	key_config: SharedKeyConfig,
}

impl DrawableComponent for SubmodulesListComponent {
	fn draw<B: Backend>(
		&self,
		f: &mut Frame<B>,
		rect: Rect,
	) -> Result<()> {
		if self.is_visible() {
			const PERCENT_SIZE: Size = Size::new(80, 80);
			const MIN_SIZE: Size = Size::new(60, 30);

			let area = ui::centered_rect(
				PERCENT_SIZE.width,
				PERCENT_SIZE.height,
				rect,
			);
			let area = ui::rect_inside(MIN_SIZE, rect.into(), area);
			let area = area.intersection(rect);

			f.render_widget(Clear, area);

			f.render_widget(
				Block::default()
					.title(strings::POPUP_TITLE_SUBMODULES)
					.border_type(tui::widgets::BorderType::Thick)
					.borders(Borders::ALL),
				area,
			);

			let area = area.inner(&Margin {
				vertical: 1,
				horizontal: 1,
			});

			let chunks_vertical = Layout::default()
				.direction(Direction::Vertical)
				.constraints(
					[Constraint::Min(1), Constraint::Length(5)]
						.as_ref(),
				)
				.split(area);

			let chunks = Layout::default()
				.direction(Direction::Horizontal)
				.constraints(
					[Constraint::Min(40), Constraint::Length(40)]
						.as_ref(),
				)
				.split(chunks_vertical[0]);

			self.draw_list(f, chunks[0])?;
			self.draw_info(f, chunks[1]);
			self.draw_local_info(f, chunks_vertical[1]);
		}

		Ok(())
	}
}

impl Component for SubmodulesListComponent {
	fn commands(
		&self,
		out: &mut Vec<CommandInfo>,
		force_all: bool,
	) -> CommandBlocking {
		if self.visible || force_all {
			if !force_all {
				out.clear();
			}

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

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

			out.push(CommandInfo::new(
				strings::commands::open_submodule(&self.key_config),
				self.is_valid_selection(),
				true,
			));

			out.push(CommandInfo::new(
				strings::commands::update_submodule(&self.key_config),
				self.is_valid_selection(),
				true,
			));

			out.push(CommandInfo::new(
				strings::commands::open_submodule_parent(
					&self.key_config,
				),
				self.submodule_parent.is_some(),
				true,
			));
		}
		visibility_blocking(self)
	}

	fn event(&mut self, ev: &Event) -> Result<EventState> {
		if !self.visible {
			return Ok(EventState::NotConsumed);
		}

		if let Event::Key(e) = ev {
			if key_match(e, self.key_config.keys.exit_popup) {
				self.hide();
			} else if key_match(e, self.key_config.keys.move_down) {
				return self
					.move_selection(ScrollType::Up)
					.map(Into::into);
			} else if key_match(e, self.key_config.keys.move_up) {
				return self
					.move_selection(ScrollType::Down)
					.map(Into::into);
			} else if key_match(e, self.key_config.keys.page_down) {
				return self
					.move_selection(ScrollType::PageDown)
					.map(Into::into);
			} else if key_match(e, self.key_config.keys.page_up) {
				return self
					.move_selection(ScrollType::PageUp)
					.map(Into::into);
			} else if key_match(e, self.key_config.keys.home) {
				return self
					.move_selection(ScrollType::Home)
					.map(Into::into);
			} else if key_match(e, self.key_config.keys.end) {
				return self
					.move_selection(ScrollType::End)
					.map(Into::into);
			} else if key_match(e, self.key_config.keys.enter) {
				if let Some(submodule) = self.selected_entry() {
					self.queue.push(InternalEvent::OpenRepo {
						path: submodule.path.clone(),
					});
				}
			} else if key_match(
				e,
				self.key_config.keys.update_submodule,
			) {
				if let Some(submodule) = self.selected_entry() {
					try_or_popup!(
						self,
						"update submodule:",
						update_submodule(
							&self.repo.borrow(),
							&submodule.name,
						)
					);

					self.update_submodules()?;

					self.queue.push(InternalEvent::Update(
						NeedsUpdate::ALL,
					));
				}
			} else if key_match(
				e,
				self.key_config.keys.view_submodule_parent,
			) {
				if let Some(parent) = &self.submodule_parent {
					self.queue.push(InternalEvent::OpenRepo {
						path: parent.parent_gitpath.clone(),
					});
				}
			} else if key_match(
				e,
				self.key_config.keys.cmd_bar_toggle,
			) {
				//do not consume if its the more key
				return Ok(EventState::NotConsumed);
			}
		}

		Ok(EventState::Consumed)
	}

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

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

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

		Ok(())
	}
}

impl SubmodulesListComponent {
	pub fn new(
		repo: RepoPathRef,
		queue: &Queue,
		theme: SharedTheme,
		key_config: SharedKeyConfig,
	) -> Self {
		Self {
			submodules: Vec::new(),
			submodule_parent: None,
			scroll: VerticalScroll::new(),
			queue: queue.clone(),
			selection: 0,
			visible: false,
			theme,
			key_config,
			current_height: Cell::new(0),
			repo,
			repo_path: String::new(),
		}
	}

	///
	pub fn open(&mut self) -> Result<()> {
		self.show()?;
		self.update_submodules()?;

		Ok(())
	}

	///
	pub fn update_submodules(&mut self) -> Result<()> {
		if self.is_visible() {
			self.submodules = get_submodules(&self.repo.borrow())?;

			self.submodule_parent =
				submodule_parent_info(&self.repo.borrow())?;

			self.repo_path = repo_dir(&self.repo.borrow())
				.map(|e| e.to_string_lossy().to_string())
				.unwrap_or_default();

			self.set_selection(self.selection)?;
		}
		Ok(())
	}

	fn selected_entry(&self) -> Option<&SubmoduleInfo> {
		self.submodules.get(self.selection as usize)
	}

	fn is_valid_selection(&self) -> bool {
		self.selected_entry().is_some()
	}

	//TODO: dedup this almost identical with BranchListComponent
	fn move_selection(&mut self, scroll: ScrollType) -> Result<bool> {
		let new_selection = match scroll {
			ScrollType::Up => self.selection.saturating_add(1),
			ScrollType::Down => self.selection.saturating_sub(1),
			ScrollType::PageDown => self
				.selection
				.saturating_add(self.current_height.get()),
			ScrollType::PageUp => self
				.selection
				.saturating_sub(self.current_height.get()),
			ScrollType::Home => 0,
			ScrollType::End => {
				let count: u16 = self.submodules.len().try_into()?;
				count.saturating_sub(1)
			}
		};

		self.set_selection(new_selection)?;

		Ok(true)
	}

	fn set_selection(&mut self, selection: u16) -> Result<()> {
		let num_entriess: u16 = self.submodules.len().try_into()?;
		let num_entriess = num_entriess.saturating_sub(1);

		let selection = if selection > num_entriess {
			num_entriess
		} else {
			selection
		};

		self.selection = selection;

		Ok(())
	}

	fn get_text(
		&self,
		theme: &SharedTheme,
		width_available: u16,
		height: usize,
	) -> Text {
		const THREE_DOTS: &str = "...";
		const THREE_DOTS_LENGTH: usize = THREE_DOTS.len(); // "..."
		const COMMIT_HASH_LENGTH: usize = 8;

		let mut txt = Vec::with_capacity(3);

		let name_length: usize = (width_available as usize)
			.saturating_sub(COMMIT_HASH_LENGTH)
			.saturating_sub(THREE_DOTS_LENGTH);

		for (i, submodule) in self
			.submodules
			.iter()
			.skip(self.scroll.get_top())
			.take(height)
			.enumerate()
		{
			let mut module_path = submodule
				.path
				.as_os_str()
				.to_string_lossy()
				.to_string();

			if module_path.len() > name_length {
				module_path.unicode_truncate(
					name_length.saturating_sub(THREE_DOTS_LENGTH),
				);
				module_path += THREE_DOTS;
			}

			let selected = (self.selection as usize
				- self.scroll.get_top())
				== i;

			let span_hash = Span::styled(
				format!(
					"{} ",
					submodule
						.head_id
						.unwrap_or_default()
						.get_short_string()
				),
				theme.commit_hash(selected),
			);

			let span_name = Span::styled(
				format!("{module_path:name_length$} "),
				theme.text(true, selected),
			);

			txt.push(Spans::from(vec![span_name, span_hash]));
		}

		Text::from(txt)
	}

	fn get_info_text(&self, theme: &SharedTheme) -> Text {
		self.selected_entry().map_or_else(
			Text::default,
			|submodule| {
				let span_title_path =
					Span::styled("Path:", theme.text(false, false));
				let span_path = Span::styled(
					submodule.path.to_string_lossy(),
					theme.text(true, false),
				);

				let span_title_commit =
					Span::styled("Commit:", theme.text(false, false));
				let span_commit = Span::styled(
					submodule.id.unwrap_or_default().to_string(),
					theme.commit_hash(false),
				);

				let span_title_url =
					Span::styled("Url:", theme.text(false, false));
				let span_url = Span::styled(
					submodule.url.clone().unwrap_or_default(),
					theme.text(true, false),
				);

				let span_title_status =
					Span::styled("Status:", theme.text(false, false));
				let span_status = Span::styled(
					format!("{:?}", submodule.status),
					theme.text(true, false),
				);

				Text::from(vec![
					Spans::from(vec![span_title_path]),
					Spans::from(vec![span_path]),
					Spans::from(vec![]),
					Spans::from(vec![span_title_commit]),
					Spans::from(vec![span_commit]),
					Spans::from(vec![]),
					Spans::from(vec![span_title_url]),
					Spans::from(vec![span_url]),
					Spans::from(vec![]),
					Spans::from(vec![span_title_status]),
					Spans::from(vec![span_status]),
				])
			},
		)
	}

	fn get_local_info_text(&self, theme: &SharedTheme) -> Text {
		let mut spans = vec![
			Spans::from(vec![Span::styled(
				"Current:",
				theme.text(false, false),
			)]),
			Spans::from(vec![Span::styled(
				self.repo_path.to_string(),
				theme.text(true, false),
			)]),
			Spans::from(vec![Span::styled(
				"Parent:",
				theme.text(false, false),
			)]),
		];

		if let Some(parent_info) = &self.submodule_parent {
			spans.push(Spans::from(vec![Span::styled(
				parent_info.parent_gitpath.to_string_lossy(),
				theme.text(true, false),
			)]));
		}

		Text::from(spans)
	}

	fn draw_list<B: Backend>(
		&self,
		f: &mut Frame<B>,
		r: Rect,
	) -> Result<()> {
		let height_in_lines = r.height as usize;
		self.current_height.set(height_in_lines.try_into()?);

		self.scroll.update(
			self.selection as usize,
			self.submodules.len(),
			height_in_lines,
		);

		f.render_widget(
			Paragraph::new(self.get_text(
				&self.theme,
				r.width.saturating_add(1),
				height_in_lines,
			))
			.block(Block::default().borders(Borders::RIGHT))
			.alignment(Alignment::Left),
			r,
		);

		let mut r = r;
		r.height += 2;
		r.y = r.y.saturating_sub(1);

		self.scroll.draw(f, r, &self.theme);

		Ok(())
	}

	fn draw_info<B: Backend>(&self, f: &mut Frame<B>, r: Rect) {
		f.render_widget(
			Paragraph::new(self.get_info_text(&self.theme))
				.alignment(Alignment::Left),
			r,
		);
	}

	fn draw_local_info<B: Backend>(&self, f: &mut Frame<B>, r: Rect) {
		f.render_widget(
			Paragraph::new(self.get_local_info_text(&self.theme))
				.block(Block::default().borders(Borders::TOP))
				.alignment(Alignment::Left),
			r,
		);
	}
}