Log view (#41)

This commit is contained in:
Stephan Dilly 2020-05-05 13:47:21 +02:00 committed by GitHub
parent 4df7704ef5
commit 1db1f00302
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 818 additions and 148 deletions

1
Cargo.lock generated
View file

@ -286,6 +286,7 @@ dependencies = [
"asyncgit",
"backtrace",
"bitflags",
"chrono",
"crossbeam-channel",
"crossterm",
"dirs",

View file

@ -29,6 +29,7 @@ dirs = "2.0"
crossbeam-channel = "0.4"
scopeguard = "1.1"
bitflags = "1.2"
chrono = "0.4"
backtrace = { version = "0.3" }
scopetime = { path = "./scopetime", version = "0.1" }
asyncgit = { path = "./asyncgit", version = "0.2" }

View file

@ -37,4 +37,4 @@ install:
cargo install --path "."
install-debug:
cargo install --features=timing --path "."
cargo install --features=timing --path "." --offline

View file

@ -5,11 +5,13 @@
#![deny(clippy::all)]
mod diff;
mod revlog;
mod status;
pub mod sync;
pub use crate::{
diff::{AsyncDiff, DiffParams},
revlog::AsyncLog,
status::AsyncStatus,
sync::{
diff::{DiffLine, DiffLineType, FileDiff},
@ -30,6 +32,8 @@ pub enum AsyncNotification {
Status,
///
Diff,
///
Log,
}
/// current working director `./`

105
asyncgit/src/revlog.rs Normal file
View file

@ -0,0 +1,105 @@
use crate::{sync, AsyncNotification, CWD};
use crossbeam_channel::Sender;
use git2::Oid;
use scopetime::scope_time;
use std::{
iter::FromIterator,
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
};
use sync::{utils::repo, LogWalker};
///
pub struct AsyncLog {
current: Arc<Mutex<Vec<Oid>>>,
sender: Sender<AsyncNotification>,
pending: Arc<AtomicBool>,
}
static LIMIT_COUNT: usize = 1000;
impl AsyncLog {
///
pub fn new(sender: Sender<AsyncNotification>) -> Self {
Self {
current: Arc::new(Mutex::new(Vec::new())),
sender,
pending: Arc::new(AtomicBool::new(false)),
}
}
///
pub fn count(&mut self) -> usize {
self.current.lock().unwrap().len()
}
///
pub fn get_slice(
&self,
start_index: usize,
amount: usize,
) -> Vec<Oid> {
let list = self.current.lock().unwrap();
let list_len = list.len();
let min = start_index.min(list_len);
let max = min + amount;
let max = max.min(list_len);
Vec::from_iter(list[min..max].iter().cloned())
}
///
pub fn is_pending(&self) -> bool {
self.pending.load(Ordering::Relaxed)
}
///
pub fn fetch(&mut self) {
if !self.is_pending() {
self.clear();
let arc_current = Arc::clone(&self.current);
let sender = self.sender.clone();
let arc_pending = Arc::clone(&self.pending);
rayon_core::spawn(move || {
arc_pending.store(true, Ordering::Relaxed);
scope_time!("async::revlog");
let mut entries = Vec::with_capacity(LIMIT_COUNT);
let r = repo(CWD);
let mut walker = LogWalker::new(&r);
loop {
entries.clear();
let res_is_err = walker
.read(&mut entries, LIMIT_COUNT)
.is_err();
if !res_is_err {
let mut current = arc_current.lock().unwrap();
current.extend(entries.iter());
}
if res_is_err || entries.len() <= 1 {
break;
} else {
Self::notify(&sender);
}
}
arc_pending.store(false, Ordering::Relaxed);
Self::notify(&sender);
});
}
}
fn clear(&mut self) {
self.current.lock().unwrap().clear();
}
fn notify(sender: &Sender<AsyncNotification>) {
sender.send(AsyncNotification::Log).expect("error sending");
}
}

View file

@ -0,0 +1,101 @@
use super::utils::repo;
use git2::{Commit, Error, Oid};
use scopetime::scope_time;
///
#[derive(Debug)]
pub struct CommitInfo {
///
pub message: String,
///
pub time: i64,
///
pub author: String,
///
pub hash: String,
}
///
pub fn get_commits_info(
repo_path: &str,
ids: &[Oid],
) -> Result<Vec<CommitInfo>, Error> {
scope_time!("get_commits_info");
let repo = repo(repo_path);
let commits = ids.iter().map(|id| repo.find_commit(*id).unwrap());
let res = commits
.map(|c: Commit| {
let message = get_message(&c);
let author = if let Some(name) = c.author().name() {
String::from(name)
} else {
String::from("<unknown>")
};
CommitInfo {
message,
author,
time: c.time().seconds(),
hash: c.id().to_string(),
}
})
.collect::<Vec<_>>();
Ok(res)
}
fn get_message(c: &Commit) -> String {
if let Some(msg) = c.message() {
limit_str(msg, 50)
} else {
String::from("<unknown>")
}
}
fn limit_str(s: &str, limit: usize) -> String {
if let Some(first) = s.lines().next() {
first.chars().take(limit).collect::<String>()
} else {
String::new()
}
}
#[cfg(test)]
mod tests {
use super::get_commits_info;
use crate::sync::{
commit, stage_add_file, tests::repo_init_empty,
};
use std::{
fs::File,
io::{Error, Write},
path::Path,
};
#[test]
fn test_log() -> Result<(), Error> {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(&root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path);
let c1 = commit(repo_path, "commit1");
File::create(&root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path);
let c2 = commit(repo_path, "commit2");
let res = get_commits_info(repo_path, &vec![c2, c1]).unwrap();
assert_eq!(res.len(), 2);
assert_eq!(res[0].message.as_str(), "commit2");
assert_eq!(res[0].author.as_str(), "name");
assert_eq!(res[1].message.as_str(), "commit1");
Ok(())
}
}

View file

@ -0,0 +1,117 @@
use git2::{Error, Oid, Repository, Revwalk};
///
pub struct LogWalker<'a> {
repo: &'a Repository,
revwalk: Option<Revwalk<'a>>,
}
impl<'a> LogWalker<'a> {
///
pub fn new(repo: &'a Repository) -> Self {
Self {
repo,
revwalk: None,
}
}
///
pub fn read(
&mut self,
out: &mut Vec<Oid>,
limit: usize,
) -> Result<usize, Error> {
let mut count = 0_usize;
if self.revwalk.is_none() {
let mut walk = self.repo.revwalk()?;
walk.push_head()?;
self.revwalk = Some(walk);
}
if let Some(ref mut walk) = self.revwalk {
for id in walk {
if let Ok(id) = id {
out.push(id);
count += 1;
if count == limit {
break;
}
}
}
}
Ok(count)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sync::{
commit, get_commits_info, stage_add_file,
tests::repo_init_empty,
};
use std::{
fs::File,
io::{Error, Write},
path::Path,
};
#[test]
fn test_limit() -> Result<(), Error> {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(&root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path);
commit(repo_path, "commit1");
File::create(&root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path);
let oid2 = commit(repo_path, "commit2");
let mut items = Vec::new();
let mut walk = LogWalker::new(&repo);
walk.read(&mut items, 1).unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0], oid2);
Ok(())
}
#[test]
fn test_logwalker() -> Result<(), Error> {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(&root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path);
commit(repo_path, "commit1");
File::create(&root.join(file_path))?.write_all(b"a")?;
stage_add_file(repo_path, file_path);
let oid2 = commit(repo_path, "commit2");
let mut items = Vec::new();
let mut walk = LogWalker::new(&repo);
walk.read(&mut items, 100).unwrap();
let info = get_commits_info(repo_path, &items).unwrap();
dbg!(&info);
assert_eq!(items.len(), 2);
assert_eq!(items[0], oid2);
let mut items = Vec::new();
walk.read(&mut items, 100).unwrap();
assert_eq!(items.len(), 0);
Ok(())
}
}

View file

@ -1,14 +1,18 @@
//! sync git api
mod commits_info;
pub mod diff;
mod hooks;
mod hunks;
mod logwalker;
mod reset;
pub mod status;
pub mod utils;
pub use commits_info::{get_commits_info, CommitInfo};
pub use hooks::{hooks_commit_msg, hooks_post_commit, HookResult};
pub use hunks::{stage_hunk, unstage_hunk};
pub use logwalker::LogWalker;
pub use reset::{
reset_stage, reset_workdir_file, reset_workdir_folder,
};

View file

@ -1,6 +1,6 @@
//! sync git api (various methods)
use git2::{IndexAddOption, Repository, RepositoryOpenFlags};
use git2::{IndexAddOption, Oid, Repository, RepositoryOpenFlags};
use scopetime::scope_time;
use std::path::Path;
@ -31,7 +31,7 @@ pub fn repo(repo_path: &str) -> Repository {
}
/// this does not run any git hooks
pub fn commit(repo_path: &str, msg: &str) {
pub fn commit(repo_path: &str, msg: &str) -> Oid {
scope_time!("commit");
let repo = repo(repo_path);
@ -59,7 +59,7 @@ pub fn commit(repo_path: &str, msg: &str) {
&tree,
parents.as_slice(),
)
.unwrap();
.unwrap()
}
/// add a file diff from workingdir to stage (will not add removed files see `stage_addremoved`)

View file

@ -8,6 +8,7 @@ use crate::{
keys,
queue::{InternalEvent, NeedsUpdate, Queue},
strings,
tabs::Revlog,
};
use asyncgit::{
current_tick, sync, AsyncDiff, AsyncNotification, AsyncStatus,
@ -75,13 +76,15 @@ pub struct App {
git_diff: AsyncDiff,
git_status: AsyncStatus,
current_commands: Vec<CommandInfo>,
tab: usize,
revlog: Revlog,
queue: Queue,
}
// public interface
impl App {
///
pub fn new(sender: Sender<AsyncNotification>) -> Self {
pub fn new(sender: &Sender<AsyncNotification>) -> Self {
let queue = Queue::default();
Self {
focus: Focus::WorkDir,
@ -105,8 +108,10 @@ impl App {
diff: DiffComponent::new(queue.clone()),
msg: MsgComponent::default(),
git_diff: AsyncDiff::new(sender.clone()),
git_status: AsyncStatus::new(sender),
git_status: AsyncStatus::new(sender.clone()),
current_commands: Vec::new(),
tab: 0,
revlog: Revlog::new(&sender),
queue,
}
}
@ -128,52 +133,19 @@ impl App {
f.render_widget(
Tabs::default()
.block(Block::default().borders(Borders::BOTTOM))
.titles(&[strings::TAB_STATUS])
.titles(&[strings::TAB_STATUS, strings::TAB_LOG])
.style(Style::default().fg(Color::White))
.highlight_style(Style::default().fg(Color::Yellow))
.divider(strings::TAB_DIVIDER),
.divider(strings::TAB_DIVIDER)
.select(self.tab),
chunks_main[0],
);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(
if self.focus == Focus::Diff {
[
Constraint::Percentage(30),
Constraint::Percentage(70),
]
} else {
[
Constraint::Percentage(50),
Constraint::Percentage(50),
]
}
.as_ref(),
)
.split(chunks_main[1]);
let left_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
if self.diff_target == DiffTarget::WorkingDir {
[
Constraint::Percentage(60),
Constraint::Percentage(40),
]
} else {
[
Constraint::Percentage(40),
Constraint::Percentage(60),
]
}
.as_ref(),
)
.split(chunks[0]);
self.index_wd.draw(f, left_chunks[0]);
self.index.draw(f, left_chunks[1]);
self.diff.draw(f, chunks[1]);
if self.tab == 0 {
self.draw_status_tab(f, chunks_main[1]);
} else {
self.revlog.draw(f, chunks_main[1]);
}
Self::draw_commands(
f,
@ -190,8 +162,13 @@ impl App {
let mut flags = NeedsUpdate::empty();
if Self::event_pump(ev, self.components_mut().as_mut_slice())
{
let event_used = if self.tab == 0 {
Self::event_pump(ev, self.components_mut().as_mut_slice())
} else {
self.revlog.event(ev)
};
if event_used {
flags.insert(NeedsUpdate::COMMANDS);
} else if let Event::Key(k) = ev {
let new_flags = match k {
@ -219,6 +196,10 @@ impl App {
self.commit.show();
NeedsUpdate::COMMANDS
}
keys::TAB_TOGGLE => {
self.toggle_tabs();
NeedsUpdate::COMMANDS
}
_ => NeedsUpdate::empty(),
};
@ -253,6 +234,7 @@ impl App {
match ev {
AsyncNotification::Diff => self.update_diff(),
AsyncNotification::Status => self.update_status(),
AsyncNotification::Log => self.revlog.update(),
}
}
@ -263,7 +245,9 @@ impl App {
///
pub fn any_work_pending(&self) -> bool {
self.git_diff.is_pending() || self.git_status.is_pending()
self.git_diff.is_pending()
|| self.git_status.is_pending()
|| self.revlog.any_work_pending()
}
}
@ -314,6 +298,17 @@ impl App {
None
}
fn toggle_tabs(&mut self) {
self.tab += 1;
self.tab %= 2;
if self.tab == 1 {
self.revlog.show();
} else {
self.revlog.hide();
}
}
fn can_focus_diff(&self) -> bool {
match self.focus {
Focus::WorkDir => self.index_wd.is_file_seleted(),
@ -401,92 +396,33 @@ impl App {
fn commands(&self, force_all: bool) -> Vec<CommandInfo> {
let mut res = Vec::new();
for c in self.components() {
if c.commands(&mut res, force_all)
!= CommandBlocking::PassingOn
&& !force_all
{
break;
if self.revlog.is_visible() {
self.revlog.commands(&mut res, force_all);
} else {
for c in self.components() {
if c.commands(&mut res, force_all)
!= CommandBlocking::PassingOn
&& !force_all
{
break;
}
}
//TODO: move into status tab component
self.add_commands_status_tab(
&mut res,
!self.any_popup_visible(),
);
}
let main_cmds_available = !self.any_popup_visible();
{
{
let focus_on_stage = self.focus == Focus::Stage;
let focus_not_diff = self.focus != Focus::Diff;
res.push(
CommandInfo::new(
commands::STATUS_FOCUS_UNSTAGED,
true,
main_cmds_available
&& focus_on_stage
&& !focus_not_diff,
)
.hidden(),
);
res.push(
CommandInfo::new(
commands::STATUS_FOCUS_STAGED,
true,
main_cmds_available
&& !focus_on_stage
&& !focus_not_diff,
)
.hidden(),
);
}
{
let focus_on_diff = self.focus == Focus::Diff;
res.push(CommandInfo::new(
commands::STATUS_FOCUS_LEFT,
true,
main_cmds_available && focus_on_diff,
));
res.push(CommandInfo::new(
commands::STATUS_FOCUS_RIGHT,
self.can_focus_diff(),
main_cmds_available && !focus_on_diff,
));
}
res.push(
CommandInfo::new(
commands::COMMIT_OPEN,
!self.index.is_empty(),
self.offer_open_commit_cmd(),
)
.order(-1),
);
res.push(
CommandInfo::new(
commands::SELECT_STAGING,
true,
self.focus == Focus::WorkDir,
)
.order(-2),
);
res.push(
CommandInfo::new(
commands::SELECT_UNSTAGED,
true,
self.focus == Focus::Stage,
)
.order(-2),
);
res.push(
CommandInfo::new(
commands::QUIT,
true,
main_cmds_available,
)
.order(100),
);
}
res.push(
CommandInfo::new(
commands::QUIT,
true,
!self.any_popup_visible(),
)
.order(100),
);
res
}
@ -509,6 +445,62 @@ impl App {
false
}
fn add_commands_status_tab(
&self,
res: &mut Vec<CommandInfo>,
main_cmds_available: bool,
) {
{
let focus_on_diff = self.focus == Focus::Diff;
res.push(CommandInfo::new(
commands::STATUS_FOCUS_LEFT,
true,
main_cmds_available && focus_on_diff,
));
res.push(CommandInfo::new(
commands::STATUS_FOCUS_RIGHT,
self.can_focus_diff(),
main_cmds_available && !focus_on_diff,
));
}
res.push(
CommandInfo::new(
commands::COMMIT_OPEN,
!self.index.is_empty(),
main_cmds_available && self.offer_open_commit_cmd(),
)
.order(-1),
);
res.push(
CommandInfo::new(
commands::SELECT_STATUS,
true,
main_cmds_available && self.focus == Focus::Diff,
)
.hidden(),
);
res.push(
CommandInfo::new(
commands::SELECT_STAGING,
true,
main_cmds_available && self.focus == Focus::WorkDir,
)
.order(-2),
);
res.push(
CommandInfo::new(
commands::SELECT_UNSTAGED,
true,
main_cmds_available && self.focus == Focus::Stage,
)
.order(-2),
);
}
fn any_popup_visible(&self) -> bool {
self.commit.is_visible()
|| self.help.is_visible()
@ -525,6 +517,52 @@ impl App {
self.msg.draw(f, size);
}
fn draw_status_tab<B: Backend>(
&self,
f: &mut Frame<B>,
area: Rect,
) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(
if self.focus == Focus::Diff {
[
Constraint::Percentage(30),
Constraint::Percentage(70),
]
} else {
[
Constraint::Percentage(50),
Constraint::Percentage(50),
]
}
.as_ref(),
)
.split(area);
let left_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
if self.diff_target == DiffTarget::WorkingDir {
[
Constraint::Percentage(60),
Constraint::Percentage(40),
]
} else {
[
Constraint::Percentage(40),
Constraint::Percentage(60),
]
}
.as_ref(),
)
.split(chunks[0]);
self.index_wd.draw(f, left_chunks[0]);
self.index.draw(f, left_chunks[1]);
self.diff.draw(f, chunks[1]);
}
fn draw_commands<B: Backend>(
f: &mut Frame<B>,
r: Rect,

View file

@ -14,6 +14,7 @@ const fn with_mod(
KeyEvent { code, modifiers }
}
pub const TAB_TOGGLE: KeyEvent = no_mod(KeyCode::Tab);
pub const FOCUS_WORKDIR: KeyEvent = no_mod(KeyCode::Char('1'));
pub const FOCUS_STAGE: KeyEvent = no_mod(KeyCode::Char('2'));
pub const FOCUS_RIGHT: KeyEvent = no_mod(KeyCode::Right);

View file

@ -10,6 +10,7 @@ mod poll;
mod queue;
mod spinner;
mod strings;
mod tabs;
mod ui;
mod version;
@ -59,13 +60,13 @@ fn main() -> Result<()> {
disable_raw_mode().unwrap();
}
set_panic_handlers();
let mut terminal = start_terminal(io::stdout())?;
let (tx_git, rx_git) = unbounded();
let mut app = App::new(tx_git);
set_panic_handlers();
let mut app = App::new(&tx_git);
let rx_input = poll::start_polling_thread();
let ticker = tick(TICK_INTERVAL);

View file

@ -3,6 +3,7 @@ pub static TITLE_DIFF: &str = "Diff";
pub static TITLE_INDEX: &str = "Staged Changes [2]";
pub static TAB_STATUS: &str = "Status";
pub static TAB_LOG: &str = "Log";
pub static TAB_DIVIDER: &str = " | ";
pub static CMD_SPLITTER: &str = " ";
@ -73,6 +74,12 @@ pub mod commands {
CMD_GROUP_GENERAL,
);
///
pub static SELECT_STATUS: CommandText = CommandText::new(
"Focus Files [1,2]",
"focus/select file tree of staged or unstaged files",
CMD_GROUP_GENERAL,
);
///
pub static SELECT_UNSTAGED: CommandText = CommandText::new(
"Focus Unstaged [1]",
"focus/select unstaged area",
@ -108,18 +115,7 @@ pub mod commands {
"revert changes in selected file or entire path",
CMD_GROUP_CHANGES,
);
///
pub static STATUS_FOCUS_UNSTAGED: CommandText = CommandText::new(
"Unstaged [1]",
"view changes in working dir",
CMD_GROUP_GENERAL,
);
///
pub static STATUS_FOCUS_STAGED: CommandText = CommandText::new(
"Staged [2]",
"view staged changes",
CMD_GROUP_GENERAL,
);
///
pub static STATUS_FOCUS_LEFT: CommandText = CommandText::new(
"Back [\u{2190}]", //←

5
src/tabs/mod.rs Normal file
View file

@ -0,0 +1,5 @@
mod revlog;
//TODO: tab traits?
pub use revlog::Revlog;

296
src/tabs/revlog.rs Normal file
View file

@ -0,0 +1,296 @@
use crate::{
components::{CommandBlocking, CommandInfo, Component},
keys,
strings::commands,
};
use asyncgit::{sync, AsyncLog, AsyncNotification, CWD};
use chrono::prelude::*;
use crossbeam_channel::Sender;
use crossterm::event::Event;
use std::{borrow::Cow, cmp, convert::TryFrom, time::Instant};
use sync::CommitInfo;
use tui::{
backend::Backend,
layout::{Alignment, Rect},
style::{Color, Style},
widgets::{Block, Borders, Paragraph, Text},
Frame,
};
struct LogEntry {
time: String,
author: String,
msg: String,
hash: String,
}
impl From<&CommitInfo> for LogEntry {
fn from(c: &CommitInfo) -> Self {
let time =
DateTime::<Local>::from(DateTime::<Utc>::from_utc(
NaiveDateTime::from_timestamp(c.time, 0),
Utc,
));
Self {
author: c.author.clone(),
msg: c.message.clone(),
time: time.format("%Y-%m-%d %H:%M:%S").to_string(),
hash: c.hash[0..7].to_string(),
}
}
}
const COLOR_SELECTION_BG: Color = Color::Blue;
const STYLE_HASH: Style = Style::new().fg(Color::Magenta);
const STYLE_TIME: Style = Style::new().fg(Color::Blue);
const STYLE_AUTHOR: Style = Style::new().fg(Color::Green);
const STYLE_MSG: Style = Style::new().fg(Color::Reset);
const STYLE_HASH_SELECTED: Style =
Style::new().fg(Color::Magenta).bg(COLOR_SELECTION_BG);
const STYLE_TIME_SELECTED: Style =
Style::new().fg(Color::White).bg(COLOR_SELECTION_BG);
const STYLE_AUTHOR_SELECTED: Style =
Style::new().fg(Color::Green).bg(COLOR_SELECTION_BG);
const STYLE_MSG_SELECTED: Style =
Style::new().fg(Color::Reset).bg(COLOR_SELECTION_BG);
static ELEMENTS_PER_LINE: usize = 8;
static SLICE_SIZE: usize = 1000;
static SLICE_OFFSET_RELOAD_THRESHOLD: usize = 100;
///
pub struct Revlog {
selection: usize,
selection_max: usize,
items: Vec<LogEntry>,
git_log: AsyncLog,
visible: bool,
first_open_done: bool,
scroll_state: (Instant, f32),
}
impl Revlog {
///
pub fn new(sender: &Sender<AsyncNotification>) -> Self {
Self {
items: Vec::new(),
git_log: AsyncLog::new(sender.clone()),
selection: 0,
selection_max: 0,
visible: false,
first_open_done: false,
scroll_state: (Instant::now(), 0_f32),
}
}
///
pub fn draw<B: Backend>(&self, f: &mut Frame<B>, area: Rect) {
let height = area.height as usize;
let selection = self.selection;
let height_d2 = height as usize / 2;
let min = selection.saturating_sub(height_d2);
let mut txt = Vec::new();
for (idx, e) in self.items.iter().enumerate() {
Self::add_entry(e, idx == selection, &mut txt);
}
let title =
format!("commit {}/{}", selection, self.selection_max);
f.render_widget(
Paragraph::new(
txt.iter()
.skip(min * ELEMENTS_PER_LINE)
.take(height * ELEMENTS_PER_LINE),
)
.block(
Block::default()
.borders(Borders::ALL)
.title(title.as_str()),
)
.alignment(Alignment::Left),
area,
);
}
///
pub fn any_work_pending(&self) -> bool {
self.git_log.is_pending()
}
///
pub fn update(&mut self) {
let next_idx = self.items.len();
let requires_more_data = next_idx
.saturating_sub(self.selection)
< SLICE_OFFSET_RELOAD_THRESHOLD;
self.selection_max = self.git_log.count().saturating_sub(1);
if requires_more_data {
let commits = sync::get_commits_info(
CWD,
&self.git_log.get_slice(next_idx, SLICE_SIZE),
);
if let Ok(commits) = commits {
self.items.extend(commits.iter().map(LogEntry::from));
}
}
}
fn move_selection(&mut self, up: bool) {
self.update_scroll_speed();
#[allow(clippy::cast_possible_truncation)]
let speed_int = usize::try_from(self.scroll_state.1 as i64)
.unwrap()
.max(1);
if up {
self.selection = self.selection.saturating_sub(speed_int);
} else {
self.selection = self.selection.saturating_add(speed_int);
}
self.selection = cmp::min(self.selection, self.selection_max);
self.update();
}
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 add_entry<'a>(
e: &'a LogEntry,
selected: bool,
txt: &mut Vec<Text<'a>>,
) {
let count_before = txt.len();
let splitter_txt = Cow::from(" ");
let splitter = if selected {
Text::Styled(
splitter_txt,
Style::new().bg(COLOR_SELECTION_BG),
)
} else {
Text::Raw(splitter_txt)
};
txt.push(Text::Styled(
Cow::from(e.hash.as_str()),
if selected {
STYLE_HASH_SELECTED
} else {
STYLE_HASH
},
));
txt.push(splitter.clone());
txt.push(Text::Styled(
Cow::from(e.time.as_str()),
if selected {
STYLE_TIME_SELECTED
} else {
STYLE_TIME
},
));
txt.push(splitter.clone());
txt.push(Text::Styled(
Cow::from(e.author.as_str()),
if selected {
STYLE_AUTHOR_SELECTED
} else {
STYLE_AUTHOR
},
));
txt.push(splitter);
txt.push(Text::Styled(
Cow::from(e.msg.as_str()),
if selected {
STYLE_MSG_SELECTED
} else {
STYLE_MSG
},
));
txt.push(Text::Raw(Cow::from("\n")));
assert_eq!(txt.len() - count_before, ELEMENTS_PER_LINE);
}
}
impl Component for Revlog {
fn event(&mut self, ev: Event) -> bool {
if let Event::Key(k) = ev {
return match k {
keys::MOVE_UP => {
self.move_selection(true);
true
}
keys::MOVE_DOWN => {
self.move_selection(false);
true
}
_ => false,
};
}
false
}
fn commands(
&self,
out: &mut Vec<CommandInfo>,
force_all: bool,
) -> CommandBlocking {
out.push(CommandInfo::new(
commands::SCROLL,
self.visible,
self.visible || force_all,
));
CommandBlocking::PassingOn
}
fn is_visible(&self) -> bool {
self.visible
}
fn hide(&mut self) {
self.visible = false;
}
fn show(&mut self) {
self.visible = true;
if !self.first_open_done {
self.first_open_done = true;
self.git_log.fetch();
}
}
}