diff options
Diffstat (limited to 'src/components/taglist.rs')
-rw-r--r-- | src/components/taglist.rs | 818 |
1 files changed, 409 insertions, 409 deletions
diff --git a/src/components/taglist.rs b/src/components/taglist.rs index 8df70364..67818b18 100644 --- a/src/components/taglist.rs +++ b/src/components/taglist.rs @@ -1,435 +1,435 @@ use super::{ - utils, visibility_blocking, CommandBlocking, CommandInfo, - Component, DrawableComponent, EventState, + utils, visibility_blocking, CommandBlocking, CommandInfo, + Component, DrawableComponent, EventState, }; use crate::{ - components::ScrollType, - keys::SharedKeyConfig, - queue::{Action, InternalEvent, Queue}, - strings, - ui::{self, Size}, - AsyncAppNotification, AsyncNotification, + components::ScrollType, + keys::SharedKeyConfig, + queue::{Action, InternalEvent, Queue}, + strings, + ui::{self, Size}, + AsyncAppNotification, AsyncNotification, }; use anyhow::Result; use asyncgit::{ - asyncjob::AsyncSingleJob, - remote_tags::AsyncRemoteTagsJob, - sync::cred::{ - extract_username_password, need_username_password, - BasicAuthCredential, - }, - sync::{get_tags_with_metadata, TagWithMetadata}, - AsyncGitNotification, CWD, + asyncjob::AsyncSingleJob, + remote_tags::AsyncRemoteTagsJob, + sync::cred::{ + extract_username_password, need_username_password, + BasicAuthCredential, + }, + sync::{get_tags_with_metadata, TagWithMetadata}, + AsyncGitNotification, CWD, }; use crossbeam_channel::Sender; use crossterm::event::Event; use std::convert::TryInto; use tui::{ - backend::Backend, - layout::{Constraint, Margin, Rect}, - text::Span, - widgets::{ - Block, BorderType, Borders, Cell, Clear, Row, Table, - TableState, - }, - Frame, + backend::Backend, + layout::{Constraint, Margin, Rect}, + text::Span, + widgets::{ + Block, BorderType, Borders, Cell, Clear, Row, Table, + TableState, + }, + Frame, }; use ui::style::SharedTheme; /// pub struct TagListComponent { - theme: SharedTheme, - queue: Queue, - tags: Option<Vec<TagWithMetadata>>, - visible: bool, - table_state: std::cell::Cell<TableState>, - current_height: std::cell::Cell<usize>, - missing_remote_tags: Option<Vec<String>>, - basic_credential: Option<BasicAuthCredential>, - async_remote_tags: - AsyncSingleJob<AsyncRemoteTagsJob, AsyncAppNotification>, - key_config: SharedKeyConfig, + theme: SharedTheme, + queue: Queue, + tags: Option<Vec<TagWithMetadata>>, + visible: bool, + table_state: std::cell::Cell<TableState>, + current_height: std::cell::Cell<usize>, + missing_remote_tags: Option<Vec<String>>, + basic_credential: Option<BasicAuthCredential>, + async_remote_tags: + AsyncSingleJob<AsyncRemoteTagsJob, AsyncAppNotification>, + key_config: SharedKeyConfig, } impl DrawableComponent for TagListComponent { - fn draw<B: Backend>( - &self, - f: &mut Frame<B>, - rect: Rect, - ) -> Result<()> { - if self.visible { - const PERCENT_SIZE: Size = Size::new(80, 50); - const MIN_SIZE: Size = Size::new(60, 20); - - let area = ui::centered_rect( - PERCENT_SIZE.width, - PERCENT_SIZE.height, - f.size(), - ); - let area = - ui::rect_inside(MIN_SIZE, f.size().into(), area); - let area = area.intersection(rect); - - let tag_name_width = - self.tags.as_ref().map_or(0, |tags| { - tags.iter() - .fold(0, |acc, tag| acc.max(tag.name.len())) - }); - - let constraints = [ - // symbol if tag is not yet on remote and can be pushed - Constraint::Length(1), - // tag name - Constraint::Length(tag_name_width.try_into()?), - // commit date - Constraint::Length(10), - // author width - Constraint::Length(19), - // commit id - Constraint::Min(0), - ]; - - let rows = self.get_rows(); - let number_of_rows = rows.len(); - - let table = Table::new(rows) - .widths(&constraints) - .column_spacing(1) - .highlight_style(self.theme.text(true, true)) - .block( - Block::default() - .borders(Borders::ALL) - .title(Span::styled( - strings::title_tags(), - self.theme.title(true), - )) - .border_style(self.theme.block(true)) - .border_type(BorderType::Thick), - ); - - let mut table_state = self.table_state.take(); - - f.render_widget(Clear, area); - f.render_stateful_widget(table, area, &mut table_state); - - let area = area.inner(&Margin { - vertical: 1, - horizontal: 0, - }); - - ui::draw_scrollbar( - f, - area, - &self.theme, - number_of_rows, - table_state.selected().unwrap_or(0), - ); - - self.table_state.set(table_state); - self.current_height.set(area.height.into()); - } - - Ok(()) - } + fn draw<B: Backend>( + &self, + f: &mut Frame<B>, + rect: Rect, + ) -> Result<()> { + if self.visible { + const PERCENT_SIZE: Size = Size::new(80, 50); + const MIN_SIZE: Size = Size::new(60, 20); + + let area = ui::centered_rect( + PERCENT_SIZE.width, + PERCENT_SIZE.height, + f.size(), + ); + let area = + ui::rect_inside(MIN_SIZE, f.size().into(), area); + let area = area.intersection(rect); + + let tag_name_width = + self.tags.as_ref().map_or(0, |tags| { + tags.iter() + .fold(0, |acc, tag| acc.max(tag.name.len())) + }); + + let constraints = [ + // symbol if tag is not yet on remote and can be pushed + Constraint::Length(1), + // tag name + Constraint::Length(tag_name_width.try_into()?), + // commit date + Constraint::Length(10), + // author width + Constraint::Length(19), + // commit id + Constraint::Min(0), + ]; + + let rows = self.get_rows(); + let number_of_rows = rows.len(); + + let table = Table::new(rows) + .widths(&constraints) + .column_spacing(1) + .highlight_style(self.theme.text(true, true)) + .block( + Block::default() + .borders(Borders::ALL) + .title(Span::styled( + strings::title_tags(), + self.theme.title(true), + )) + .border_style(self.theme.block(true)) + .border_type(BorderType::Thick), + ); + + let mut table_state = self.table_state.take(); + + f.render_widget(Clear, area); + f.render_stateful_widget(table, area, &mut table_state); + + let area = area.inner(&Margin { + vertical: 1, + horizontal: 0, + }); + + ui::draw_scrollbar( + f, + area, + &self.theme, + number_of_rows, + table_state.selected().unwrap_or(0), + ); + + self.table_state.set(table_state); + self.current_height.set(area.height.into()); + } + + Ok(()) + } } impl Component for TagListComponent { - 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::delete_tag_popup(&self.key_config), - self.valid_selection(), - true, - )); - out.push(CommandInfo::new( - strings::commands::select_tag(&self.key_config), - self.valid_selection(), - true, - )); - out.push(CommandInfo::new( - strings::commands::push_tags(&self.key_config), - true, - true, - )); - } - visibility_blocking(self) - } - - fn event(&mut self, event: Event) -> Result<EventState> { - if self.visible { - if let Event::Key(key) = event { - if key == self.key_config.exit_popup { - self.hide(); - } else if key == self.key_config.move_up { - self.move_selection(ScrollType::Up); - } else if key == self.key_config.move_down { - self.move_selection(ScrollType::Down); - } else if key == self.key_config.shift_up - || key == self.key_config.home - { - self.move_selection(ScrollType::Home); - } else if key == self.key_config.shift_down - || key == self.key_config.end - { - self.move_selection(ScrollType::End); - } else if key == self.key_config.page_down { - self.move_selection(ScrollType::PageDown); - } else if key == self.key_config.page_up { - self.move_selection(ScrollType::PageUp); - } else if key == self.key_config.delete_tag { - return self.selected_tag().map_or( - Ok(EventState::NotConsumed), - |tag| { - self.queue.push( - InternalEvent::ConfirmAction( - Action::DeleteTag( - tag.name.clone(), - ), - ), - ); - Ok(EventState::Consumed) - }, - ); - } else if key == self.key_config.select_tag { - return self.selected_tag().map_or( - Ok(EventState::NotConsumed), - |tag| { - self.queue.push( - InternalEvent::SelectCommitInRevlog( - tag.commit_id, - ), - ); - Ok(EventState::Consumed) - }, - ); - } else if key == self.key_config.push { - self.queue.push(InternalEvent::PushTags); - } - } - - Ok(EventState::Consumed) - } else { - 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(()) - } + 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::delete_tag_popup(&self.key_config), + self.valid_selection(), + true, + )); + out.push(CommandInfo::new( + strings::commands::select_tag(&self.key_config), + self.valid_selection(), + true, + )); + out.push(CommandInfo::new( + strings::commands::push_tags(&self.key_config), + true, + true, + )); + } + visibility_blocking(self) + } + + fn event(&mut self, event: Event) -> Result<EventState> { + if self.visible { + if let Event::Key(key) = event { + if key == self.key_config.exit_popup { + self.hide(); + } else if key == self.key_config.move_up { + self.move_selection(ScrollType::Up); + } else if key == self.key_config.move_down { + self.move_selection(ScrollType::Down); + } else if key == self.key_config.shift_up + || key == self.key_config.home + { + self.move_selection(ScrollType::Home); + } else if key == self.key_config.shift_down + || key == self.key_config.end + { + self.move_selection(ScrollType::End); + } else if key == self.key_config.page_down { + self.move_selection(ScrollType::PageDown); + } else if key == self.key_config.page_up { + self.move_selection(ScrollType::PageUp); + } else if key == self.key_config.delete_tag { + return self.selected_tag().map_or( + Ok(EventState::NotConsumed), + |tag| { + self.queue.push( + InternalEvent::ConfirmAction( + Action::DeleteTag( + tag.name.clone(), + ), + ), + ); + Ok(EventState::Consumed) + }, + ); + } else if key == self.key_config.select_tag { + return self.selected_tag().map_or( + Ok(EventState::NotConsumed), + |tag| { + self.queue.push( + InternalEvent::SelectCommitInRevlog( + tag.commit_id, + ), + ); + Ok(EventState::Consumed) + }, + ); + } else if key == self.key_config.push { + self.queue.push(InternalEvent::PushTags); + } + } + + Ok(EventState::Consumed) + } else { + 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(()) + } } impl TagListComponent { - pub fn new( - queue: &Queue, - sender: &Sender<AsyncAppNotification>, - theme: SharedTheme, - key_config: SharedKeyConfig, - ) -> Self { - Self { - theme, - queue: queue.clone(), - tags: None, - visible: false, - table_state: std::cell::Cell::new(TableState::default()), - current_height: std::cell::Cell::new(0), - basic_credential: None, - missing_remote_tags: None, - async_remote_tags: AsyncSingleJob::new( - sender.clone(), - AsyncAppNotification::RemoteTags, - ), - key_config, - } - } - - /// - pub fn open(&mut self) -> Result<()> { - self.table_state.get_mut().select(Some(0)); - self.show()?; - - let basic_credential = if need_username_password()? { - let credential = extract_username_password()?; - - if credential.is_complete() { - Some(credential) - } else { - None - } - } else { - None - }; - - self.basic_credential = basic_credential; - - self.update_tags()?; - self.update_missing_remote_tags(); - - Ok(()) - } - - /// - pub fn update(&mut self, ev: AsyncNotification) { - if matches!( - ev, - AsyncNotification::App(AsyncAppNotification::RemoteTags) - ) { - if let Some(job) = self.async_remote_tags.take_last() { - if let Some(Ok(missing_remote_tags)) = job.result() { - self.missing_remote_tags = - Some(missing_remote_tags); - } - } - } else if matches!( - ev, - AsyncNotification::Git(AsyncGitNotification::PushTags) - ) { - self.update_missing_remote_tags(); - } - } - - /// - pub fn any_work_pending(&self) -> bool { - self.async_remote_tags.is_pending() - } - - /// fetch list of tags - pub fn update_tags(&mut self) -> Result<()> { - let tags = get_tags_with_metadata(CWD)?; - - self.tags = Some(tags); - - Ok(()) - } - - pub fn update_missing_remote_tags(&mut self) { - self.async_remote_tags.spawn(AsyncRemoteTagsJob::new( - self.basic_credential.clone(), - )); - } - - /// - fn move_selection(&mut self, scroll_type: ScrollType) -> bool { - let mut table_state = self.table_state.take(); - - let old_selection = table_state.selected().unwrap_or(0); - let max_selection = - self.tags.as_ref().map_or(0, |tags| tags.len() - 1); - - let new_selection = match scroll_type { - ScrollType::Up => old_selection.saturating_sub(1), - ScrollType::Down => { - old_selection.saturating_add(1).min(max_selection) - } - ScrollType::Home => 0, - ScrollType::End => max_selection, - ScrollType::PageUp => old_selection.saturating_sub( - self.current_height.get().saturating_sub(1), - ), - ScrollType::PageDown => old_selection - .saturating_add( - self.current_height.get().saturating_sub(1), - ) - .min(max_selection), - }; - - let needs_update = new_selection != old_selection; - - table_state.select(Some(new_selection)); - self.table_state.set(table_state); - - needs_update - } - - /// - fn get_rows(&self) -> Vec<Row> { - if let Some(ref tags) = self.tags { - tags.iter().map(|tag| self.get_row(tag)).collect() - } else { - vec![] - } - } - - /// - fn get_row(&self, tag: &TagWithMetadata) -> Row { - const UPSTREAM_SYMBOL: &str = "\u{2191}"; - const EMPTY_SYMBOL: &str = " "; - - let is_tag_missing_on_remote = self - .missing_remote_tags - .as_ref() - .map_or(false, |missing_remote_tags| { - let remote_tag = format!("refs/tags/{}", tag.name); - - missing_remote_tags.contains(&remote_tag) - }); - - let has_remote_str = if is_tag_missing_on_remote { - UPSTREAM_SYMBOL - } else { - EMPTY_SYMBOL - }; - - let cells: Vec<Cell> = vec![ - Cell::from(has_remote_str) - .style(self.theme.commit_author(false)), - Cell::from(tag.name.clone()) - .style(self.theme.text(true, false)), - Cell::from(utils::time_to_string(tag.time, true)) - .style(self.theme.commit_time(false)), - Cell::from(tag.author.clone()) - .style(self.theme.commit_author(false)), - Cell::from(tag.message.clone()) - .style(self.theme.text(true, false)), - ]; - - Row::new(cells) - } - - fn valid_selection(&self) -> bool { - self.selected_tag().is_some() - } - - fn selected_tag(&self) -> Option<&TagWithMetadata> { - self.tags.as_ref().and_then(|tags| { - let table_state = self.table_state.take(); - - let tag = table_state - .selected() - .and_then(|selected| tags.get(selected)); - - self.table_state.set(table_state); - - tag - }) - } + pub fn new( + queue: &Queue, + sender: &Sender<AsyncAppNotification>, + theme: SharedTheme, + key_config: SharedKeyConfig, + ) -> Self { + Self { + theme, + queue: queue.clone(), + tags: None, + visible: false, + table_state: std::cell::Cell::new(TableState::default()), + current_height: std::cell::Cell::new(0), + basic_credential: None, + missing_remote_tags: None, + async_remote_tags: AsyncSingleJob::new( + sender.clone(), + AsyncAppNotification::RemoteTags, + ), + key_config, + } + } + + /// + pub fn open(&mut self) -> Result<()> { + self.table_state.get_mut().select(Some(0)); + self.show()?; + + let basic_credential = if need_username_password()? { + let credential = extract_username_password()?; + + if credential.is_complete() { + Some(credential) + } else { + None + } + } else { + None + }; + + self.basic_credential = basic_credential; + + self.update_tags()?; + self.update_missing_remote_tags(); + + Ok(()) + } + + /// + pub fn update(&mut self, ev: AsyncNotification) { + if matches!( + ev, + AsyncNotification::App(AsyncAppNotification::RemoteTags) + ) { + if let Some(job) = self.async_remote_tags.take_last() { + if let Some(Ok(missing_remote_tags)) = job.result() { + self.missing_remote_tags = + Some(missing_remote_tags); + } + } + } else if matches!( + ev, + AsyncNotification::Git(AsyncGitNotification::PushTags) + ) { + self.update_missing_remote_tags(); + } + } + + /// + pub fn any_work_pending(&self) -> bool { + self.async_remote_tags.is_pending() + } + + /// fetch list of tags + pub fn update_tags(&mut self) -> Result<()> { + let tags = get_tags_with_metadata(CWD)?; + + self.tags = Some(tags); + + Ok(()) + } + + pub fn update_missing_remote_tags(&mut self) { + self.async_remote_tags.spawn(AsyncRemoteTagsJob::new( + self.basic_credential.clone(), + )); + } + + /// + fn move_selection(&mut self, scroll_type: ScrollType) -> bool { + let mut table_state = self.table_state.take(); + + let old_selection = table_state.selected().unwrap_or(0); + let max_selection = + self.tags.as_ref().map_or(0, |tags| tags.len() - 1); + + let new_selection = match scroll_type { + ScrollType::Up => old_selection.saturating_sub(1), + ScrollType::Down => { + old_selection.saturating_add(1).min(max_selection) + } + ScrollType::Home => 0, + ScrollType::End => max_selection, + ScrollType::PageUp => old_selection.saturating_sub( + self.current_height.get().saturating_sub(1), + ), + ScrollType::PageDown => old_selection + .saturating_add( + self.current_height.get().saturating_sub(1), + ) + .min(max_selection), + }; + + let needs_update = new_selection != old_selection; + + table_state.select(Some(new_selection)); + self.table_state.set(table_state); + + needs_update + } + + /// + fn get_rows(&self) -> Vec<Row> { + if let Some(ref tags) = self.tags { + tags.iter().map(|tag| self.get_row(tag)).collect() + } else { + vec![] + } + } + + /// + fn get_row(&self, tag: &TagWithMetadata) -> Row { + const UPSTREAM_SYMBOL: &str = "\u{2191}"; + const EMPTY_SYMBOL: &str = " "; + + let is_tag_missing_on_remote = self + .missing_remote_tags + .as_ref() + .map_or(false, |missing_remote_tags| { + let remote_tag = format!("refs/tags/{}", tag.name); + + missing_remote_tags.contains(&remote_tag) + }); + + let has_remote_str = if is_tag_missing_on_remote { + UPSTREAM_SYMBOL + } else { + EMPTY_SYMBOL + }; + + let cells: Vec<Cell> = vec![ + Cell::from(has_remote_str) + .style(self.theme.commit_author(false)), + Cell::from(tag.name.clone()) + .style(self.theme.text(true, false)), + Cell::from(utils::time_to_string(tag.time, true)) + .style(self.theme.commit_time(false)), + Cell::from(tag.author.clone()) + .style(self.theme.commit_author(false)), + Cell::from(tag.message.clone()) + .style(self.theme.text(true, false)), + ]; + + Row::new(cells) + } + + fn valid_selection(&self) -> bool { + self.selected_tag().is_some() + } + + fn selected_tag(&self) -> Option<&TagWithMetadata> { + self.tags.as_ref().and_then(|tags| { + let table_state = self.table_state.take(); + + let tag = table_state + .selected() + .and_then(|selected| tags.get(selected)); + + self.table_state.set(table_state); + + tag + }) + } } |