diff --git a/CHANGELOG.md b/CHANGELOG.md index 271cfcfe16..274d6467cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +* add sort_by popup to branchlist [[@UUGTech](https://github.com/UUGTech)]([#2146](https://github.com/extrawurst/gitui/issues/2146)) + ## [0.26.0+1] - 2024-04-14 **0.26.1** diff --git a/Cargo.lock b/Cargo.lock index aff3eed553..89550abd69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,6 +153,7 @@ name = "asyncgit" version = "0.26.0" dependencies = [ "bitflags 2.5.0", + "chrono", "crossbeam-channel", "dirs", "easy-cast", @@ -1053,6 +1054,8 @@ dependencies = [ "shellexpand", "simplelog", "struct-patch", + "strum", + "strum_macros", "syntect", "tempfile", "tui-textarea", diff --git a/Cargo.toml b/Cargo.toml index 7bbb5ae873..bbd5f3579f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,8 @@ serde = "1.0" shellexpand = "3.1" simplelog = { version = "0.12", default-features = false } struct-patch = "0.4" +strum = "0.25" +strum_macros = "0.25" syntect = { version = "5.2", default-features = false, features = [ "parsing", "default-syntaxes", diff --git a/asyncgit/Cargo.toml b/asyncgit/Cargo.toml index 50a924a1d5..547efb8b54 100644 --- a/asyncgit/Cargo.toml +++ b/asyncgit/Cargo.toml @@ -13,6 +13,7 @@ keywords = ["git"] [dependencies] bitflags = "2" +chrono = { version = "0.4", default-features = false, features = ["clock"] } crossbeam-channel = "0.5" dirs = "5.0" easy-cast = "0.5" diff --git a/asyncgit/src/sync/branch/mod.rs b/asyncgit/src/sync/branch/mod.rs index 02645cf5fa..eade780bcb 100644 --- a/asyncgit/src/sync/branch/mod.rs +++ b/asyncgit/src/sync/branch/mod.rs @@ -11,8 +11,10 @@ use crate::{ sync::{ remotes::get_default_remote_for_push_in_repo, repository::repo, utils::get_head_repo, CommitId, + CommitSignature, }, }; +use chrono::{DateTime, Local, TimeZone}; use git2::{Branch, BranchType, Repository}; use scopetime::scope_time; use std::collections::HashSet; @@ -92,6 +94,12 @@ pub struct BranchInfo { /// pub top_commit: CommitId, /// + pub top_commit_time: i64, + /// + pub top_commit_time_local: Option>, + /// + pub top_commit_author: String, + /// pub details: BranchDetails, } @@ -181,6 +189,8 @@ pub fn get_branches_info( }) }; + let author = CommitSignature::from(&top_commit.author()); + Ok(BranchInfo { name: bytes2string(name_bytes)?, reference, @@ -188,6 +198,11 @@ pub fn get_branches_info( top_commit.summary_bytes().unwrap_or_default(), )?, top_commit: top_commit.id().into(), + top_commit_time: top_commit.time().seconds(), + top_commit_time_local: Local + .timestamp_opt(top_commit.time().seconds(), 0) + .earliest(), + top_commit_author: author.name, details, }) }) diff --git a/src/app.rs b/src/app.rs index e35da87051..5ba5c60e4e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,14 +10,15 @@ use crate::{ options::{Options, SharedOptions}, popup_stack::PopupStack, popups::{ - AppOption, BlameFilePopup, BranchListPopup, CommitPopup, - CompareCommitsPopup, ConfirmPopup, CreateBranchPopup, - ExternalEditorPopup, FetchPopup, FileRevlogPopup, - FuzzyFindPopup, HelpPopup, InspectCommitPopup, - LogSearchPopupPopup, MsgPopup, OptionsPopup, PullPopup, - PushPopup, PushTagsPopup, RenameBranchPopup, ResetPopup, - RevisionFilesPopup, StashMsgPopup, SubmodulesListPopup, - TagCommitPopup, TagListPopup, + AppOption, BlameFilePopup, BranchListPopup, BranchSortPopup, + CommitPopup, CompareCommitsPopup, ConfirmPopup, + CreateBranchPopup, ExternalEditorPopup, FetchPopup, + FileRevlogPopup, FuzzyFindPopup, HelpPopup, + InspectCommitPopup, LogSearchPopupPopup, MsgPopup, + OptionsPopup, PullPopup, PushPopup, PushTagsPopup, + RenameBranchPopup, ResetPopup, RevisionFilesPopup, + StashMsgPopup, SubmodulesListPopup, TagCommitPopup, + TagListPopup, }, queue::{ Action, AppTabs, InternalEvent, NeedsUpdate, Queue, @@ -88,6 +89,7 @@ pub struct App { create_branch_popup: CreateBranchPopup, rename_branch_popup: RenameBranchPopup, select_branch_popup: BranchListPopup, + sort_branch_popup: BranchSortPopup, options_popup: OptionsPopup, submodule_popup: SubmodulesListPopup, tags_popup: TagListPopup, @@ -196,6 +198,7 @@ impl App { submodule_popup: SubmodulesListPopup::new(&env), log_search_popup: LogSearchPopupPopup::new(&env), fuzzy_find_popup: FuzzyFindPopup::new(&env), + sort_branch_popup: BranchSortPopup::new(&env), do_quit: QuitState::None, cmdbar: RefCell::new(CommandBar::new( env.theme.clone(), @@ -468,6 +471,7 @@ impl App { [ log_search_popup, fuzzy_find_popup, + sort_branch_popup, msg_popup, confirm_popup, commit_popup, @@ -517,6 +521,7 @@ impl App { reset_popup, create_branch_popup, rename_branch_popup, + sort_branch_popup, revision_files_popup, fuzzy_find_popup, log_search_popup, @@ -803,6 +808,17 @@ impl App { flags .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); } + InternalEvent::OpenBranchSortPopup(sort_by) => { + self.sort_branch_popup.open(sort_by)?; + flags + .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); + } + InternalEvent::BranchListSort(sort_by) => { + self.select_branch_popup.change_sort_by(sort_by); + self.select_branch_popup.sort()?; + flags + .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); + } InternalEvent::OptionSwitched(o) => { match o { AppOption::StatusShowUntracked => { diff --git a/src/components/mod.rs b/src/components/mod.rs index 4f1f3f4006..fc0cdb9205 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -18,6 +18,8 @@ pub use commitlist::CommitList; pub use cred::CredComponent; pub use diff::DiffComponent; pub use revision_files::RevisionFilesComponent; +use strum::EnumIter; +use strum_macros::{EnumCount, EnumIs}; pub use syntax_text::SyntaxTextComponent; pub use textinput::{InputType, TextInputComponent}; pub use utils::{ @@ -191,6 +193,16 @@ pub enum FuzzyFinderTarget { Files, } +#[derive(Copy, Clone, EnumCount, EnumIs, EnumIter)] +pub enum BranchListSortBy { + BranchNameAsc, + BranchNameDesc, + LastCommitTimeDesc, + LastCommitTimeAsc, + LastCommitAuthorAsc, + LastCommitAuthorDesc, +} + impl EventState { pub fn is_consumed(&self) -> bool { *self == Self::Consumed diff --git a/src/keys/key_list.rs b/src/keys/key_list.rs index a542ef938a..081a78cdbf 100644 --- a/src/keys/key_list.rs +++ b/src/keys/key_list.rs @@ -108,6 +108,7 @@ pub struct KeysList { pub open_file_tree: GituiKeyEvent, pub file_find: GituiKeyEvent, pub branch_find: GituiKeyEvent, + pub branch_sort: GituiKeyEvent, pub force_push: GituiKeyEvent, pub fetch: GituiKeyEvent, pub pull: GituiKeyEvent, @@ -205,6 +206,7 @@ impl Default for KeysList { open_file_tree: GituiKeyEvent::new(KeyCode::Char('F'), KeyModifiers::SHIFT), file_find: GituiKeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()), branch_find: GituiKeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()), + branch_sort: GituiKeyEvent::new(KeyCode::Char('S'), KeyModifiers::SHIFT), diff_hunk_next: GituiKeyEvent::new(KeyCode::Char('n'), KeyModifiers::empty()), diff_hunk_prev: GituiKeyEvent::new(KeyCode::Char('p'), KeyModifiers::empty()), stage_unstage_item: GituiKeyEvent::new(KeyCode::Enter, KeyModifiers::empty()), diff --git a/src/popups/branch_sort.rs b/src/popups/branch_sort.rs new file mode 100644 index 0000000000..2550a829fa --- /dev/null +++ b/src/popups/branch_sort.rs @@ -0,0 +1,207 @@ +use anyhow::Result; +use crossterm::event::Event; +use ratatui::{ + layout::{Alignment, Margin, Rect}, + text::{Line, Span}, + widgets::{Block, Borders, Clear, Paragraph}, + Frame, +}; +use strum::{EnumCount, IntoEnumIterator}; + +use crate::{ + app::Environment, + components::{ + visibility_blocking, BranchListSortBy, CommandBlocking, + CommandInfo, Component, DrawableComponent, EventState, + }, + keys::{key_match, SharedKeyConfig}, + queue::{InternalEvent, Queue}, + strings, + ui::{self, style::SharedTheme}, +}; + +pub struct BranchSortPopup { + queue: Queue, + visible: bool, + selection: BranchListSortBy, + key_config: SharedKeyConfig, + theme: SharedTheme, +} + +impl BranchSortPopup { + /// + pub fn new(env: &Environment) -> Self { + Self { + queue: env.queue.clone(), + visible: false, + selection: BranchListSortBy::BranchNameAsc, + key_config: env.key_config.clone(), + theme: env.theme.clone(), + } + } + + pub fn open(&mut self, sort_by: BranchListSortBy) -> Result<()> { + self.show()?; + self.update_sort_key(sort_by); + Ok(()) + } + + fn update_sort_key(&mut self, sort_by: BranchListSortBy) { + self.queue.push(InternalEvent::BranchListSort(sort_by)); + } + + fn move_selection(&mut self, up: bool) { + let diff = if up { + BranchListSortBy::COUNT.saturating_sub(1) + } else { + 1 + }; + let new_selection = (self.selection as usize) + .saturating_add(diff) + .rem_euclid(BranchListSortBy::COUNT); + self.selection = BranchListSortBy::iter() + .collect::>()[new_selection]; + } + + fn get_sort_key_lines(&self) -> Vec { + let texts = [ + strings::sort_branch_by_name_msg( + self.selection.is_branch_name_asc(), + ), + strings::sort_branch_by_name_rev_msg( + self.selection.is_branch_name_desc(), + ), + strings::sort_branch_by_time_msg( + self.selection.is_last_commit_time_desc(), + ), + strings::sort_branch_by_time_rev_msg( + self.selection.is_last_commit_time_asc(), + ), + strings::sort_branch_by_author_msg( + self.selection.is_last_commit_author_asc(), + ), + strings::sort_branch_by_author_rev_msg( + self.selection.is_last_commit_author_desc(), + ), + ]; + texts + .iter() + .map(|t| { + let selected = t.starts_with("[X]"); + Line::from(vec![Span::styled( + t.clone(), + self.theme + .popup_selection(selected, selected, false), + )]) + }) + .collect() + } +} + +impl DrawableComponent for BranchSortPopup { + fn draw(&self, f: &mut Frame, area: Rect) -> Result<()> { + if self.is_visible() { + let height = u16::try_from(BranchListSortBy::COUNT)? + .saturating_add(2); + let max_size: (u16, u16) = (50, height); + + let mut area = ui::centered_rect_absolute( + max_size.0, max_size.1, area, + ); + + f.render_widget(Clear, area); + f.render_widget( + Block::default() + .borders(Borders::all()) + .style(self.theme.title(true)) + .title(Span::styled( + strings::POPUP_TITLE_BRANCH_SORT, + self.theme.title(true), + )), + area, + ); + + area = area.inner(&Margin { + horizontal: 1, + vertical: 1, + }); + f.render_widget( + Paragraph::new(self.get_sort_key_lines()) + .block( + Block::default() + .borders(Borders::NONE) + .border_style(self.theme.block(true)), + ) + .alignment(Alignment::Left), + area, + ); + } + Ok(()) + } +} + +impl Component for BranchSortPopup { + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.is_visible() || force_all { + out.push(CommandInfo::new( + strings::commands::close_branch_sort_popup( + &self.key_config, + ), + true, + true, + )); + out.push(CommandInfo::new( + strings::commands::scroll(&self.key_config), + true, + true, + )); + } + + visibility_blocking(self) + } + + fn event( + &mut self, + event: &crossterm::event::Event, + ) -> Result { + if self.is_visible() { + if let Event::Key(key) = event { + if key_match(key, self.key_config.keys.exit_popup) + || key_match(key, self.key_config.keys.enter) + { + self.hide(); + } else if key_match(key, self.key_config.keys.move_up) + { + self.move_selection(true); + self.update_sort_key(self.selection); + } else if key_match( + key, + self.key_config.keys.move_down, + ) { + self.move_selection(false); + self.update_sort_key(self.selection); + } + } + 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(()) + } +} diff --git a/src/popups/branchlist.rs b/src/popups/branchlist.rs index 599b3bea23..67e6b79c08 100644 --- a/src/popups/branchlist.rs +++ b/src/popups/branchlist.rs @@ -1,6 +1,7 @@ use crate::components::{ - visibility_blocking, CommandBlocking, CommandInfo, Component, - DrawableComponent, EventState, FuzzyFinderTarget, VerticalScroll, + visibility_blocking, BranchListSortBy, CommandBlocking, + CommandInfo, Component, DrawableComponent, EventState, + FuzzyFinderTarget, VerticalScroll, }; use crate::{ app::Environment, @@ -44,6 +45,7 @@ use super::InspectCommitOpen; pub struct BranchListPopup { repo: RepoPathRef, branches: Vec, + sort_by: BranchListSortBy, local: bool, has_remotes: bool, visible: bool, @@ -102,6 +104,8 @@ impl DrawableComponent for BranchListPopup { } impl Component for BranchListPopup { + // TODO: clean up + #[allow(clippy::too_many_lines)] fn commands( &self, out: &mut Vec, @@ -212,6 +216,12 @@ impl Component for BranchListPopup { true, )); + out.push(CommandInfo::new( + strings::commands::sort_branch(&self.key_config), + true, + true, + )); + out.push(CommandInfo::new( strings::commands::reset_branch(&self.key_config), self.valid_selection(), @@ -317,6 +327,10 @@ impl Component for BranchListPopup { branches, FuzzyFinderTarget::Branches, )); + } else if key_match(e, self.key_config.keys.branch_sort) { + self.queue.push(InternalEvent::OpenBranchSortPopup( + self.sort_by, + )); } } @@ -342,6 +356,7 @@ impl BranchListPopup { pub fn new(env: &Environment) -> Self { Self { branches: Vec::new(), + sort_by: BranchListSortBy::BranchNameAsc, local: true, has_remotes: false, visible: false, @@ -386,6 +401,7 @@ impl BranchListPopup { self.local = !self.local; self.check_remotes(); self.update_branches()?; + self.set_selection(0)?; } Ok(EventState::NotConsumed) } @@ -412,12 +428,96 @@ impl BranchListPopup { } } + pub fn change_sort_by(&mut self, sort_by: BranchListSortBy) { + self.sort_by = sort_by; + } + + pub fn sort(&mut self) -> Result<()> { + let pre_selected = self + .branches + .get(self.selection as usize) + .map(|b| b.name.clone()); + match &self.sort_by { + BranchListSortBy::LastCommitAuthorAsc => { + self.branches.sort_by(|a, b| { + match b + .top_commit_author + .cmp(&a.top_commit_author) + { + std::cmp::Ordering::Equal => { + a.name.cmp(&b.name) + } + other => other, + } + }); + } + BranchListSortBy::LastCommitAuthorDesc => { + self.branches.sort_by(|a, b| { + match a + .top_commit_author + .cmp(&b.top_commit_author) + { + std::cmp::Ordering::Equal => { + a.name.cmp(&b.name) + } + other => other, + } + }); + } + BranchListSortBy::LastCommitTimeAsc => { + self.branches.sort_by(|a, b| { + match a.top_commit_time.cmp(&b.top_commit_time) { + std::cmp::Ordering::Equal => { + a.name.cmp(&b.name) + } + other => other, + } + }); + } + BranchListSortBy::LastCommitTimeDesc => { + self.branches.sort_by(|a, b| { + match b.top_commit_time.cmp(&a.top_commit_time) { + std::cmp::Ordering::Equal => { + a.name.cmp(&b.name) + } + other => other, + } + }); + } + BranchListSortBy::BranchNameAsc => { + self.branches.sort_by(|a, b| a.name.cmp(&b.name)); + } + BranchListSortBy::BranchNameDesc => { + self.branches.sort_by(|a, b| b.name.cmp(&a.name)); + } + } + + match pre_selected { + Some(pre_selected) => { + let next_selecttion = self + .branches + .iter() + .position(|b| b.name == pre_selected) + .unwrap_or(0); + self.set_selection( + next_selecttion.try_into().unwrap_or_default(), + )?; + } + None => { + self.set_selection(0)?; + } + } + + Ok(()) + } + /// fetch list of branches pub fn update_branches(&mut self) -> Result<()> { if self.is_visible() { self.check_remotes(); self.branches = get_branches_info(&self.repo.borrow(), self.local)?; + self.sort()?; //remove remote branch called `HEAD` if !self.local { self.branches @@ -568,6 +668,46 @@ impl BranchListPopup { Ok(()) } + const fn calculate_shared_commit_message_length( + width_available: u16, + three_dots_length: usize, + ) -> usize { + const COMMIT_HASH_LENGTH: usize = 8; + const COMMIT_DATE_LENGTH: usize = 11; + const IS_HEAD_STAR_LENGTH: usize = 3; + let branch_name_length: usize = + width_available as usize * 40 / 100; + + (width_available as usize) + .saturating_sub(COMMIT_HASH_LENGTH) + .saturating_sub(COMMIT_DATE_LENGTH) + .saturating_sub(branch_name_length) + .saturating_sub(IS_HEAD_STAR_LENGTH) + .saturating_sub(three_dots_length) + } + + fn get_branch_name_text( + branch_name_length: usize, + displaybranch: &BranchInfo, + three_dots_length: usize, + three_dots: &str, + ) -> String { + 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; + } + branch_name + } + /// Get branches to display fn get_text( &self, @@ -581,17 +721,15 @@ impl BranchListPopup { const EMPTY_SYMBOL: char = ' '; const THREE_DOTS: &str = "..."; const THREE_DOTS_LENGTH: usize = THREE_DOTS.len(); // "..." - const COMMIT_HASH_LENGTH: usize = 8; - const IS_HEAD_STAR_LENGTH: usize = 3; // "* " 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 shared_commit_message_length: usize = + Self::calculate_shared_commit_message_length( + width_available, + THREE_DOTS_LENGTH, + ); let mut txt = Vec::new(); for (i, displaybranch) in self @@ -601,6 +739,16 @@ impl BranchListPopup { .take(height) .enumerate() { + let date_text = displaybranch + .top_commit_time_local + .map_or("????-??-?? ".to_string(), |time| { + time.date_naive().to_string() + " " + }); + let author_text = + displaybranch.top_commit_author.clone() + " "; + + let commit_message_length = shared_commit_message_length + .saturating_sub(author_text.len()); let mut commit_message = displaybranch.top_commit_message.clone(); if commit_message.len() > commit_message_length { @@ -611,20 +759,12 @@ impl BranchListPopup { 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 branch_name = Self::get_branch_name_text( + branch_name_length, + displaybranch, + THREE_DOTS_LENGTH, + THREE_DOTS, + ); let selected = (self.selection as usize - self.scroll.get_top()) == i; @@ -657,6 +797,12 @@ impl BranchListPopup { ), theme.commit_hash(selected), ); + let span_date = + Span::styled(date_text, theme.text(true, selected)); + let span_author = Span::styled( + author_text, + theme.commit_author(selected), + ); let span_msg = Span::styled( commit_message.to_string(), theme.text(true, selected), @@ -665,15 +811,15 @@ impl BranchListPopup { format!("{branch_name:branch_name_length$} "), theme.branch(selected, is_head), ); - txt.push(Line::from(vec![ span_prefix, span_name, span_hash, + span_date, + span_author, span_msg, ])); } - Text::from(txt) } diff --git a/src/popups/fuzzy_find.rs b/src/popups/fuzzy_find.rs index 507b64e18f..9cd2ce55f3 100644 --- a/src/popups/fuzzy_find.rs +++ b/src/popups/fuzzy_find.rs @@ -203,7 +203,8 @@ impl FuzzyFindPopup { .map(|(c_idx, c)| { Span::styled( Cow::from(c.to_string()), - self.theme.text( + self.theme.popup_selection( + selected, selected, indices.contains( &(c_idx + trim_length), diff --git a/src/popups/log_search.rs b/src/popups/log_search.rs index e0c57b2cbe..831a374202 100644 --- a/src/popups/log_search.rs +++ b/src/popups/log_search.rs @@ -212,21 +212,24 @@ impl LogSearchPopupPopup { vec![ Line::from(vec![Span::styled( format!("[{x_opt_fuzzy}] fuzzy search"), - self.theme.text( + self.theme.popup_selection( + x_opt_fuzzy == "X", matches!(self.selection, Selection::FuzzyOption), false, ), )]), Line::from(vec![Span::styled( format!("[{x_opt_casesensitive}] case sensitive"), - self.theme.text( + self.theme.popup_selection( + x_opt_casesensitive == "X", matches!(self.selection, Selection::CaseOption), false, ), )]), Line::from(vec![Span::styled( format!("[{x_summary}] summary",), - self.theme.text( + self.theme.popup_selection( + x_summary == "X", matches!( self.selection, Selection::SummarySearch @@ -236,7 +239,8 @@ impl LogSearchPopupPopup { )]), Line::from(vec![Span::styled( format!("[{x_body}] message body",), - self.theme.text( + self.theme.popup_selection( + x_body == "X", matches!( self.selection, Selection::MessageBodySearch @@ -246,7 +250,8 @@ impl LogSearchPopupPopup { )]), Line::from(vec![Span::styled( format!("[{x_files}] committed files",), - self.theme.text( + self.theme.popup_selection( + x_files == "X", matches!( self.selection, Selection::FilenameSearch @@ -256,7 +261,8 @@ impl LogSearchPopupPopup { )]), Line::from(vec![Span::styled( format!("[{x_authors}] authors",), - self.theme.text( + self.theme.popup_selection( + x_authors == "X", matches!( self.selection, Selection::AuthorsSearch diff --git a/src/popups/mod.rs b/src/popups/mod.rs index 2216461ad7..81f1990935 100644 --- a/src/popups/mod.rs +++ b/src/popups/mod.rs @@ -1,4 +1,5 @@ mod blame_file; +mod branch_sort; mod branchlist; mod commit; mod compare_commits; @@ -25,6 +26,7 @@ mod tag_commit; mod taglist; pub use blame_file::{BlameFileOpen, BlameFilePopup}; +pub use branch_sort::BranchSortPopup; pub use branchlist::BranchListPopup; pub use commit::CommitPopup; pub use compare_commits::CompareCommitsPopup; diff --git a/src/queue.rs b/src/queue.rs index 9ee7830bdd..842e528f99 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -1,5 +1,5 @@ use crate::{ - components::FuzzyFinderTarget, + components::{BranchListSortBy, FuzzyFinderTarget}, popups::{ AppOption, BlameFileOpen, FileRevOpen, FileTreeOpen, InspectCommitOpen, @@ -113,6 +113,10 @@ pub enum InternalEvent { /// SelectBranch, /// + OpenBranchSortPopup(BranchListSortBy), + /// + BranchListSort(BranchListSortBy), + /// OpenExternalEditor(Option), /// Push(String, PushType, bool, bool), diff --git a/src/strings.rs b/src/strings.rs index 70ca9e3e8f..6cdc52e525 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -32,6 +32,7 @@ pub static PUSH_TAGS_STATES_DONE: &str = "done"; pub static POPUP_TITLE_SUBMODULES: &str = "Submodules"; pub static POPUP_TITLE_FUZZY_FIND: &str = "Fuzzy Finder"; pub static POPUP_TITLE_LOG_SEARCH: &str = "Search"; +pub static POPUP_TITLE_BRANCH_SORT: &str = "Sort by"; pub static POPUP_FAIL_COPY: &str = "Failed to copy text"; pub static POPUP_SUCCESS_COPY: &str = "Copied Text"; @@ -363,6 +364,43 @@ pub fn rename_branch_popup_msg( "new branch name".to_string() } +pub fn sort_branch_by_name_msg(selected: bool) -> String { + format!( + "[{}] branch name (a → z)", + if selected { "X" } else { " " } + ) +} +pub fn sort_branch_by_name_rev_msg(selected: bool) -> String { + format!( + "[{}] branch name (z → a)", + if selected { "X" } else { " " } + ) +} +pub fn sort_branch_by_time_msg(selected: bool) -> String { + format!( + "[{}] last commit time (new → old)", + if selected { "X" } else { " " } + ) +} +pub fn sort_branch_by_time_rev_msg(selected: bool) -> String { + format!( + "[{}] last commit time (old → new)", + if selected { "X" } else { " " } + ) +} +pub fn sort_branch_by_author_msg(selected: bool) -> String { + format!( + "[{}] last commit author (a → z)", + if selected { "X" } else { " " } + ) +} +pub fn sort_branch_by_author_rev_msg(selected: bool) -> String { + format!( + "[{}] last commit author (z → a)", + if selected { "X" } else { " " } + ) +} + pub fn copy_success(s: &str) -> String { format!("{POPUP_SUCCESS_COPY} \"{s}\"") } @@ -469,6 +507,16 @@ pub mod commands { CMD_GROUP_GENERAL, ) } + pub fn sort_branch(key_config: &SharedKeyConfig) -> CommandText { + CommandText::new( + format!( + "Sort by [{}]", + key_config.get_hint(key_config.keys.branch_sort) + ), + "sort branches", + CMD_GROUP_GENERAL, + ) + } pub fn toggle_tabs_direct( key_config: &SharedKeyConfig, ) -> CommandText { @@ -751,6 +799,19 @@ pub mod commands { CMD_GROUP_GENERAL, ) } + pub fn close_branch_sort_popup( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Close [{}{}]", + key_config.get_hint(key_config.keys.exit_popup), + key_config.get_hint(key_config.keys.enter), + ), + "close branch sort popup", + CMD_GROUP_GENERAL, + ) + } pub fn close_popup(key_config: &SharedKeyConfig) -> CommandText { CommandText::new( format!( diff --git a/src/ui/style.rs b/src/ui/style.rs index e687e45ece..20923a8f89 100644 --- a/src/ui/style.rs +++ b/src/ui/style.rs @@ -1,6 +1,6 @@ use anyhow::Result; use asyncgit::{DiffLineType, StatusItemType}; -use ratatui::style::{Color, Modifier, Style}; +use ratatui::style::{Color, Modifier, Style, Stylize}; use ron::ser::{to_string_pretty, PrettyConfig}; use serde::{Deserialize, Serialize}; use std::{fs::File, io::Write, path::PathBuf, rc::Rc}; @@ -106,6 +106,29 @@ impl Theme { } } + pub fn popup_selection( + &self, + selected: bool, + focused: bool, + colored_bg: bool, + ) -> Style { + let style = match (selected, focused) { + (false, false) => { + Style::default().fg(self.disabled_fg).not_bold() + } + (false, true) => Style::default().not_bold(), + (true, false) => { + Style::default().fg(self.disabled_fg).bold() + } + (true, true) => Style::default().bold(), + }; + if colored_bg { + style.bg(self.selection_bg) + } else { + style + } + } + pub fn item(&self, typ: StatusItemType, selected: bool) -> Style { let style = match typ { StatusItemType::New => {