mirror of
https://github.com/extrawurst/gitui.git
synced 2024-11-22 02:12:58 +03:00
Move status tab into its own component
This commit is contained in:
parent
fa2aabfee0
commit
0e9ba8aef6
13
.vscode/launch.json
vendored
Normal file
13
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "(OSX) Launch",
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/target/debug/gitui",
|
||||
"args": [],
|
||||
"cwd": "${workspaceRoot}",
|
||||
}
|
||||
]
|
||||
}
|
374
src/app.rs
374
src/app.rs
@ -1,19 +1,16 @@
|
||||
use crate::{
|
||||
accessors,
|
||||
components::{
|
||||
ChangesComponent, CommandBlocking, CommandInfo,
|
||||
CommitComponent, Component, DiffComponent, DrawableComponent,
|
||||
FileTreeItemKind, HelpComponent, MsgComponent,
|
||||
event_pump, CommandBlocking, CommandInfo, CommitComponent,
|
||||
Component, DrawableComponent, HelpComponent, MsgComponent,
|
||||
ResetComponent,
|
||||
},
|
||||
keys,
|
||||
queue::{InternalEvent, NeedsUpdate, Queue},
|
||||
strings,
|
||||
tabs::Revlog,
|
||||
};
|
||||
use asyncgit::{
|
||||
current_tick, sync, AsyncDiff, AsyncNotification, AsyncStatus,
|
||||
DiffParams, CWD,
|
||||
tabs::{Revlog, Status},
|
||||
};
|
||||
use asyncgit::{sync, AsyncNotification, CWD};
|
||||
use crossbeam_channel::Sender;
|
||||
use crossterm::event::Event;
|
||||
use itertools::Itertools;
|
||||
@ -28,56 +25,17 @@ use tui::{
|
||||
Frame,
|
||||
};
|
||||
|
||||
///
|
||||
#[derive(PartialEq)]
|
||||
enum DiffTarget {
|
||||
Stage,
|
||||
WorkingDir,
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(PartialEq)]
|
||||
enum Focus {
|
||||
WorkDir,
|
||||
Diff,
|
||||
Stage,
|
||||
}
|
||||
|
||||
/// allows generating code to make sure
|
||||
/// we always enumerate all components in both getter functions
|
||||
macro_rules! components {
|
||||
($self:ident, [$($element:ident),+]) => {
|
||||
fn components(& $self) -> Vec<&dyn Component> {
|
||||
vec![
|
||||
$(&$self.$element,)+
|
||||
]
|
||||
}
|
||||
|
||||
fn components_mut(&mut $self) -> Vec<&mut dyn Component> {
|
||||
vec![
|
||||
$(&mut $self.$element,)+
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
pub struct App {
|
||||
focus: Focus,
|
||||
diff_target: DiffTarget,
|
||||
do_quit: bool,
|
||||
help: HelpComponent,
|
||||
msg: MsgComponent,
|
||||
reset: ResetComponent,
|
||||
commit: CommitComponent,
|
||||
help: HelpComponent,
|
||||
index: ChangesComponent,
|
||||
index_wd: ChangesComponent,
|
||||
diff: DiffComponent,
|
||||
msg: MsgComponent,
|
||||
git_diff: AsyncDiff,
|
||||
git_status: AsyncStatus,
|
||||
current_commands: Vec<CommandInfo>,
|
||||
tab: usize,
|
||||
revlog: Revlog,
|
||||
status_tab: Status,
|
||||
queue: Queue,
|
||||
}
|
||||
|
||||
@ -87,31 +45,15 @@ impl App {
|
||||
pub fn new(sender: &Sender<AsyncNotification>) -> Self {
|
||||
let queue = Queue::default();
|
||||
Self {
|
||||
focus: Focus::WorkDir,
|
||||
diff_target: DiffTarget::WorkingDir,
|
||||
do_quit: false,
|
||||
reset: ResetComponent::new(queue.clone()),
|
||||
commit: CommitComponent::new(queue.clone()),
|
||||
help: HelpComponent::default(),
|
||||
index_wd: ChangesComponent::new(
|
||||
strings::TITLE_STATUS,
|
||||
true,
|
||||
true,
|
||||
queue.clone(),
|
||||
),
|
||||
index: ChangesComponent::new(
|
||||
strings::TITLE_INDEX,
|
||||
false,
|
||||
false,
|
||||
queue.clone(),
|
||||
),
|
||||
diff: DiffComponent::new(queue.clone()),
|
||||
msg: MsgComponent::default(),
|
||||
git_diff: AsyncDiff::new(sender.clone()),
|
||||
git_status: AsyncStatus::new(sender.clone()),
|
||||
do_quit: false,
|
||||
current_commands: Vec::new(),
|
||||
help: HelpComponent::default(),
|
||||
msg: MsgComponent::default(),
|
||||
tab: 0,
|
||||
revlog: Revlog::new(&sender),
|
||||
status_tab: Status::new(&sender, &queue),
|
||||
queue,
|
||||
}
|
||||
}
|
||||
@ -142,7 +84,7 @@ impl App {
|
||||
);
|
||||
|
||||
if self.tab == 0 {
|
||||
self.draw_status_tab(f, chunks_main[1]);
|
||||
self.status_tab.draw(f, chunks_main[1]);
|
||||
} else {
|
||||
self.revlog.draw(f, chunks_main[1]);
|
||||
}
|
||||
@ -162,40 +104,23 @@ impl App {
|
||||
|
||||
let mut flags = NeedsUpdate::empty();
|
||||
|
||||
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 {
|
||||
if event_pump(ev, self.components_mut().as_mut_slice()) {
|
||||
flags.insert(NeedsUpdate::COMMANDS);
|
||||
} else if let Event::Key(k) = ev {
|
||||
let new_flags = match k {
|
||||
keys::FOCUS_WORKDIR => {
|
||||
self.switch_focus(Focus::WorkDir)
|
||||
}
|
||||
keys::FOCUS_STAGE => self.switch_focus(Focus::Stage),
|
||||
keys::FOCUS_RIGHT if self.can_focus_diff() => {
|
||||
self.switch_focus(Focus::Diff)
|
||||
}
|
||||
keys::FOCUS_LEFT => {
|
||||
self.switch_focus(match self.diff_target {
|
||||
DiffTarget::Stage => Focus::Stage,
|
||||
DiffTarget::WorkingDir => Focus::WorkDir,
|
||||
})
|
||||
}
|
||||
//TODO: move into status tab
|
||||
keys::OPEN_COMMIT
|
||||
if !self.index.is_empty()
|
||||
&& self.offer_open_commit_cmd() =>
|
||||
if self.status_tab.offer_open_commit_cmd() =>
|
||||
{
|
||||
self.commit.show();
|
||||
NeedsUpdate::COMMANDS
|
||||
}
|
||||
|
||||
keys::TAB_TOGGLE => {
|
||||
self.toggle_tabs();
|
||||
NeedsUpdate::COMMANDS
|
||||
}
|
||||
|
||||
_ => NeedsUpdate::empty(),
|
||||
};
|
||||
|
||||
@ -211,28 +136,31 @@ impl App {
|
||||
self.update();
|
||||
}
|
||||
if flags.contains(NeedsUpdate::DIFF) {
|
||||
self.update_diff();
|
||||
self.status_tab.update_diff();
|
||||
}
|
||||
if flags.contains(NeedsUpdate::COMMANDS) {
|
||||
self.update_commands();
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: do we need this?
|
||||
///
|
||||
pub fn update(&mut self) {
|
||||
trace!("update");
|
||||
|
||||
self.git_diff.refresh();
|
||||
self.git_status.fetch(current_tick());
|
||||
self.status_tab.update();
|
||||
}
|
||||
|
||||
///
|
||||
pub fn update_git(&mut self, ev: AsyncNotification) {
|
||||
trace!("update_git: {:?}", ev);
|
||||
|
||||
self.status_tab.update_git(ev);
|
||||
|
||||
match ev {
|
||||
AsyncNotification::Diff => self.update_diff(),
|
||||
AsyncNotification::Status => self.update_status(),
|
||||
AsyncNotification::Diff => (),
|
||||
AsyncNotification::Log => self.revlog.update(),
|
||||
//TODO: is that needed?
|
||||
AsyncNotification::Status => self.update_commands(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -243,58 +171,14 @@ impl App {
|
||||
|
||||
///
|
||||
pub fn any_work_pending(&self) -> bool {
|
||||
self.git_diff.is_pending()
|
||||
|| self.git_status.is_pending()
|
||||
self.status_tab.anything_pending()
|
||||
|| self.revlog.any_work_pending()
|
||||
}
|
||||
}
|
||||
|
||||
// private impls
|
||||
impl App {
|
||||
components!(
|
||||
self,
|
||||
[msg, reset, commit, help, index, index_wd, diff]
|
||||
);
|
||||
|
||||
fn update_diff(&mut self) {
|
||||
if let Some((path, is_stage)) = self.selected_path() {
|
||||
let diff_params = DiffParams(path.clone(), is_stage);
|
||||
|
||||
if self.diff.current() == (path.clone(), is_stage) {
|
||||
// we are already showing a diff of the right file
|
||||
// maybe the diff changed (outside file change)
|
||||
if let Some((params, last)) = self.git_diff.last() {
|
||||
if params == diff_params {
|
||||
self.diff.update(path, is_stage, last);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// we dont show the right diff right now, so we need to request
|
||||
if let Some(diff) = self.git_diff.request(diff_params)
|
||||
{
|
||||
self.diff.update(path, is_stage, diff);
|
||||
} else {
|
||||
self.diff.clear();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.diff.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn selected_path(&self) -> Option<(String, bool)> {
|
||||
let (idx, is_stage) = match self.diff_target {
|
||||
DiffTarget::Stage => (&self.index, true),
|
||||
DiffTarget::WorkingDir => (&self.index_wd, false),
|
||||
};
|
||||
|
||||
if let Some(item) = idx.selection() {
|
||||
if let FileTreeItemKind::File(i) = item.kind {
|
||||
return Some((i.path, is_stage));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
accessors!(self, [msg, reset, commit, help, revlog, status_tab]);
|
||||
|
||||
fn check_quit(&mut self, ev: Event) {
|
||||
if let Event::Key(e) = ev {
|
||||
@ -309,35 +193,20 @@ impl App {
|
||||
self.tab %= 2;
|
||||
|
||||
if self.tab == 1 {
|
||||
self.status_tab.hide();
|
||||
self.revlog.show();
|
||||
} else {
|
||||
self.status_tab.show();
|
||||
self.revlog.hide();
|
||||
}
|
||||
}
|
||||
|
||||
fn can_focus_diff(&self) -> bool {
|
||||
match self.focus {
|
||||
Focus::WorkDir => self.index_wd.is_file_seleted(),
|
||||
Focus::Stage => self.index.is_file_seleted(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_commands(&mut self) {
|
||||
self.help.set_cmds(self.commands(true));
|
||||
self.current_commands = self.commands(false);
|
||||
self.current_commands.sort_by_key(|e| e.order);
|
||||
}
|
||||
|
||||
fn update_status(&mut self) {
|
||||
let status = self.git_status.last();
|
||||
self.index.update(&status.stage);
|
||||
self.index_wd.update(&status.work_dir);
|
||||
|
||||
self.update_diff();
|
||||
self.update_commands();
|
||||
}
|
||||
|
||||
fn process_queue(&mut self) -> NeedsUpdate {
|
||||
let mut flags = NeedsUpdate::empty();
|
||||
loop {
|
||||
@ -379,7 +248,9 @@ impl App {
|
||||
flags.insert(NeedsUpdate::COMMANDS);
|
||||
}
|
||||
InternalEvent::AddHunk(hash) => {
|
||||
if let Some((path, is_stage)) = self.selected_path() {
|
||||
if let Some((path, is_stage)) =
|
||||
self.status_tab.selected_path()
|
||||
{
|
||||
if is_stage {
|
||||
if sync::unstage_hunk(CWD, path, hash) {
|
||||
flags.insert(NeedsUpdate::ALL);
|
||||
@ -402,23 +273,13 @@ impl App {
|
||||
fn commands(&self, force_all: bool) -> Vec<CommandInfo> {
|
||||
let mut res = Vec::new();
|
||||
|
||||
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;
|
||||
}
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
res.push(
|
||||
@ -442,80 +303,6 @@ impl App {
|
||||
res
|
||||
}
|
||||
|
||||
fn offer_open_commit_cmd(&self) -> bool {
|
||||
!self.commit.is_visible()
|
||||
&& self.diff_target == DiffTarget::Stage
|
||||
}
|
||||
|
||||
fn event_pump(
|
||||
ev: Event,
|
||||
components: &mut [&mut dyn Component],
|
||||
) -> bool {
|
||||
for c in components {
|
||||
if c.event(ev) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
@ -532,52 +319,6 @@ 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,
|
||||
@ -617,39 +358,4 @@ impl App {
|
||||
r,
|
||||
);
|
||||
}
|
||||
|
||||
fn switch_focus(&mut self, f: Focus) -> NeedsUpdate {
|
||||
if self.focus == f {
|
||||
NeedsUpdate::empty()
|
||||
} else {
|
||||
self.focus = f;
|
||||
|
||||
match self.focus {
|
||||
Focus::WorkDir => {
|
||||
self.set_diff_target(DiffTarget::WorkingDir);
|
||||
self.diff.focus(false);
|
||||
}
|
||||
Focus::Stage => {
|
||||
self.set_diff_target(DiffTarget::Stage);
|
||||
self.diff.focus(false);
|
||||
}
|
||||
Focus::Diff => {
|
||||
self.index.focus(false);
|
||||
self.index_wd.focus(false);
|
||||
|
||||
self.diff.focus(true);
|
||||
}
|
||||
};
|
||||
|
||||
NeedsUpdate::DIFF | NeedsUpdate::COMMANDS
|
||||
}
|
||||
}
|
||||
|
||||
fn set_diff_target(&mut self, target: DiffTarget) {
|
||||
self.diff_target = target;
|
||||
let is_stage = self.diff_target == DiffTarget::Stage;
|
||||
|
||||
self.index_wd.focus_select(!is_stage);
|
||||
self.index.focus_select(is_stage);
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,38 @@ pub use help::HelpComponent;
|
||||
pub use msg::MsgComponent;
|
||||
pub use reset::ResetComponent;
|
||||
|
||||
/// allows generating code to make sure
|
||||
/// we always enumerate all components in both getter functions
|
||||
#[macro_export]
|
||||
macro_rules! accessors {
|
||||
($self:ident, [$($element:ident),+]) => {
|
||||
fn components(& $self) -> Vec<&dyn Component> {
|
||||
vec![
|
||||
$(&$self.$element,)+
|
||||
]
|
||||
}
|
||||
|
||||
fn components_mut(&mut $self) -> Vec<&mut dyn Component> {
|
||||
vec![
|
||||
$(&mut $self.$element,)+
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn event_pump(
|
||||
ev: Event,
|
||||
components: &mut [&mut dyn Component],
|
||||
) -> bool {
|
||||
for c in components {
|
||||
if c.event(ev) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum ScrollType {
|
||||
Up,
|
||||
|
@ -7,7 +7,7 @@ use crate::{
|
||||
strings, ui,
|
||||
};
|
||||
|
||||
use crossterm::event::{Event, KeyCode};
|
||||
use crossterm::event::{Event, KeyCode, KeyModifiers};
|
||||
use std::borrow::Cow;
|
||||
use strings::commands;
|
||||
use tui::{
|
||||
@ -74,16 +74,24 @@ impl Component for ResetComponent {
|
||||
if self.visible {
|
||||
if let Event::Key(e) = ev {
|
||||
return match e.code {
|
||||
KeyCode::Char(c) => {
|
||||
// ignore and early out on ctrl+c
|
||||
!(c == 'c'
|
||||
&& e.modifiers
|
||||
.contains(KeyModifiers::CONTROL))
|
||||
}
|
||||
|
||||
KeyCode::Esc => {
|
||||
self.hide();
|
||||
true
|
||||
}
|
||||
|
||||
KeyCode::Enter => {
|
||||
self.confirm();
|
||||
true
|
||||
}
|
||||
|
||||
_ => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
mod revlog;
|
||||
|
||||
//TODO: tab traits?
|
||||
mod status;
|
||||
|
||||
pub use revlog::Revlog;
|
||||
pub use status::Status;
|
||||
|
@ -1,3 +1,5 @@
|
||||
mod utils;
|
||||
|
||||
use crate::{
|
||||
components::{
|
||||
CommandBlocking, CommandInfo, Component, ScrollType,
|
||||
@ -6,11 +8,10 @@ use crate::{
|
||||
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, Tags};
|
||||
use sync::Tags;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Alignment, Rect},
|
||||
@ -18,30 +19,7 @@ use tui::{
|
||||
widgets::{Block, Borders, Paragraph, Text},
|
||||
Frame,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
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,
|
||||
msg: c.message,
|
||||
time: time.format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
hash: c.hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
use utils::{ItemBatch, LogEntry};
|
||||
|
||||
const COLOR_SELECTION_BG: Color = Color::Blue;
|
||||
|
||||
@ -64,42 +42,6 @@ const STYLE_MSG_SELECTED: Style =
|
||||
|
||||
static ELEMENTS_PER_LINE: usize = 10;
|
||||
static SLICE_SIZE: usize = 1200;
|
||||
static SLICE_OFFSET_RELOAD_THRESHOLD: usize = 100;
|
||||
|
||||
///
|
||||
#[derive(Default)]
|
||||
struct ItemBatch {
|
||||
index_offset: usize,
|
||||
items: Vec<LogEntry>,
|
||||
}
|
||||
|
||||
impl ItemBatch {
|
||||
fn last_idx(&self) -> usize {
|
||||
self.index_offset + self.items.len()
|
||||
}
|
||||
|
||||
fn set_items(
|
||||
&mut self,
|
||||
start_index: usize,
|
||||
commits: Vec<CommitInfo>,
|
||||
) {
|
||||
self.items.clear();
|
||||
self.items.extend(commits.into_iter().map(LogEntry::from));
|
||||
self.index_offset = start_index;
|
||||
}
|
||||
|
||||
fn needs_data(&self, idx: usize, idx_max: usize) -> bool {
|
||||
let want_min =
|
||||
idx.saturating_sub(SLICE_OFFSET_RELOAD_THRESHOLD);
|
||||
let want_max = idx
|
||||
.saturating_add(SLICE_OFFSET_RELOAD_THRESHOLD)
|
||||
.min(idx_max);
|
||||
|
||||
let needs_data_top = want_min < self.index_offset;
|
||||
let needs_data_bottom = want_max > self.last_idx();
|
||||
needs_data_bottom || needs_data_top
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub struct Revlog {
|
||||
@ -320,26 +262,28 @@ impl Revlog {
|
||||
|
||||
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(ScrollType::Up);
|
||||
true
|
||||
}
|
||||
keys::MOVE_DOWN => {
|
||||
self.move_selection(ScrollType::Down);
|
||||
true
|
||||
}
|
||||
keys::SHIFT_UP | keys::HOME => {
|
||||
self.move_selection(ScrollType::Home);
|
||||
true
|
||||
}
|
||||
keys::SHIFT_DOWN | keys::END => {
|
||||
self.move_selection(ScrollType::End);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if self.visible {
|
||||
if let Event::Key(k) = ev {
|
||||
return match k {
|
||||
keys::MOVE_UP => {
|
||||
self.move_selection(ScrollType::Up);
|
||||
true
|
||||
}
|
||||
keys::MOVE_DOWN => {
|
||||
self.move_selection(ScrollType::Down);
|
||||
true
|
||||
}
|
||||
keys::SHIFT_UP | keys::HOME => {
|
||||
self.move_selection(ScrollType::Home);
|
||||
true
|
||||
}
|
||||
keys::SHIFT_DOWN | keys::END => {
|
||||
self.move_selection(ScrollType::End);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
@ -356,7 +300,11 @@ impl Component for Revlog {
|
||||
self.visible || force_all,
|
||||
));
|
||||
|
||||
CommandBlocking::PassingOn
|
||||
if self.visible {
|
||||
CommandBlocking::Blocking
|
||||
} else {
|
||||
CommandBlocking::PassingOn
|
||||
}
|
||||
}
|
||||
|
||||
fn is_visible(&self) -> bool {
|
63
src/tabs/revlog/utils.rs
Normal file
63
src/tabs/revlog/utils.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use asyncgit::sync::CommitInfo;
|
||||
use chrono::prelude::*;
|
||||
|
||||
static SLICE_OFFSET_RELOAD_THRESHOLD: usize = 100;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct LogEntry {
|
||||
pub time: String,
|
||||
pub author: String,
|
||||
pub msg: String,
|
||||
pub 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,
|
||||
msg: c.message,
|
||||
time: time.format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
hash: c.hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Default)]
|
||||
pub(super) struct ItemBatch {
|
||||
pub index_offset: usize,
|
||||
pub items: Vec<LogEntry>,
|
||||
}
|
||||
|
||||
impl ItemBatch {
|
||||
fn last_idx(&self) -> usize {
|
||||
self.index_offset + self.items.len()
|
||||
}
|
||||
|
||||
pub fn set_items(
|
||||
&mut self,
|
||||
start_index: usize,
|
||||
commits: Vec<CommitInfo>,
|
||||
) {
|
||||
self.items.clear();
|
||||
self.items.extend(commits.into_iter().map(LogEntry::from));
|
||||
self.index_offset = start_index;
|
||||
}
|
||||
|
||||
pub fn needs_data(&self, idx: usize, idx_max: usize) -> bool {
|
||||
let want_min =
|
||||
idx.saturating_sub(SLICE_OFFSET_RELOAD_THRESHOLD);
|
||||
let want_max = idx
|
||||
.saturating_add(SLICE_OFFSET_RELOAD_THRESHOLD)
|
||||
.min(idx_max);
|
||||
|
||||
let needs_data_top = want_min < self.index_offset;
|
||||
let needs_data_bottom = want_max > self.last_idx();
|
||||
needs_data_bottom || needs_data_top
|
||||
}
|
||||
}
|
368
src/tabs/status.rs
Normal file
368
src/tabs/status.rs
Normal file
@ -0,0 +1,368 @@
|
||||
use crate::{
|
||||
accessors,
|
||||
components::{
|
||||
event_pump, ChangesComponent, CommandBlocking, CommandInfo,
|
||||
Component, DiffComponent, DrawableComponent,
|
||||
FileTreeItemKind,
|
||||
},
|
||||
keys,
|
||||
queue::Queue,
|
||||
strings,
|
||||
};
|
||||
use asyncgit::{
|
||||
current_tick, AsyncDiff, AsyncNotification, AsyncStatus,
|
||||
DiffParams,
|
||||
};
|
||||
use crossbeam_channel::Sender;
|
||||
use crossterm::event::Event;
|
||||
use strings::commands;
|
||||
use tui::layout::{Constraint, Direction, Layout};
|
||||
|
||||
///
|
||||
#[derive(PartialEq)]
|
||||
enum Focus {
|
||||
WorkDir,
|
||||
Diff,
|
||||
Stage,
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(PartialEq, Copy, Clone)]
|
||||
enum DiffTarget {
|
||||
Stage,
|
||||
WorkingDir,
|
||||
}
|
||||
|
||||
pub struct Status {
|
||||
visible: bool,
|
||||
focus: Focus,
|
||||
diff_target: DiffTarget,
|
||||
index: ChangesComponent,
|
||||
index_wd: ChangesComponent,
|
||||
diff: DiffComponent,
|
||||
git_diff: AsyncDiff,
|
||||
git_status: AsyncStatus,
|
||||
}
|
||||
|
||||
impl DrawableComponent for Status {
|
||||
fn draw<B: tui::backend::Backend>(
|
||||
&self,
|
||||
f: &mut tui::Frame<B>,
|
||||
rect: tui::layout::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(rect);
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
impl Status {
|
||||
accessors!(self, [index, index_wd, diff]);
|
||||
|
||||
///
|
||||
pub fn new(
|
||||
sender: &Sender<AsyncNotification>,
|
||||
queue: &Queue,
|
||||
) -> Self {
|
||||
Self {
|
||||
visible: true,
|
||||
focus: Focus::WorkDir,
|
||||
diff_target: DiffTarget::WorkingDir,
|
||||
index_wd: ChangesComponent::new(
|
||||
strings::TITLE_STATUS,
|
||||
true,
|
||||
true,
|
||||
queue.clone(),
|
||||
),
|
||||
index: ChangesComponent::new(
|
||||
strings::TITLE_INDEX,
|
||||
false,
|
||||
false,
|
||||
queue.clone(),
|
||||
),
|
||||
diff: DiffComponent::new(queue.clone()),
|
||||
git_diff: AsyncDiff::new(sender.clone()),
|
||||
git_status: AsyncStatus::new(sender.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn can_focus_diff(&self) -> bool {
|
||||
match self.focus {
|
||||
Focus::WorkDir => self.index_wd.is_file_seleted(),
|
||||
Focus::Stage => self.index.is_file_seleted(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: unpub
|
||||
pub fn offer_open_commit_cmd(&self) -> bool {
|
||||
self.visible
|
||||
&& self.diff_target == DiffTarget::Stage
|
||||
&& !self.index.is_empty()
|
||||
}
|
||||
|
||||
fn switch_focus(&mut self, f: Focus) -> bool {
|
||||
if self.focus != f {
|
||||
self.focus = f;
|
||||
|
||||
match self.focus {
|
||||
Focus::WorkDir => {
|
||||
self.set_diff_target(DiffTarget::WorkingDir);
|
||||
self.diff.focus(false);
|
||||
}
|
||||
Focus::Stage => {
|
||||
self.set_diff_target(DiffTarget::Stage);
|
||||
self.diff.focus(false);
|
||||
}
|
||||
Focus::Diff => {
|
||||
self.index.focus(false);
|
||||
self.index_wd.focus(false);
|
||||
|
||||
self.diff.focus(true);
|
||||
}
|
||||
};
|
||||
|
||||
self.update_diff();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn set_diff_target(&mut self, target: DiffTarget) {
|
||||
self.diff_target = target;
|
||||
let is_stage = self.diff_target == DiffTarget::Stage;
|
||||
|
||||
self.index_wd.focus_select(!is_stage);
|
||||
self.index.focus_select(is_stage);
|
||||
}
|
||||
|
||||
pub fn selected_path(&self) -> Option<(String, bool)> {
|
||||
let (idx, is_stage) = match self.diff_target {
|
||||
DiffTarget::Stage => (&self.index, true),
|
||||
DiffTarget::WorkingDir => (&self.index_wd, false),
|
||||
};
|
||||
|
||||
if let Some(item) = idx.selection() {
|
||||
if let FileTreeItemKind::File(i) = item.kind {
|
||||
return Some((i.path, is_stage));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
///
|
||||
pub fn update(&mut self) {
|
||||
self.git_diff.refresh();
|
||||
self.git_status.fetch(current_tick());
|
||||
}
|
||||
|
||||
///
|
||||
pub fn anything_pending(&self) -> bool {
|
||||
self.git_diff.is_pending() || self.git_status.is_pending()
|
||||
}
|
||||
|
||||
///
|
||||
pub fn update_git(&mut self, ev: AsyncNotification) {
|
||||
match ev {
|
||||
AsyncNotification::Diff => self.update_diff(),
|
||||
AsyncNotification::Status => self.update_status(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_status(&mut self) {
|
||||
let status = self.git_status.last();
|
||||
self.index.update(&status.stage);
|
||||
self.index_wd.update(&status.work_dir);
|
||||
|
||||
self.update_diff();
|
||||
}
|
||||
|
||||
///
|
||||
pub fn update_diff(&mut self) {
|
||||
if let Some((path, is_stage)) = self.selected_path() {
|
||||
let diff_params = DiffParams(path.clone(), is_stage);
|
||||
|
||||
if self.diff.current() == (path.clone(), is_stage) {
|
||||
// we are already showing a diff of the right file
|
||||
// maybe the diff changed (outside file change)
|
||||
if let Some((params, last)) = self.git_diff.last() {
|
||||
if params == diff_params {
|
||||
self.diff.update(path, is_stage, last);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// we dont show the right diff right now, so we need to request
|
||||
if let Some(diff) = self.git_diff.request(diff_params)
|
||||
{
|
||||
self.diff.update(path, is_stage, diff);
|
||||
} else {
|
||||
self.diff.clear();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.diff.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Status {
|
||||
fn commands(
|
||||
&self,
|
||||
out: &mut Vec<CommandInfo>,
|
||||
force_all: bool,
|
||||
) -> CommandBlocking {
|
||||
for c in self.components() {
|
||||
if c.commands(out, force_all)
|
||||
!= CommandBlocking::PassingOn
|
||||
&& !force_all
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let focus_on_diff = self.focus == Focus::Diff;
|
||||
out.push(CommandInfo::new(
|
||||
commands::STATUS_FOCUS_LEFT,
|
||||
true,
|
||||
(self.visible && focus_on_diff) || force_all,
|
||||
));
|
||||
out.push(CommandInfo::new(
|
||||
commands::STATUS_FOCUS_RIGHT,
|
||||
self.can_focus_diff(),
|
||||
(self.visible && !focus_on_diff) || force_all,
|
||||
));
|
||||
}
|
||||
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
commands::COMMIT_OPEN,
|
||||
!self.index.is_empty(),
|
||||
(self.visible && self.offer_open_commit_cmd())
|
||||
|| force_all,
|
||||
)
|
||||
.order(-1),
|
||||
);
|
||||
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
commands::SELECT_STATUS,
|
||||
true,
|
||||
(self.visible && self.focus == Focus::Diff)
|
||||
|| force_all,
|
||||
)
|
||||
.hidden(),
|
||||
);
|
||||
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
commands::SELECT_STAGING,
|
||||
true,
|
||||
(self.visible && self.focus == Focus::WorkDir)
|
||||
|| force_all,
|
||||
)
|
||||
.order(-2),
|
||||
);
|
||||
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
commands::SELECT_UNSTAGED,
|
||||
true,
|
||||
(self.visible && self.focus == Focus::Stage)
|
||||
|| force_all,
|
||||
)
|
||||
.order(-2),
|
||||
);
|
||||
|
||||
if self.visible {
|
||||
CommandBlocking::Blocking
|
||||
} else {
|
||||
CommandBlocking::PassingOn
|
||||
}
|
||||
}
|
||||
|
||||
fn event(&mut self, ev: crossterm::event::Event) -> bool {
|
||||
if self.visible {
|
||||
let conusmed =
|
||||
event_pump(ev, self.components_mut().as_mut_slice());
|
||||
|
||||
if conusmed {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Event::Key(k) = ev {
|
||||
return match k {
|
||||
keys::FOCUS_WORKDIR => {
|
||||
self.switch_focus(Focus::WorkDir)
|
||||
}
|
||||
keys::FOCUS_STAGE => {
|
||||
self.switch_focus(Focus::Stage)
|
||||
}
|
||||
keys::FOCUS_RIGHT if self.can_focus_diff() => {
|
||||
self.switch_focus(Focus::Diff)
|
||||
}
|
||||
keys::FOCUS_LEFT => {
|
||||
self.switch_focus(match self.diff_target {
|
||||
DiffTarget::Stage => Focus::Stage,
|
||||
DiffTarget::WorkingDir => Focus::WorkDir,
|
||||
})
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn is_visible(&self) -> bool {
|
||||
self.visible
|
||||
}
|
||||
|
||||
fn hide(&mut self) {
|
||||
self.visible = false;
|
||||
}
|
||||
|
||||
fn show(&mut self) {
|
||||
self.visible = true;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user