summaryrefslogtreecommitdiffstats
path: root/src/components/branchlist.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/branchlist.rs')
-rw-r--r--src/components/branchlist.rs1109
1 files changed, 554 insertions, 555 deletions
diff --git a/src/components/branchlist.rs b/src/components/branchlist.rs
index 0c144b26..b183cace 100644
--- a/src/components/branchlist.rs
+++ b/src/components/branchlist.rs
@@ -1,581 +1,580 @@
use super::{
- utils::scroll_vertical::VerticalScroll, visibility_blocking,
- CommandBlocking, CommandInfo, Component, DrawableComponent,
- EventState,
+ utils::scroll_vertical::VerticalScroll, visibility_blocking,
+ CommandBlocking, CommandInfo, Component, DrawableComponent,
+ EventState,
};
use crate::{
- components::ScrollType,
- keys::SharedKeyConfig,
- queue::{Action, InternalEvent, NeedsUpdate, Queue},
- strings, try_or_popup,
- ui::{self, Size},
+ components::ScrollType,
+ keys::SharedKeyConfig,
+ queue::{Action, InternalEvent, NeedsUpdate, Queue},
+ strings, try_or_popup,
+ ui::{self, Size},
};
use anyhow::Result;
use asyncgit::{
- sync::{
- self, branch::checkout_remote_branch, checkout_branch,
- get_branches_info, BranchInfo,
- },
- AsyncGitNotification, CWD,
+ sync::{
+ self, branch::checkout_remote_branch, checkout_branch,
+ get_branches_info, BranchInfo,
+ },
+ AsyncGitNotification, CWD,
};
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, BorderType, Borders, Clear, Paragraph, Tabs},
- Frame,
+ backend::Backend,
+ layout::{
+ Alignment, Constraint, Direction, Layout, Margin, Rect,
+ },
+ text::{Span, Spans, Text},
+ widgets::{Block, BorderType, Borders, Clear, Paragraph, Tabs},
+ Frame,
};
use ui::style::SharedTheme;
use unicode_truncate::UnicodeTruncateStr;
///
pub struct BranchListComponent {
- branches: Vec<BranchInfo>,
- local: bool,
- visible: bool,
- selection: u16,
- scroll: VerticalScroll,
- current_height: Cell<u16>,
- queue: Queue,
- theme: SharedTheme,
- key_config: SharedKeyConfig,
+ branches: Vec<BranchInfo>,
+ local: bool,
+ visible: bool,
+ selection: u16,
+ scroll: VerticalScroll,
+ current_height: Cell<u16>,
+ queue: Queue,
+ theme: SharedTheme,
+ key_config: SharedKeyConfig,
}
impl DrawableComponent for BranchListComponent {
- fn draw<B: Backend>(
- &self,
- f: &mut Frame<B>,
- rect: Rect,
- ) -> Result<()> {
- if self.is_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);
-
- f.render_widget(Clear, area);
-
- f.render_widget(
- Block::default()
- .title(strings::title_branches())
- .border_type(BorderType::Thick)
- .borders(Borders::ALL),
- area,
- );
-
- let area = area.inner(&Margin {
- vertical: 1,
- horizontal: 1,
- });
-
- let chunks = Layout::default()
- .direction(Direction::Vertical)
- .constraints(
- [Constraint::Length(2), Constraint::Min(1)]
- .as_ref(),
- )
- .split(area);
-
- self.draw_tabs(f, chunks[0]);
- self.draw_list(f, chunks[1])?;
- }
-
- Ok(())
- }
+ fn draw<B: Backend>(
+ &self,
+ f: &mut Frame<B>,
+ rect: Rect,
+ ) -> Result<()> {
+ if self.is_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);
+
+ f.render_widget(Clear, area);
+
+ f.render_widget(
+ Block::default()
+ .title(strings::title_branches())
+ .border_type(BorderType::Thick)
+ .borders(Borders::ALL),
+ area,
+ );
+
+ let area = area.inner(&Margin {
+ vertical: 1,
+ horizontal: 1,
+ });
+
+ let chunks = Layout::default()
+ .direction(Direction::Vertical)
+ .constraints(
+ [Constraint::Length(2), Constraint::Min(1)]
+ .as_ref(),
+ )
+ .split(area);
+
+ self.draw_tabs(f, chunks[0]);
+ self.draw_list(f, chunks[1])?;
+ }
+
+ Ok(())
+ }
}
impl Component for BranchListComponent {
- 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::toggle_branch_popup(
- &self.key_config,
- self.local,
- ),
- true,
- true,
- ));
-
- out.push(CommandInfo::new(
- strings::commands::select_branch_popup(
- &self.key_config,
- ),
- !self.selection_is_cur_branch()
- && self.valid_selection(),
- true,
- ));
-
- out.push(CommandInfo::new(
- strings::commands::open_branch_create_popup(
- &self.key_config,
- ),
- true,
- self.local,
- ));
-
- out.push(CommandInfo::new(
- strings::commands::delete_branch_popup(
- &self.key_config,
- ),
- !self.selection_is_cur_branch(),
- true,
- ));
-
- out.push(CommandInfo::new(
- strings::commands::merge_branch_popup(
- &self.key_config,
- ),
- !self.selection_is_cur_branch(),
- self.local,
- ));
-
- out.push(CommandInfo::new(
- strings::commands::rename_branch_popup(
- &self.key_config,
- ),
- true,
- self.local,
- ));
- }
- visibility_blocking(self)
- }
-
- fn event(&mut self, ev: Event) -> Result<EventState> {
- if self.visible {
- if let Event::Key(e) = ev {
- if e == self.key_config.exit_popup {
- self.hide();
- } else if e == self.key_config.move_down {
- return self
- .move_selection(ScrollType::Up)
- .map(Into::into);
- } else if e == self.key_config.move_up {
- return self
- .move_selection(ScrollType::Down)
- .map(Into::into);
- } else if e == self.key_config.page_down {
- return self
- .move_selection(ScrollType::PageDown)
- .map(Into::into);
- } else if e == self.key_config.page_up {
- return self
- .move_selection(ScrollType::PageUp)
- .map(Into::into);
- } else if e == self.key_config.enter {
- try_or_popup!(
- self,
- "switch branch error:",
- self.switch_to_selected_branch()
- );
- } else if e == self.key_config.create_branch
- && self.local
- {
- self.queue.push(InternalEvent::CreateBranch);
- } else if e == self.key_config.rename_branch
- && self.valid_selection()
- {
- let cur_branch =
- &self.branches[self.selection as usize];
- self.queue.push(InternalEvent::RenameBranch(
- cur_branch.reference.clone(),
- cur_branch.name.clone(),
- ));
-
- self.update_branches()?;
- } else if e == self.key_config.delete_branch
- && !self.selection_is_cur_branch()
- && self.valid_selection()
- {
- self.queue.push(InternalEvent::ConfirmAction(
- Action::DeleteBranch(
- self.branches[self.selection as usize]
- .reference
- .clone(),
- self.local,
- ),
- ));
- } else if e == self.key_config.merge_branch
- && !self.selection_is_cur_branch()
- && self.valid_selection()
- {
- try_or_popup!(
- self,
- "merge branch error:",
- self.merge_branch()
- );
- self.hide();
- self.queue.push(InternalEvent::Update(
- NeedsUpdate::ALL,
- ));
- } else if e == self.key_config.tab_toggle {
- self.local = !self.local;
- self.update_branches()?;
- }
- }
-
- 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::toggle_branch_popup(
+ &self.key_config,
+ self.local,
+ ),
+ true,
+ true,
+ ));
+
+ out.push(CommandInfo::new(
+ strings::commands::select_branch_popup(
+ &self.key_config,
+ ),
+ !self.selection_is_cur_branch()
+ && self.valid_selection(),
+ true,
+ ));
+
+ out.push(CommandInfo::new(
+ strings::commands::open_branch_create_popup(
+ &self.key_config,
+ ),
+ true,
+ self.local,
+ ));
+
+ out.push(CommandInfo::new(
+ strings::commands::delete_branch_popup(
+ &self.key_config,
+ ),
+ !self.selection_is_cur_branch(),
+ true,
+ ));
+
+ out.push(CommandInfo::new(
+ strings::commands::merge_branch_popup(
+ &self.key_config,
+ ),
+ !self.selection_is_cur_branch(),
+ self.local,
+ ));
+
+ out.push(CommandInfo::new(
+ strings::commands::rename_branch_popup(
+ &self.key_config,
+ ),
+ true,
+ self.local,
+ ));
+ }
+ visibility_blocking(self)
+ }
+
+ fn event(&mut self, ev: Event) -> Result<EventState> {
+ if self.visible {
+ if let Event::Key(e) = ev {
+ if e == self.key_config.exit_popup {
+ self.hide();
+ } else if e == self.key_config.move_down {
+ return self
+ .move_selection(ScrollType::Up)
+ .map(Into::into);
+ } else if e == self.key_config.move_up {
+ return self
+ .move_selection(ScrollType::Down)
+ .map(Into::into);
+ } else if e == self.key_config.page_down {
+ return self
+ .move_selection(ScrollType::PageDown)
+ .map(Into::into);
+ } else if e == self.key_config.page_up {
+ return self
+ .move_selection(ScrollType::PageUp)
+ .map(Into::into);
+ } else if e == self.key_config.enter {
+ try_or_popup!(
+ self,
+ "switch branch error:",
+ self.switch_to_selected_branch()
+ );
+ } else if e == self.key_config.create_branch
+ && self.local
+ {
+ self.queue.push(InternalEvent::CreateBranch);
+ } else if e == self.key_config.rename_branch
+ && self.valid_selection()
+ {
+ let cur_branch =
+ &self.branches[self.selection as usize];
+ self.queue.push(InternalEvent::RenameBranch(
+ cur_branch.reference.clone(),
+ cur_branch.name.clone(),
+ ));
+
+ self.update_branches()?;
+ } else if e == self.key_config.delete_branch
+ && !self.selection_is_cur_branch()
+ && self.valid_selection()
+ {
+ self.queue.push(InternalEvent::ConfirmAction(
+ Action::DeleteBranch(
+ self.branches[self.selection as usize]
+ .reference
+ .clone(),
+ self.local,
+ ),
+ ));
+ } else if e == self.key_config.merge_branch
+ && !self.selection_is_cur_branch()
+ && self.valid_selection()
+ {
+ try_or_popup!(
+ self,
+ "merge branch error:",
+ self.merge_branch()
+ );
+ self.hide();
+ self.queue.push(InternalEvent::Update(
+ NeedsUpdate::ALL,
+ ));
+ } else if e == self.key_config.tab_toggle {
+ self.local = !self.local;
+ self.update_branches()?;
+ }
+ }
+
+ 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 BranchListComponent {
- pub fn new(
- queue: Queue,
- theme: SharedTheme,
- key_config: SharedKeyConfig,
- ) -> Self {
- Self {
- branches: Vec::new(),
- local: true,
- visible: false,
- selection: 0,
- scroll: VerticalScroll::new(),
- queue,
- theme,
- key_config,
- current_height: Cell::new(0),
- }
- }
-
- ///
- pub fn open(&mut self) -> Result<()> {
- self.show()?;
- self.update_branches()?;
-
- Ok(())
- }
-
- /// fetch list of branches
- pub fn update_branches(&mut self) -> Result<()> {
- if self.is_visible() {
- self.branches = get_branches_info(CWD, self.local)?;
- //remove remote branch called `HEAD`
- if !self.local {
- self.branches
- .iter()
- .position(|b| b.name.ends_with("/HEAD"))
- .map(|idx| self.branches.remove(idx));
- }
- self.set_selection(self.selection)?;
- }
- Ok(())
- }
-
- ///
- pub fn update_git(
- &mut self,
- ev: AsyncGitNotification,
- ) -> Result<()> {
- if self.is_visible() {
- if let AsyncGitNotification::Push = ev {
- self.update_branches()?;
- }
- }
-
- Ok(())
- }
-
- fn valid_selection(&self) -> bool {
- !self.branches.is_empty()
- }
-
- fn merge_branch(&self) -> Result<()> {
- if let Some(branch) =
- self.branches.get(usize::from(self.selection))
- {
- sync::merge_branch(CWD, &branch.name)?;
- }
-
- Ok(())
- }
-
- fn selection_is_cur_branch(&self) -> bool {
- self.branches
- .iter()
- .enumerate()
- .filter(|(index, b)| {
- b.local_details()
- .map(|details| {
- details.is_head
- && *index == self.selection as usize
- })
- .unwrap_or_default()
- })
- .count()
- > 0
- }
-
- ///
- 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()),
- _ => self.selection,
- };
-
- self.set_selection(new_selection)?;
-
- Ok(true)
- }
-
- fn set_selection(&mut self, selection: u16) -> Result<()> {
- let num_branches: u16 = self.branches.len().try_into()?;
- let num_branches = num_branches.saturating_sub(1);
-
- let selection = if selection > num_branches {
- num_branches
- } else {
- selection
- };
-
- self.selection = selection;
-
- Ok(())
- }
-
- /// Get branches to display
- fn get_text(
- &self,
- theme: &SharedTheme,
- width_available: u16,
- height: usize,
- ) -> Text {
- const UPSTREAM_SYMBOL: char = '\u{2191}';
- const HEAD_SYMBOL: char = '*';
- const EMPTY_SYMBOL: char = ' ';
- const THREE_DOTS: &str = "...";
- const COMMIT_HASH_LENGTH: usize = 8;
- const IS_HEAD_STAR_LENGTH: usize = 3; // "* "
- const THREE_DOTS_LENGTH: usize = THREE_DOTS.len(); // "..."
-
- let branch_name_length: usize =
- width_available as usize * 40 / 100;
- // commit message takes up the remaining width
- let commit_message_length: usize = (width_available as usize)
- .saturating_sub(COMMIT_HASH_LENGTH)
- .saturating_sub(branch_name_length)
- .saturating_sub(IS_HEAD_STAR_LENGTH)
- .saturating_sub(THREE_DOTS_LENGTH);
- let mut txt = Vec::new();
-
- for (i, displaybranch) in self
- .branches
- .iter()
- .skip(self.scroll.get_top())
- .take(height)
- .enumerate()
- {
- let mut commit_message =
- displaybranch.top_commit_message.clone();
- if commit_message.len() > commit_message_length {
- commit_message.unicode_truncate(
- commit_message_length
- .saturating_sub(THREE_DOTS_LENGTH),
- );
- commit_message += THREE_DOTS;
- }
-
- let mut branch_name = displaybranch.name.clone();
- if branch_name.len()
- > branch_name_length.saturating_sub(THREE_DOTS_LENGTH)
- {
- branch_name = branch_name
- .unicode_truncate(
- branch_name_length
- .saturating_sub(THREE_DOTS_LENGTH),
- )
- .0
- .to_string();
- branch_name += THREE_DOTS;
- }
-
- let selected = (self.selection as usize
- - self.scroll.get_top())
- == i;
-
- let is_head = displaybranch
- .local_details()
- .map(|details| details.is_head)
- .unwrap_or_default();
- let is_head_str =
- if is_head { HEAD_SYMBOL } else { EMPTY_SYMBOL };
- let has_upstream_str = if displaybranch
- .local_details()
- .map(|details| details.has_upstream)
- .unwrap_or_default()
- {
- UPSTREAM_SYMBOL
- } else {
- EMPTY_SYMBOL
- };
-
- let span_prefix = Span::styled(
- format!("{}{} ", is_head_str, has_upstream_str),
- theme.commit_author(selected),
- );
- let span_hash = Span::styled(
- format!(
- "{} ",
- displaybranch.top_commit.get_short_string()
- ),
- theme.commit_hash(selected),
- );
- let span_msg = Span::styled(
- commit_message.to_string(),
- theme.text(true, selected),
- );
- let span_name = Span::styled(
- format!(
- "{:w$} ",
- branch_name,
- w = branch_name_length
- ),
- theme.branch(selected, is_head),
- );
-
- txt.push(Spans::from(vec![
- span_prefix,
- span_name,
- span_hash,
- span_msg,
- ]));
- }
-
- Text::from(txt)
- }
-
- ///
- fn switch_to_selected_branch(&mut self) -> Result<()> {
- if !self.valid_selection() {
- anyhow::bail!("no valid branch selected");
- }
-
- if self.local {
- checkout_branch(
- asyncgit::CWD,
- &self.branches[self.selection as usize].reference,
- )?;
- self.hide();
- } else {
- checkout_remote_branch(
- CWD,
- &self.branches[self.selection as usize],
- )?;
- self.local = true;
- self.update_branches()?;
- }
-
- self.queue.push(InternalEvent::Update(NeedsUpdate::ALL));
-
- Ok(())
- }
-
- fn draw_tabs<B: Backend>(&self, f: &mut Frame<B>, r: Rect) {
- let tabs = [Span::raw("Local"), Span::raw("Remote")]
- .iter()
- .cloned()
- .map(Spans::from)
- .collect();
-
- f.render_widget(
- Tabs::new(tabs)
- .block(
- Block::default()
- .borders(Borders::BOTTOM)
- .border_style(self.theme.block(false)),
- )
- .style(self.theme.tab(false))
- .highlight_style(self.theme.tab(true))
- .divider(strings::tab_divider(&self.key_config))
- .select(if self.local { 0 } else { 1 }),
- r,
- );
- }
-
- 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.branches.len(),
- height_in_lines,
- );
-
- f.render_widget(
- Paragraph::new(self.get_text(
- &self.theme,
- r.width,
- height_in_lines,
- ))
- .alignment(Alignment::Left),
- r,
- );
-
- let mut r = r;
- r.width += 1;
- r.height += 2;
- r.y = r.y.saturating_sub(1);
-
- self.scroll.draw(f, r, &self.theme);
-
- Ok(())
- }
+ pub fn new(
+ queue: Queue,
+ theme: SharedTheme,
+ key_config: SharedKeyConfig,
+ ) -> Self {
+ Self {
+ branches: Vec::new(),
+ local: true,
+ visible: false,
+ selection: 0,
+ scroll: VerticalScroll::new(),
+ queue,
+ theme,
+ key_config,
+ current_height: Cell::new(0),
+ }
+ }
+
+ ///
+ pub fn open(&mut self) -> Result<()> {
+ self.show()?;
+ self.update_branches()?;
+
+ Ok(())
+ }
+
+ /// fetch list of branches
+ pub fn update_branches(&mut self) -> Result<()> {
+ if self.is_visible() {
+ self.branches = get_branches_info(CWD, self.local)?;
+ //remove remote branch called `HEAD`
+ if !self.local {
+ self.branches
+ .iter()
+ .position(|b| b.name.ends_with("/HEAD"))
+ .map(|idx| self.branches.remove(idx));
+ }
+ self.set_selection(self.selection)?;
+ }
+ Ok(())
+ }
+
+ ///
+ pub fn update_git(
+ &mut self,
+ ev: AsyncGitNotification,
+ ) -> Result<()> {
+ if self.is_visible() {
+ if let AsyncGitNotification::Push = ev {
+ self.update_branches()?;
+ }
+ }
+
+ Ok(())
+ }
+
+ fn valid_selection(&self) -> bool {
+ !self.branches.is_empty()
+ }
+
+ fn merge_branch(&self) -> Result<()> {
+ if let Some(branch) =
+ self.branches.get(usize::from(self.selection))
+ {
+ sync::merge_branch(CWD, &branch.name)?;
+ }
+
+ Ok(())
+ }
+
+ fn selection_is_cur_branch(&self) -> bool {
+ self.branches
+ .iter()
+ .enumerate()
+ .filter(|(index, b)| {
+ b.local_details()
+ .map(|details| {
+ details.is_head
+ && *index == self.selection as usize
+ })
+ .unwrap_or_default()
+ })
+ .count() > 0
+ }
+
+ ///
+ 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()),
+ _ => self.selection,
+ };
+
+ self.set_selection(new_selection)?;
+
+ Ok(true)
+ }
+
+ fn set_selection(&mut self, selection: u16) -> Result<()> {
+ let num_branches: u16 = self.branches.len().try_into()?;
+ let num_branches = num_branches.saturating_sub(1);
+
+ let selection = if selection > num_branches {
+ num_branches
+ } else {
+ selection
+ };
+
+ self.selection = selection;
+
+ Ok(())
+ }
+
+ /// Get branches to display
+ fn get_text(
+ &self,
+ theme: &SharedTheme,
+ width_available: u16,
+ height: usize,
+ ) -> Text {
+ const UPSTREAM_SYMBOL: char = '\u{2191}';
+ const HEAD_SYMBOL: char = '*';
+ const EMPTY_SYMBOL: char = ' ';
+ const THREE_DOTS: &str = "...";
+ const COMMIT_HASH_LENGTH: usize = 8;
+ const IS_HEAD_STAR_LENGTH: usize = 3; // "* "
+ const THREE_DOTS_LENGTH: usize = THREE_DOTS.len(); // "..."
+
+ let branch_name_length: usize =
+ width_available as usize * 40 / 100;
+ // commit message takes up the remaining width
+ let commit_message_length: usize = (width_available as usize)
+ .saturating_sub(COMMIT_HASH_LENGTH)
+ .saturating_sub(branch_name_length)
+ .saturating_sub(IS_HEAD_STAR_LENGTH)
+ .saturating_sub(THREE_DOTS_LENGTH);
+ let mut txt = Vec::new();
+
+ for (i, displaybranch) in self
+ .branches
+ .iter()
+ .skip(self.scroll.get_top())
+ .take(height)
+ .enumerate()
+ {
+ let mut commit_message =
+ displaybranch.top_commit_message.clone();
+ if commit_message.len() > commit_message_length {
+ commit_message.unicode_truncate(
+ commit_message_length
+ .saturating_sub(THREE_DOTS_LENGTH),
+ );
+ commit_message += THREE_DOTS;
+ }
+
+ let mut branch_name = displaybranch.name.clone();
+ if branch_name.len()
+ > branch_name_length.saturating_sub(THREE_DOTS_LENGTH)
+ {
+ branch_name = branch_name
+ .unicode_truncate(
+ branch_name_length
+ .saturating_sub(THREE_DOTS_LENGTH),
+ )
+ .0
+ .to_string();
+ branch_name += THREE_DOTS;
+ }
+
+ let selected = (self.selection as usize
+ - self.scroll.get_top())
+ == i;
+
+ let is_head = displaybranch
+ .local_details()
+ .map(|details| details.is_head)
+ .unwrap_or_default();
+ let is_head_str =
+ if is_head { HEAD_SYMBOL } else { EMPTY_SYMBOL };
+ let has_upstream_str = if displaybranch
+ .local_details()
+ .map(|details| details.has_upstream)
+ .unwrap_or_default()
+ {
+ UPSTREAM_SYMBOL
+ } else {
+ EMPTY_SYMBOL
+ };
+
+ let span_prefix = Span::styled(
+ format!("{}{} ", is_head_str, has_upstream_str),
+ theme.commit_author(selected),
+ );
+ let span_hash = Span::styled(
+ format!(
+ "{} ",
+ displaybranch.top_commit.get_short_string()
+ ),
+ theme.commit_hash(selected),
+ );
+ let span_msg = Span::styled(
+ commit_message.to_string(),
+ theme.text(true, selected),
+ );
+ let span_name = Span::styled(
+ format!(
+ "{:w$} ",
+ branch_name,
+ w = branch_name_length
+ ),
+ theme.branch(selected, is_head),
+ );
+
+ txt.push(Spans::from(vec![
+ span_prefix,
+ span_name,
+ span_hash,
+ span_msg,
+ ]));
+ }
+
+ Text::from(txt)
+ }
+
+ ///
+ fn switch_to_selected_branch(&mut self) -> Result<()> {
+ if !self.valid_selection() {
+ anyhow::bail!("no valid branch selected");
+ }
+
+ if self.local {
+ checkout_branch(
+ asyncgit::CWD,
+ &self.branches[self.selection as usize].reference,
+ )?;
+ self.hide();
+ } else {
+ checkout_remote_branch(
+ CWD,
+ &self.branches[self.selection as usize],
+ )?;
+ self.local = true;
+ self.update_branches()?;
+ }
+
+ self.queue.push(InternalEvent::Update(NeedsUpdate::ALL));
+
+ Ok(())
+ }
+
+ fn draw_tabs<B: Backend>(&self, f: &mut Frame<B>, r: Rect) {
+ let tabs = [Span::raw("Local"), Span::raw("Remote")]
+ .iter()
+ .cloned()
+ .map(Spans::from)
+ .collect();
+
+ f.render_widget(
+ Tabs::new(tabs)
+ .block(
+ Block::default()
+ .borders(Borders::BOTTOM)
+ .border_style(self.theme.block(false)),
+ )
+ .style(self.theme.tab(false))
+ .highlight_style(self.theme.tab(true))
+ .divider(strings::tab_divider(&self.key_config))
+ .select(if self.local { 0 } else { 1 }),
+ r,
+ );
+ }
+
+ 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.branches.len(),
+ height_in_lines,
+ );
+
+ f.render_widget(
+ Paragraph::new(self.get_text(
+ &self.theme,
+ r.width,
+ height_in_lines,
+ ))
+ .alignment(Alignment::Left),
+ r,
+ );
+
+ let mut r = r;
+ r.width += 1;
+ r.height += 2;
+ r.y = r.y.saturating_sub(1);
+
+ self.scroll.draw(f, r, &self.theme);
+
+ Ok(())
+ }
}