diff options
author | extrawurst <mail@rusticorn.com> | 2024-04-16 08:25:20 +0200 |
---|---|---|
committer | extrawurst <mail@rusticorn.com> | 2024-04-16 08:25:20 +0200 |
commit | 115fd168f5a6fb8b726b8ccee51b05fd722027c8 (patch) | |
tree | af6716ab93062d876e901e365c5fa73c19f7d4a7 /src | |
parent | 47db649e39d98560ffc6f977f0e33e8ff9088d18 (diff) | |
parent | 920c28cfd78b75d84aed7e3fcc543ac821e7afcb (diff) |
Merge branch 'master'ratatui-25-update
Diffstat (limited to 'src')
33 files changed, 269 insertions, 175 deletions
@@ -482,13 +482,13 @@ impl App { pull_popup, fetch_popup, tag_commit_popup, + reset_popup, create_branch_popup, rename_branch_popup, select_branch_popup, revision_files_popup, submodule_popup, tags_popup, - reset_popup, options_popup, help_popup, revlog, diff --git a/src/args.rs b/src/args.rs index 2effe5fc..74654220 100644 --- a/src/args.rs +++ b/src/args.rs @@ -2,7 +2,7 @@ use crate::bug_report; use anyhow::{anyhow, Result}; use asyncgit::sync::RepoPath; use clap::{ - crate_authors, crate_description, crate_name, crate_version, Arg, + crate_authors, crate_description, crate_name, Arg, Command as ClapApp, }; use simplelog::{Config, LevelFilter, WriteLogger}; @@ -63,7 +63,7 @@ pub fn process_cmdline() -> Result<CliArgs> { fn app() -> ClapApp { ClapApp::new(crate_name!()) .author(crate_authors!()) - .version(crate_version!()) + .version(env!("GITUI_BUILD_NAME")) .about(crate_description!()) .help_template( "\ diff --git a/src/components/changes.rs b/src/components/changes.rs index 9a8d5859..3ac42b74 100644 --- a/src/components/changes.rs +++ b/src/components/changes.rs @@ -148,12 +148,9 @@ impl ChangesComponent { fn dispatch_reset_workdir(&mut self) -> bool { if let Some(tree_item) = self.selection() { - let is_folder = - matches!(tree_item.kind, FileTreeItemKind::Path(_)); self.queue.push(InternalEvent::ConfirmAction( Action::Reset(ResetItem { path: tree_item.info.full_path, - is_folder, }), )); diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index 84f9c3ae..44fdb1e2 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -296,7 +296,6 @@ impl CommitList { self.current_size.get() } - #[allow(clippy::missing_const_for_fn)] fn selection_max(&self) -> usize { self.commits.len().saturating_sub(1) } @@ -664,7 +663,6 @@ impl CommitList { }) } - #[allow(clippy::missing_const_for_fn)] fn relative_selection(&self) -> usize { self.selection.saturating_sub(self.items.index_offset()) } diff --git a/src/components/diff.rs b/src/components/diff.rs index a6ce3ee3..b7df165d 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -617,7 +617,6 @@ impl DiffComponent { self.queue.push(InternalEvent::ConfirmAction(Action::Reset( ResetItem { path: self.current.path.clone(), - is_folder: false, }, ))); } diff --git a/src/components/revision_files.rs b/src/components/revision_files.rs index fdd71866..d2f5bb8f 100644 --- a/src/components/revision_files.rs +++ b/src/components/revision_files.rs @@ -101,11 +101,6 @@ impl RevisionFilesComponent { } /// - pub const fn selection(&self) -> Option<usize> { - self.tree.selection() - } - - /// pub fn update(&mut self, ev: AsyncNotification) -> Result<()> { self.current_file.update(ev); diff --git a/src/components/textinput.rs b/src/components/textinput.rs index a92519b3..df87ae9d 100644 --- a/src/components/textinput.rs +++ b/src/components/textinput.rs @@ -88,6 +88,12 @@ impl TextInputComponent { self } + /// + pub fn set_input_type(&mut self, input_type: InputType) { + self.clear(); + self.input_type = input_type; + } + /// Clear the `msg`. pub fn clear(&mut self) { self.msg.take(); @@ -756,7 +762,7 @@ mod tests { if let Some(ta) = &mut comp.textarea { let txt = ta.lines(); assert_eq!(txt[0].len(), 1); - assert_eq!(txt[0].as_bytes()[0], 'a' as u8); + assert_eq!(txt[0].as_bytes()[0], b'a'); } } diff --git a/src/components/utils/filetree.rs b/src/components/utils/filetree.rs index 73ecfc74..8b298dbb 100644 --- a/src/components/utils/filetree.rs +++ b/src/components/utils/filetree.rs @@ -404,9 +404,9 @@ mod tests { ) .unwrap(); - assert_eq!(res.multiple_items_at_path(0), false); - assert_eq!(res.multiple_items_at_path(1), false); - assert_eq!(res.multiple_items_at_path(2), true); + assert!(!res.multiple_items_at_path(0)); + assert!(!res.multiple_items_at_path(1)); + assert!(res.multiple_items_at_path(2)); } #[test] diff --git a/src/components/utils/logitems.rs b/src/components/utils/logitems.rs index 4534f225..67b9e56e 100644 --- a/src/components/utils/logitems.rs +++ b/src/components/utils/logitems.rs @@ -1,5 +1,5 @@ use asyncgit::sync::{CommitId, CommitInfo}; -use chrono::{DateTime, Duration, Local, NaiveDateTime, Utc}; +use chrono::{DateTime, Duration, Local, Utc}; use indexmap::IndexSet; use std::{rc::Rc, slice::Iter}; @@ -27,7 +27,8 @@ impl From<CommitInfo> for LogEntry { let hash_short = c.id.get_short_string().into(); let time = { - let date = NaiveDateTime::from_timestamp_opt(c.time, 0); + let date = DateTime::from_timestamp(c.time, 0) + .map(|d| d.naive_utc()); if date.is_none() { log::error!("error reading commit date: {hash_short} - timestamp: {}",c.time); } @@ -61,8 +62,10 @@ impl From<CommitInfo> for LogEntry { impl LogEntry { pub fn time_to_string(&self, now: DateTime<Local>) -> String { let delta = now - self.time; - if delta < Duration::minutes(30) { - let delta_str = if delta < Duration::minutes(1) { + if delta < Duration::try_minutes(30).unwrap_or_default() { + let delta_str = if delta + < Duration::try_minutes(1).unwrap_or_default() + { "<1m ago".to_string() } else { format!("{:0>2}m ago", delta.num_minutes()) diff --git a/src/components/utils/mod.rs b/src/components/utils/mod.rs index 54298d7a..29485be1 100644 --- a/src/components/utils/mod.rs +++ b/src/components/utils/mod.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, Local, NaiveDateTime, Utc}; +use chrono::{DateTime, Local, Utc}; use unicode_width::UnicodeWidthStr; #[cfg(feature = "ghemoji")] @@ -30,8 +30,9 @@ macro_rules! try_or_popup { pub fn time_to_string(secs: i64, short: bool) -> String { let time = DateTime::<Local>::from( DateTime::<Utc>::from_naive_utc_and_offset( - NaiveDateTime::from_timestamp_opt(secs, 0) - .unwrap_or_default(), + DateTime::from_timestamp(secs, 0) + .unwrap_or_default() + .naive_utc(), Utc, ), ); diff --git a/src/components/utils/statustree.rs b/src/components/utils/statustree.rs index 4b3384d7..59c2b154 100644 --- a/src/components/utils/statustree.rs +++ b/src/components/utils/statustree.rs @@ -509,10 +509,7 @@ mod tests { false, // ] ); - assert_eq!( - res.is_visible_index(res.selection.unwrap()), - true - ); + assert!(res.is_visible_index(res.selection.unwrap())); assert_eq!(res.selection, Some(0)); } diff --git a/src/keys/key_list.rs b/src/keys/key_list.rs index 71e6756d..a542ef93 100644 --- a/src/keys/key_list.rs +++ b/src/keys/key_list.rs @@ -99,6 +99,7 @@ pub struct KeysList { pub delete_branch: GituiKeyEvent, pub merge_branch: GituiKeyEvent, pub rebase_branch: GituiKeyEvent, + pub reset_branch: GituiKeyEvent, pub compare_commits: GituiKeyEvent, pub tags: GituiKeyEvent, pub delete_tag: GituiKeyEvent, @@ -190,6 +191,7 @@ impl Default for KeysList { delete_branch: GituiKeyEvent::new(KeyCode::Char('D'), KeyModifiers::SHIFT), merge_branch: GituiKeyEvent::new(KeyCode::Char('m'), KeyModifiers::empty()), rebase_branch: GituiKeyEvent::new(KeyCode::Char('R'), KeyModifiers::SHIFT), + reset_branch: GituiKeyEvent::new(KeyCode::Char('s'), KeyModifiers::empty()), compare_commits: GituiKeyEvent::new(KeyCode::Char('C'), KeyModifiers::SHIFT), tags: GituiKeyEvent::new(KeyCode::Char('T'), KeyModifiers::SHIFT), delete_tag: GituiKeyEvent::new(KeyCode::Char('D'), KeyModifiers::SHIFT), diff --git a/src/main.rs b/src/main.rs index 91d6649a..26093811 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,8 +21,6 @@ clippy::bool_to_int_with_if, clippy::module_name_repetitions )] -// high number of false positives on nightly (as of Oct 2022 with 1.66.0-nightly) -#![allow(clippy::missing_const_for_fn)] //TODO: // #![deny(clippy::expect_used)] @@ -45,7 +43,6 @@ mod string_utils; mod strings; mod tabs; mod ui; -mod version; mod watcher; use crate::{app::App, args::process_cmdline}; diff --git a/src/popups/blame_file.rs b/src/popups/blame_file.rs index cc1f51e8..9413b7db 100644 --- a/src/popups/blame_file.rs +++ b/src/popups/blame_file.rs @@ -45,11 +45,11 @@ impl SyntaxFileBlame { &self.file_blame.path } - fn commit_id(&self) -> &CommitId { + const fn commit_id(&self) -> &CommitId { &self.file_blame.commit_id } - fn lines(&self) -> &Vec<(Option<BlameHunk>, String)> { + const fn lines(&self) -> &Vec<(Option<BlameHunk>, String)> { &self.file_blame.lines } } @@ -64,7 +64,7 @@ enum BlameProcess { } impl BlameProcess { - fn result(&self) -> Option<&SyntaxFileBlame> { + const fn result(&self) -> Option<&SyntaxFileBlame> { match self { Self::GettingBlame(_) => None, Self::SyntaxHighlighting { @@ -196,8 +196,7 @@ impl Component for BlameFilePopup { let has_result = self .blame .as_ref() - .map(|blame| blame.result().is_some()) - .unwrap_or_default(); + .is_some_and(|blame| blame.result().is_some()); if self.is_visible() || force_all { out.push( CommandInfo::new( @@ -385,7 +384,7 @@ impl BlameFilePopup { } /// - pub fn any_work_pending(&self) -> bool { + pub const fn any_work_pending(&self) -> bool { self.blame.is_some() && !matches!(self.blame, Some(BlameProcess::Result(_))) } @@ -571,7 +570,8 @@ impl BlameFilePopup { .iter() .map(|l| l.1.clone()) .collect::<Vec<_>>(); - let text = tabs_to_spaces(raw_lines.join("\n")); + let mut text = tabs_to_spaces(raw_lines.join("\n")); + text.push('\n'); job.spawn(AsyncSyntaxJob::new( text, diff --git a/src/popups/branchlist.rs b/src/popups/branchlist.rs index f7c9b94d..599b3bea 100644 --- a/src/popups/branchlist.rs +++ b/src/popups/branchlist.rs @@ -211,6 +211,12 @@ impl Component for BranchListPopup { true, true, )); + + out.push(CommandInfo::new( + strings::commands::reset_branch(&self.key_config), + self.valid_selection(), + true, + )); } visibility_blocking(self) } @@ -277,7 +283,7 @@ impl Component for BranchListPopup { ) && self.valid_selection() { self.hide(); - if let Some(commit_id) = self.get_selected() { + if let Some(commit_id) = self.get_selected_commit() { self.queue.push(InternalEvent::OpenPopup( StackablePopupOpen::CompareCommits( InspectCommitOpen::new(commit_id), @@ -288,6 +294,13 @@ impl Component for BranchListPopup { && self.has_remotes { self.queue.push(InternalEvent::FetchRemotes); + } else if key_match(e, self.key_config.keys.reset_branch) + { + if let Some(commit_id) = self.get_selected_commit() { + self.queue.push(InternalEvent::OpenResetPopup( + commit_id, + )); + } } else if key_match( e, self.key_config.keys.cmd_bar_toggle, @@ -466,7 +479,7 @@ impl BranchListPopup { } fn inspect_head_of_branch(&mut self) { - if let Some(commit_id) = self.get_selected() { + if let Some(commit_id) = self.get_selected_commit() { self.hide(); self.queue.push(InternalEvent::OpenPopup( StackablePopupOpen::InspectCommit( @@ -509,7 +522,8 @@ impl BranchListPopup { .count() > 0 } - fn get_selected(&self) -> Option<CommitId> { + // top commit of selected branch + fn get_selected_commit(&self) -> Option<CommitId> { self.branches .get(usize::from(self.selection)) .map(|b| b.top_commit) diff --git a/src/popups/commit.rs b/src/popups/commit.rs index 2511d8b8..9bd48069 100644 --- a/src/popups/commit.rs +++ b/src/popups/commit.rs @@ -11,8 +11,9 @@ use crate::{ ui::style::SharedTheme, }; use anyhow::{bail, Ok, Result}; +use asyncgit::sync::commit::commit_message_prettify; use asyncgit::{ - cached, message_prettify, + cached, sync::{ self, get_config_string, CommitId, HookResult, PrepareCommitMsgSource, RepoPathRef, RepoState, @@ -195,7 +196,8 @@ impl CommitPopup { drop(file); std::fs::remove_file(&file_path)?; - message = message_prettify(message, Some(b'#'))?; + message = + commit_message_prettify(&self.repo.borrow(), message)?; self.input.set_text(message); self.input.show()?; @@ -203,17 +205,6 @@ impl CommitPopup { } fn commit(&mut self) -> Result<()> { - let gpgsign = - get_config_string(&self.repo.borrow(), "commit.gpgsign") - .ok() - .flatten() - .and_then(|path| path.parse::<bool>().ok()) - .unwrap_or_default(); - - if gpgsign { - anyhow::bail!("config commit.gpgsign=true detected.\ngpg signing not supported.\ndeactivate in your repo/gitconfig to be able to commit without signing."); - } - let msg = self.input.get_text().to_string(); if matches!( @@ -254,7 +245,8 @@ impl CommitPopup { } } - let mut msg = message_prettify(msg, Some(b'#'))?; + let mut msg = + commit_message_prettify(&self.repo.borrow(), msg)?; if verify { // run commit message check hook - can reject commit diff --git a/src/popups/help.rs b/src/popups/help.rs index 9933d5c7..d472257a 100644 --- a/src/popups/help.rs +++ b/src/popups/help.rs @@ -6,7 +6,6 @@ use crate::{ app::Environment, keys::{key_match, SharedKeyConfig}, strings, ui, - version::Version, }; use anyhow::Result; use asyncgit::hash; @@ -70,7 +69,10 @@ impl DrawableComponent for HelpPopup { f.render_widget( Paragraph::new(Line::from(vec![Span::styled( - Cow::from(format!("gitui {}", Version::new(),)), + Cow::from(format!( + "gitui {}", + env!("GITUI_BUILD_NAME"), + )), Style::default(), )])) .alignment(Alignment::Right), diff --git a/src/popups/inspect_commit.rs b/src/popups/inspect_commit.rs index 42c95dd2..7ce1aced 100644 --- a/src/popups/inspect_commit.rs +++ b/src/popups/inspect_commit.rs @@ -23,6 +23,8 @@ use ratatui::{ Frame, }; +use super::FileTreeOpen; + #[derive(Clone, Debug)] pub struct InspectCommitOpen { pub commit_id: CommitId, @@ -169,6 +171,24 @@ impl Component for InspectCommitPopup { } else if key_match(e, self.key_config.keys.move_left) { self.hide_stacked(false); + } else if key_match( + e, + self.key_config.keys.open_file_tree, + ) { + if let Some(commit_id) = self + .open_request + .as_ref() + .map(|open_commit| open_commit.commit_id) + { + self.hide_stacked(true); + self.queue.push(InternalEvent::OpenPopup( + StackablePopupOpen::FileTree( + FileTreeOpen::new(commit_id), + ), + )); + return Ok(EventState::Consumed); + } + return Ok(EventState::NotConsumed); } return Ok(EventState::Consumed); diff --git a/src/popups/log_search.rs b/src/popups/log_search.rs index 2513d7ec..e0c57b2c 100644 --- a/src/popups/log_search.rs +++ b/src/popups/log_search.rs @@ -267,7 +267,7 @@ impl LogSearchPopupPopup { ] } - fn option_selected(&self) -> bool { + const fn option_selected(&self) -> bool { !matches!(self.selection, Selection::EnterText) } diff --git a/src/popups/msg.rs b/src/popups/msg.rs index 6da957ce..759c3b26 100644 --- a/src/popups/msg.rs +++ b/src/popups/msg.rs @@ -1,13 +1,16 @@ use crate::components::{ visibility_blocking, CommandBlocking, CommandInfo, Component, - DrawableComponent, EventState, + DrawableComponent, EventState, ScrollType, VerticalScroll, }; +use crate::strings::order; use crate::{ app::Environment, keys::{key_match, SharedKeyConfig}, strings, ui, }; +use anyhow::Result; use crossterm::event::Event; +use ratatui::text::Line; use ratatui::{ layout::{Alignment, Rect}, text::Span, @@ -22,9 +25,12 @@ pub struct MsgPopup { visible: bool, theme: SharedTheme, key_config: SharedKeyConfig, + scroll: VerticalScroll, } -use anyhow::Result; +const POPUP_HEIGHT: u16 = 25; +const BORDER_WIDTH: u16 = 2; +const MINIMUM_WIDTH: u16 = 60; impl DrawableComponent for MsgPopup { fn draw(&self, f: &mut Frame, _rect: Rect) -> Result<()> { @@ -32,29 +38,55 @@ impl DrawableComponent for MsgPopup { return Ok(()); } + let max_width = f.size().width.max(MINIMUM_WIDTH); + // determine the maximum width of text block - let lens = self + let width = self .msg - .split('\n') + .lines() .map(str::len) - .collect::<Vec<usize>>(); - let mut max = lens.iter().max().expect("max") + 2; - if max > std::u16::MAX as usize { - max = std::u16::MAX as usize; - } - let mut width = u16::try_from(max) - .expect("can't fail due to check above"); - // dont overflow screen, and dont get too narrow - if width > f.size().width { - width = f.size().width; - } else if width < 60 { - width = 60; - } + .max() + .unwrap_or(0) + .saturating_add(BORDER_WIDTH.into()) + .clamp(MINIMUM_WIDTH.into(), max_width.into()) + .try_into() + .expect("can't fail because we're clamping to u16 value"); + + let area = + ui::centered_rect_absolute(width, POPUP_HEIGHT, f.size()); + + // Wrap lines and break words if there is not enough space + let wrapped_msg = bwrap::wrap_maybrk!( + &self.msg, + area.width.saturating_sub(BORDER_WIDTH).into() + ); + + let msg_lines: Vec<String> = + wrapped_msg.lines().map(String::from).collect(); + let line_num = msg_lines.len(); + + let height = POPUP_HEIGHT + .saturating_sub(BORDER_WIDTH) + .min(f.size().height.saturating_sub(BORDER_WIDTH)); + + let top = + self.scroll.update_no_selection(line_num, height.into()); + + let scrolled_lines = msg_lines + .iter() + .skip(top) + .take(height.into()) + .map(|line| { + Line::from(vec![Span::styled( + line.clone(), + self.theme.text(true, false), + )]) + }) + .collect::<Vec<Line>>(); - let area = ui::centered_rect_absolute(width, 25, f.size()); f.render_widget(Clear, area); f.render_widget( - Paragraph::new(self.msg.clone()) + Paragraph::new(scrolled_lines) .block( Block::default() .title(Span::styled( @@ -69,6 +101,8 @@ impl DrawableComponent for MsgPopup { area, ); + self.scroll.draw(f, area, &self.theme); + Ok(()) } } @@ -84,6 +118,16 @@ impl Component for MsgPopup { true, self.visible, )); + out.push( + CommandInfo::new( + strings::commands::navigate_commit_message( + &self.key_config, + ), + true, + self.visible, + ) + .order(order::NAV), + ); visibility_blocking(self) } @@ -93,6 +137,14 @@ impl Component for MsgPopup { if let Event::Key(e) = ev { if key_match(e, self.key_config.keys.enter) { self.hide(); + } else if key_match( + e, + self.key_config.keys.popup_down, + ) { + self.scroll.move_top(ScrollType::Down); + } else if key_match(e, self.key_config.keys.popup_up) + { + self.scroll.move_top(ScrollType::Up); } } Ok(EventState::Consumed) @@ -124,24 +176,34 @@ impl MsgPopup { visible: false, theme: env.theme.clone(), key_config: env.key_config.clone(), + scroll: VerticalScroll::new(), } } - /// - pub fn show_error(&mut self, msg: &str) -> Result<()> { - self.title = strings::msg_title_error(&self.key_config); + fn set_new_msg( + &mut self, + msg: &str, + title: String, + ) -> Result<()> { + self.title = title; self.msg = msg.to_string(); - self.show()?; + self.scroll.reset(); + self.show() + } - Ok(()) + /// + pub fn show_error(&mut self, msg: &str) -> Result<()> { + self.set_new_msg( + msg, + strings::msg_title_error(&self.key_config), + ) } /// pub fn show_info(&mut self, msg: &str) -> Result<()> { - self.title = strings::msg_title_info(&self.key_config); - self.msg = msg.to_string(); - self.show()?; - - Ok(()) + self.set_new_msg( + msg, + strings::msg_title_info(&self.key_config), + ) } } diff --git a/src/popups/push.rs b/src/popups/push.rs index a192e2ea..82787cce 100644 --- a/src/popups/push.rs +++ b/src/popups/push.rs @@ -13,10 +13,12 @@ use anyhow::Result; use asyncgit::{ sync::{ cred::{ - extract_username_password, need_username_password, - BasicAuthCredential, + extract_username_password_for_push, + need_username_password_for_push, BasicAuthCredential, }, - get_branch_remote, get_default_remote, RepoPathRef, + get_branch_remote, + remotes::get_default_remote_for_push, + RepoPathRef, }, AsyncGitNotification, AsyncPush, PushRequest, PushType, RemoteProgress, RemoteProgressState, @@ -104,11 +106,11 @@ impl PushPopup { self.show()?; - if need_username_password(&self.repo.borrow())? { - let cred = extract_username_password(&self.repo.borrow()) - .unwrap_or_else(|_| { - BasicAuthCredential::new(None, None) - }); + if need_username_password_for_push(&self.repo.borrow())? { + let cred = extract_username_password_for_push( + &self.repo.borrow(), + ) + .unwrap_or_else(|_| BasicAuthCredential::new(None, None)); if cred.is_complete() { self.push_to_remote(Some(cred), force) } else { @@ -132,7 +134,8 @@ impl PushPopup { remote } else { log::info!("push: branch '{}' has no upstream - looking up default remote",self.branch); - let remote = get_default_remote(&self.repo.borrow())?; + let remote = + get_default_remote_for_push(&self.repo.borrow())?; log::info!( "push: branch '{}' to remote '{}'", self.branch, diff --git a/src/popups/revision_files.rs b/src/popups/revision_files.rs index 92560ccf..9fbe9e25 100644 --- a/src/popups/revision_files.rs +++ b/src/popups/revision_file |