summaryrefslogtreecommitdiffstats
path: root/src/components/commitlist.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/commitlist.rs')
-rw-r--r--src/components/commitlist.rs871
1 files changed, 435 insertions, 436 deletions
diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs
index 332a611a..04a57f2e 100644
--- a/src/components/commitlist.rs
+++ b/src/components/commitlist.rs
@@ -1,466 +1,465 @@
use super::utils::logitems::{ItemBatch, LogEntry};
use crate::{
- components::{
- utils::string_width_align, CommandBlocking, CommandInfo,
- Component, DrawableComponent, EventState, ScrollType,
- },
- keys::SharedKeyConfig,
- strings,
- ui::calc_scroll_top,
- ui::style::{SharedTheme, Theme},
+ components::{
+ utils::string_width_align, CommandBlocking, CommandInfo,
+ Component, DrawableComponent, EventState, ScrollType,
+ },
+ keys::SharedKeyConfig,
+ strings,
+ ui::calc_scroll_top,
+ ui::style::{SharedTheme, Theme},
};
use anyhow::Result;
use asyncgit::sync::{CommitId, Tags};
use chrono::{DateTime, Local};
use crossterm::event::Event;
use std::{
- borrow::Cow, cell::Cell, cmp, convert::TryFrom, time::Instant,
+ borrow::Cow, cell::Cell, cmp, convert::TryFrom, time::Instant,
};
use tui::{
- backend::Backend,
- layout::{Alignment, Rect},
- text::{Span, Spans},
- widgets::{Block, Borders, Paragraph},
- Frame,
+ backend::Backend,
+ layout::{Alignment, Rect},
+ text::{Span, Spans},
+ widgets::{Block, Borders, Paragraph},
+ Frame,
};
const ELEMENTS_PER_LINE: usize = 10;
///
pub struct CommitList {
- title: String,
- selection: usize,
- branch: Option<String>,
- count_total: usize,
- items: ItemBatch,
- marked: Vec<CommitId>,
- scroll_state: (Instant, f32),
- tags: Option<Tags>,
- current_size: Cell<(u16, u16)>,
- scroll_top: Cell<usize>,
- theme: SharedTheme,
- key_config: SharedKeyConfig,
+ title: String,
+ selection: usize,
+ branch: Option<String>,
+ count_total: usize,
+ items: ItemBatch,
+ marked: Vec<CommitId>,
+ scroll_state: (Instant, f32),
+ tags: Option<Tags>,
+ current_size: Cell<(u16, u16)>,
+ scroll_top: Cell<usize>,
+ theme: SharedTheme,
+ key_config: SharedKeyConfig,
}
impl CommitList {
- ///
- pub fn new(
- title: &str,
- theme: SharedTheme,
- key_config: SharedKeyConfig,
- ) -> Self {
- Self {
- items: ItemBatch::default(),
- marked: Vec::with_capacity(2),
- selection: 0,
- branch: None,
- count_total: 0,
- scroll_state: (Instant::now(), 0_f32),
- tags: None,
- current_size: Cell::new((0, 0)),
- scroll_top: Cell::new(0),
- theme,
- key_config,
- title: String::from(title),
- }
- }
-
- ///
- pub fn items(&mut self) -> &mut ItemBatch {
- &mut self.items
- }
-
- ///
- pub fn set_branch(&mut self, name: Option<String>) {
- self.branch = name;
- }
-
- ///
- pub const fn selection(&self) -> usize {
- self.selection
- }
-
- ///
- pub fn current_size(&self) -> (u16, u16) {
- self.current_size.get()
- }
-
- ///
- pub fn set_count_total(&mut self, total: usize) {
- self.count_total = total;
- self.selection =
- cmp::min(self.selection, self.selection_max());
- }
-
- ///
- #[allow(clippy::missing_const_for_fn)]
- pub fn selection_max(&self) -> usize {
- self.count_total.saturating_sub(1)
- }
-
- ///
- pub const fn tags(&self) -> Option<&Tags> {
- self.tags.as_ref()
- }
-
- ///
- pub fn clear(&mut self) {
- self.items.clear();
- }
-
- ///
- pub fn set_tags(&mut self, tags: Tags) {
- self.tags = Some(tags);
- }
-
- ///
- pub fn selected_entry(&self) -> Option<&LogEntry> {
- self.items.iter().nth(
- self.selection.saturating_sub(self.items.index_offset()),
- )
- }
-
- pub fn copy_entry_hash(&self) -> Result<()> {
- if let Some(e) = self.items.iter().nth(
- self.selection.saturating_sub(self.items.index_offset()),
- ) {
- crate::clipboard::copy_string(&e.hash_short)?;
- }
- Ok(())
- }
-
- fn move_selection(&mut self, scroll: ScrollType) -> Result<bool> {
- self.update_scroll_speed();
-
- #[allow(clippy::cast_possible_truncation)]
- let speed_int =
- usize::try_from(self.scroll_state.1 as i64)?.max(1);
-
- let page_offset =
- usize::from(self.current_size.get().1).saturating_sub(1);
-
- let new_selection = match scroll {
- ScrollType::Up => {
- self.selection.saturating_sub(speed_int)
- }
- ScrollType::Down => {
- self.selection.saturating_add(speed_int)
- }
- ScrollType::PageUp => {
- self.selection.saturating_sub(page_offset)
- }
- ScrollType::PageDown => {
- self.selection.saturating_add(page_offset)
- }
- ScrollType::Home => 0,
- ScrollType::End => self.selection_max(),
- };
-
- let new_selection =
- cmp::min(new_selection, self.selection_max());
-
- let needs_update = new_selection != self.selection;
-
- self.selection = new_selection;
-
- Ok(needs_update)
- }
-
- fn mark(&mut self) {
- if let Some(e) = self.selected_entry() {
- let id = e.id;
- if self.is_marked(&id).unwrap_or_default() {
- self.marked.retain(|marked| marked != &id);
- } else {
- self.marked.push(id);
- }
- }
- }
-
- fn update_scroll_speed(&mut self) {
- const REPEATED_SCROLL_THRESHOLD_MILLIS: u128 = 300;
- const SCROLL_SPEED_START: f32 = 0.1_f32;
- const SCROLL_SPEED_MAX: f32 = 10_f32;
- const SCROLL_SPEED_MULTIPLIER: f32 = 1.05_f32;
-
- let now = Instant::now();
-
- let since_last_scroll =
- now.duration_since(self.scroll_state.0);
-
- self.scroll_state.0 = now;
-
- let speed = if since_last_scroll.as_millis()
- < REPEATED_SCROLL_THRESHOLD_MILLIS
- {
- self.scroll_state.1 * SCROLL_SPEED_MULTIPLIER
- } else {
- SCROLL_SPEED_START
- };
-
- self.scroll_state.1 = speed.min(SCROLL_SPEED_MAX);
- }
-
- fn is_marked(&self, id: &CommitId) -> Option<bool> {
- if self.marked.is_empty() {
- None
- } else {
- let found = self.marked.iter().any(|entry| entry == id);
- Some(found)
- }
- }
-
- fn get_entry_to_add<'a>(
- e: &'a LogEntry,
- selected: bool,
- tags: Option<String>,
- theme: &Theme,
- width: usize,
- now: DateTime<Local>,
- marked: Option<bool>,
- ) -> Spans<'a> {
- let mut txt: Vec<Span> = Vec::new();
- txt.reserve(
- ELEMENTS_PER_LINE + if marked.is_some() { 2 } else { 0 },
- );
-
- let splitter_txt = Cow::from(" ");
- let splitter =
- Span::styled(splitter_txt, theme.text(true, selected));
-
- // marked
- if let Some(marked) = marked {
- txt.push(Span::styled(
- Cow::from(if marked { "X" } else { " " }),
- theme.text(true, selected),
- ));
- txt.push(splitter.clone());
- }
-
- // commit hash
- txt.push(Span::styled(
- Cow::from(e.hash_short.as_str()),
- theme.commit_hash(selected),
- ));
-
- txt.push(splitter.clone());
-
- // commit timestamp
- txt.push(Span::styled(
- Cow::from(e.time_to_string(now)),
- theme.commit_time(selected),
- ));
-
- txt.push(splitter.clone());
-
- let author_width =
- (width.saturating_sub(19) / 3).max(3).min(20);
- let author = string_width_align(&e.author, author_width);
-
- // commit author
- txt.push(Span::styled::<String>(
- author,
- theme.commit_author(selected),
- ));
-
- txt.push(splitter.clone());
-
- // commit tags
- txt.push(Span::styled(
- Cow::from(if let Some(tags) = tags {
- format!(" {}", tags)
- } else {
- String::from("")
- }),
- theme.tags(selected),
- ));
-
- txt.push(splitter);
-
- // commit msg
- txt.push(Span::styled(
- Cow::from(e.msg.as_str()),
- theme.text(true, selected),
- ));
- Spans::from(txt)
- }
-
- fn get_text(&self, height: usize, width: usize) -> Vec<Spans> {
- let selection = self.relative_selection();
-
- let mut txt: Vec<Spans> = Vec::with_capacity(height);
-
- let now = Local::now();
-
- let any_marked = !self.marked.is_empty();
-
- for (idx, e) in self
- .items
- .iter()
- .skip(self.scroll_top.get())
- .take(height)
- .enumerate()
- {
- let tags = self
- .tags
- .as_ref()
- .and_then(|t| t.get(&e.id))
- .map(|tags| tags.join(" "));
-
- let marked = if any_marked {
- self.is_marked(&e.id)
- } else {
- None
- };
-
- txt.push(Self::get_entry_to_add(
- e,
- idx + self.scroll_top.get() == selection,
- tags,
- &self.theme,
- width,
- now,
- marked,
- ));
- }
-
- txt
- }
-
- #[allow(clippy::missing_const_for_fn)]
- fn relative_selection(&self) -> usize {
- self.selection.saturating_sub(self.items.index_offset())
- }
-
- pub fn select_entry(&mut self, position: usize) {
- self.selection = position;
- }
+ ///
+ pub fn new(
+ title: &str,
+ theme: SharedTheme,
+ key_config: SharedKeyConfig,
+ ) -> Self {
+ Self {
+ items: ItemBatch::default(),
+ marked: Vec::with_capacity(2),
+ selection: 0,
+ branch: None,
+ count_total: 0,
+ scroll_state: (Instant::now(), 0_f32),
+ tags: None,
+ current_size: Cell::new((0, 0)),
+ scroll_top: Cell::new(0),
+ theme,
+ key_config,
+ title: String::from(title),
+ }
+ }
+
+ ///
+ pub fn items(&mut self) -> &mut ItemBatch {
+ &mut self.items
+ }
+
+ ///
+ pub fn set_branch(&mut self, name: Option<String>) {
+ self.branch = name;
+ }
+
+ ///
+ pub const fn selection(&self) -> usize {
+ self.selection
+ }
+
+ ///
+ pub fn current_size(&self) -> (u16, u16) {
+ self.current_size.get()
+ }
+
+ ///
+ pub fn set_count_total(&mut self, total: usize) {
+ self.count_total = total;
+ self.selection =
+ cmp::min(self.selection, self.selection_max());
+ }
+
+ ///
+ #[allow(clippy::missing_const_for_fn)]
+ pub fn selection_max(&self) -> usize {
+ self.count_total.saturating_sub(1)
+ }
+
+ ///
+ pub const fn tags(&self) -> Option<&Tags> {
+ self.tags.as_ref()
+ }
+
+ ///
+ pub fn clear(&mut self) {
+ self.items.clear();
+ }
+
+ ///
+ pub fn set_tags(&mut self, tags: Tags) {
+ self.tags = Some(tags);
+ }
+
+ ///
+ pub fn selected_entry(&self) -> Option<&LogEntry> {
+ self.items.iter().nth(
+ self.selection.saturating_sub(self.items.index_offset()),
+ )
+ }
+
+ pub fn copy_entry_hash(&self) -> Result<()> {
+ if let Some(e) = self.items.iter().nth(
+ self.selection.saturating_sub(self.items.index_offset()),
+ ) {
+ crate::clipboard::copy_string(&e.hash_short)?;
+ }
+ Ok(())
+ }
+
+ fn move_selection(&mut self, scroll: ScrollType) -> Result<bool> {
+ self.update_scroll_speed();
+
+ #[allow(clippy::cast_possible_truncation)]
+ let speed_int = usize::try_from(self.scroll_state.1 as i64)?.max(1);
+
+ let page_offset =
+ usize::from(self.current_size.get().1).saturating_sub(1);
+
+ let new_selection = match scroll {
+ ScrollType::Up => {
+ self.selection.saturating_sub(speed_int)
+ }
+ ScrollType::Down => {
+ self.selection.saturating_add(speed_int)
+ }
+ ScrollType::PageUp => {
+ self.selection.saturating_sub(page_offset)
+ }
+ ScrollType::PageDown => {
+ self.selection.saturating_add(page_offset)
+ }
+ ScrollType::Home => 0,
+ ScrollType::End => self.selection_max(),
+ };
+
+ let new_selection =
+ cmp::min(new_selection, self.selection_max());
+
+ let needs_update = new_selection != self.selection;
+
+ self.selection = new_selection;
+
+ Ok(needs_update)
+ }
+
+ fn mark(&mut self) {
+ if let Some(e) = self.selected_entry() {
+ let id = e.id;
+ if self.is_marked(&id).unwrap_or_default() {
+ self.marked.retain(|marked| marked != &id);
+ } else {
+ self.marked.push(id);
+ }
+ }
+ }
+
+ fn update_scroll_speed(&mut self) {
+ const REPEATED_SCROLL_THRESHOLD_MILLIS: u128 = 300;
+ const SCROLL_SPEED_START: f32 = 0.1_f32;
+ const SCROLL_SPEED_MAX: f32 = 10_f32;
+ const SCROLL_SPEED_MULTIPLIER: f32 = 1.05_f32;
+
+ let now = Instant::now();
+
+ let since_last_scroll =
+ now.duration_since(self.scroll_state.0);
+
+ self.scroll_state.0 = now;
+
+ let speed = if since_last_scroll.as_millis()
+ < REPEATED_SCROLL_THRESHOLD_MILLIS
+ {
+ self.scroll_state.1 * SCROLL_SPEED_MULTIPLIER
+ } else {
+ SCROLL_SPEED_START
+ };
+
+ self.scroll_state.1 = speed.min(SCROLL_SPEED_MAX);
+ }
+
+ fn is_marked(&self, id: &CommitId) -> Option<bool> {
+ if self.marked.is_empty() {
+ None
+ } else {
+ let found = self.marked.iter().any(|entry| entry == id);
+ Some(found)
+ }
+ }
+
+ fn get_entry_to_add<'a>(
+ e: &'a LogEntry,
+ selected: bool,
+ tags: Option<String>,
+ theme: &Theme,
+ width: usize,
+ now: DateTime<Local>,
+ marked: Option<bool>,
+ ) -> Spans<'a> {
+ let mut txt: Vec<Span> = Vec::new();
+ txt.reserve(
+ ELEMENTS_PER_LINE + if marked.is_some() { 2 } else { 0 },
+ );
+
+ let splitter_txt = Cow::from(" ");
+ let splitter =
+ Span::styled(splitter_txt, theme.text(true, selected));
+
+ // marked
+ if let Some(marked) = marked {
+ txt.push(Span::styled(
+ Cow::from(if marked { "X" } else { " " }),
+ theme.text(true, selected),
+ ));
+ txt.push(splitter.clone());
+ }
+
+ // commit hash
+ txt.push(Span::styled(
+ Cow::from(e.hash_short.as_str()),
+ theme.commit_hash(selected),
+ ));
+
+ txt.push(splitter.clone());
+
+ // commit timestamp
+ txt.push(Span::styled(
+ Cow::from(e.time_to_string(now)),
+ theme.commit_time(selected),
+ ));
+
+ txt.push(splitter.clone());
+
+ let author_width =
+ (width.saturating_sub(19) / 3).max(3).min(20);
+ let author = string_width_align(&e.author, author_width);
+
+ // commit author
+ txt.push(Span::styled::<String>(
+ author,
+ theme.commit_author(selected),
+ ));
+
+ txt.push(splitter.clone());
+
+ // commit tags
+ txt.push(Span::styled(
+ Cow::from(if let Some(tags) = tags {
+ format!(" {}", tags)
+ } else {
+ String::from("")
+ }),
+ theme.tags(selected),
+ ));
+
+ txt.push(splitter);
+
+ // commit msg
+ txt.push(Span::styled(
+ Cow::from(e.msg.as_str()),
+ theme.text(true, selected),
+ ));
+ Spans::from(txt)
+ }
+
+ fn get_text(&self, height: usize, width: usize) -> Vec<Spans> {
+ let selection = self.relative_selection();
+
+ let mut txt: Vec<Spans> = Vec::with_capacity(height);
+
+ let now = Local::now();
+
+ let any_marked = !self.marked.is_empty();
+
+ for (idx, e) in self
+ .items
+ .iter()
+ .skip(self.scroll_top.get())
+ .take(height)
+ .enumerate()
+ {
+ let tags = self
+ .tags
+ .as_ref()
+ .and_then(|t| t.get(&e.id))
+ .map(|tags| tags.join(" "));
+
+ let marked = if any_marked {
+ self.is_marked(&e.id)
+ } else {
+ None
+ };
+
+ txt.push(Self::get_entry_to_add(
+ e,
+ idx + self.scroll_top.get() == selection,
+ tags,
+ &self.theme,
+ width,
+ now,
+ marked,
+ ));
+ }
+
+ txt
+ }
+
+ #[allow(clippy::missing_const_for_fn)]
+ fn relative_selection(&self) -> usize {
+ self.selection.saturating_sub(self.items.index_offset())
+ }
+
+ pub fn select_entry(&mut self, position: usize) {
+ self.selection = position;
+ }
}
impl DrawableComponent for CommitList {
- fn draw<B: Backend>(
- &self,
- f: &mut Frame<B>,
- area: Rect,
- ) -> Result<()> {
- let current_size = (
- area.width.saturating_sub(2),
- area.height.saturating_sub(2),
- );
- self.current_size.set(current_size);
-
- let height_in_lines = self.current_size.get().1 as usize;
- let selection = self.relative_selection();
-
- self.scroll_top.set(calc_scroll_top(
- self.scroll_top.get(),
- height_in_lines,
- selection,
- ));
-
- let branch_post_fix =
- self.branch.as_ref().map(|b| format!("- {{{}}}", b));
-
- let title = format!(
- "{} {}/{} {}",
- self.title,
- self.count_total.saturating_sub(self.selection),
- self.count_total,
- branch_post_fix.as_deref().unwrap_or(""),
- );
-
- f.render_widget(
- Paragraph::new(
- self.get_text(
- height_in_lines,
- current_size.0 as usize,
- ),
- )
- .block(
- Block::default()
- .borders(Borders::ALL)
- .title(Span::styled(
- title.as_str(),
- self.theme.title(true),
- ))
- .border_style(self.theme.block(true)),
- )
- .alignment(Alignment::Left),
- area,
- );
-
- Ok(())
- }
+ fn draw<B: Backend>(
+ &self,
+ f: &mut Frame<B>,
+ area: Rect,
+ ) -> Result<()> {
+ let current_size = (
+ area.width.saturating_sub(2),
+ area.height.saturating_sub(2),
+ );
+ self.current_size.set(current_size);
+
+ let height_in_lines = self.current_size.get().1 as usize;
+ let selection = self.relative_selection();
+
+ self.scroll_top.set(calc_scroll_top(
+ self.scroll_top.get(),
+ height_in_lines,
+ selection,
+ ));
+
+ let branch_post_fix =
+ self.branch.as_ref().map(|b| format!("- {{{}}}", b));
+
+ let title = format!(
+ "{} {}/{} {}",
+ self.title,
+ self.count_total.saturating_sub(self.selection),
+ self.count_total,
+ branch_post_fix.as_deref().unwrap_or(""),
+ );
+
+ f.render_widget(
+ Paragraph::new(
+ self.get_text(
+ height_in_lines,
+ current_size.0 as usize,
+ ),
+ )
+ .block(
+ Block::default()
+ .borders(Borders::ALL)
+ .title(Span::styled(
+ title.as_str(),
+ self.theme.title(true),
+ ))
+ .border_style(self.theme.block(true)),
+ )
+ .alignment(Alignment::Left),
+ area,
+ );
+
+ Ok(())
+ }
}
impl Component for CommitList {
- fn event(&mut self, ev: Event) -> Result<EventState> {
- if let Event::Key(k) = ev {
- let selection_changed = if k == self.key_config.move_up {
- self.move_selection(ScrollType::Up)?
- } else if k == self.key_config.move_down {
- self.move_selection(ScrollType::Down)?
- } else if k == self.key_config.shift_up
- || k == self.key_config.home
- {
- self.move_selection(ScrollType::Home)?
- } else if k == self.key_config.shift_down
- || k == self.key_config.end
- {
- self.move_selection(ScrollType::End)?
- } else if k == self.key_config.page_up {
- self.move_selection(ScrollType::PageUp)?
- } else if k == self.key_config.page_down {
- self.move_selection(ScrollType::PageDown)?
- } else if k == self.key_config.log_mark_commit {
- self.mark();
- true
- } else {
- false
- };
- return Ok(selection_changed.into());
- }
-
- Ok(EventState::NotConsumed)
- }
-
- fn commands(
- &self,
- out: &mut Vec<CommandInfo>,
- _force_all: bool,
- ) -> CommandBlocking {
- out.push(CommandInfo::new(
- strings::commands::scroll(&self.key_config),
- self.selected_entry().is_some(),
- true,
- ));
- CommandBlocking::PassingOn
- }
+ fn event(&mut self, ev: Event) -> Result<EventState> {
+ if let Event::Key(k) = ev {
+ let selection_changed = if k == self.key_config.move_up {
+ self.move_selection(ScrollType::Up)?
+ } else if k == self.key_config.move_down {
+ self.move_selection(ScrollType::Down)?
+ } else if k == self.key_config.shift_up
+ || k == self.key_config.home
+ {
+ self.move_selection(ScrollType::Home)?
+ } else if k == self.key_config.shift_down
+ || k == self.key_config.end
+ {
+ self.move_selection(ScrollType::End)?
+ } else if k == self.key_config.page_up {
+ self.move_selection(ScrollType::PageUp)?
+ } else if k == self.key_config.page_down {
+ self.move_selection(ScrollType::PageDown)?
+ } else if k == self.key_config.log_mark_commit {
+ self.mark();
+ true
+ } else {
+ false
+ };
+ return Ok(selection_changed.into());
+ }
+
+ Ok(EventState::NotConsumed)
+ }
+
+ fn commands(
+ &self,
+ out: &mut Vec<CommandInfo>,
+ _force_all: bool,
+ ) -> CommandBlocking {
+ out.push(CommandInfo::new(
+ strings::commands::scroll(&self.key_config),
+ self.selected_entry().is_some(),
+ true,
+ ));
+ CommandBlocking::PassingOn
+ }
}
#[cfg(test)]
mod tests {
- use super::*;
-
- #[test]
- fn test_string_width_align() {
- assert_eq!(string_width_align("123", 3), "123");
- assert_eq!(string_width_align("123", 2), "..");
- assert_eq!(string_width_align("123", 3), "123");
- assert_eq!(string_width_align("12345", 6), "12345 ");
- assert_eq!(string_width_align("1234556", 4), "12..");
- }
-
- #[test]
- fn test_string_width_align_unicode() {
- assert_eq!(string_width_align("äste", 3), "ä..");
- assert_eq!(
- string_width_align("wüsten äste", 10),
- "wüsten ä.."
- );
- assert_eq!(
- string_width_align("Jon Grythe Stødle", 19),
- "Jon Grythe Stødle "
- );
- }
+ use super::*;
+
+ #[test]
+ fn test_string_width_align() {
+ assert_eq!(string_width_align("123", 3), "123");
+ assert_eq!(string_width_align("123", 2), "..");
+ assert_eq!(string_width_align("123", 3), "123");
+ assert_eq!(string_width_align("12345", 6), "12345 ");
+ assert_eq!(string_width_align("1234556", 4), "12..");
+ }
+
+ #[test]
+ fn test_string_width_align_unicode() {
+ assert_eq!(string_width_align("äste", 3), "ä..");
+ assert_eq!(
+ string_width_align("wüsten äste", 10),
+ "wüsten ä.."
+ );
+ assert_eq!(
+ string_width_align("Jon Grythe Stødle", 19),
+ "Jon Grythe Stødle "
+ );
+ }
}