anyhow integration (closes #77)

This commit is contained in:
Mehran Kordi 2020-05-23 17:12:38 +02:00 committed by GitHub
parent fc884525be
commit 4c17660956
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 595 additions and 391 deletions

7
Cargo.lock generated
View File

@ -9,6 +9,12 @@ dependencies = [
"gimli",
]
[[package]]
name = "anyhow"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85bb70cc08ec97ca5450e6eba421deeea5f172c0fc61f78b5357b2a8e8be195f"
[[package]]
name = "arc-swap"
version = "0.4.6"
@ -295,6 +301,7 @@ dependencies = [
name = "gitui"
version = "0.3.0"
dependencies = [
"anyhow",
"asyncgit",
"backtrace",
"bitflags",

View File

@ -35,6 +35,7 @@ chrono = "0.4"
backtrace = "0.3"
ron = "0.6"
serde = "1.0"
anyhow = "1.0.31"
[features]
default=[]

View File

@ -3,6 +3,7 @@
#![forbid(unsafe_code)]
#![forbid(missing_docs)]
#![deny(clippy::all)]
#![deny(clippy::result_unwrap_used)]
mod diff;
mod error;

View File

@ -16,7 +16,7 @@ use sync::status::StatusType;
fn current_tick() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.expect("time before unix epoch!")
.as_millis() as u64
}

View File

@ -2,6 +2,7 @@
#![forbid(unsafe_code)]
#![forbid(missing_docs)]
#![deny(clippy::result_unwrap_used)]
use log::trace;
use std::time::Instant;

View File

@ -11,6 +11,7 @@ use crate::{
tabs::{Revlog, Stashing, Status},
ui::style::Theme,
};
use anyhow::Result;
use asyncgit::{sync, AsyncNotification, CWD};
use crossbeam_channel::Sender;
use crossterm::event::Event;
@ -26,7 +27,6 @@ use tui::{
widgets::{Block, Borders, Paragraph, Tabs, Text},
Frame,
};
///
pub struct App {
do_quit: bool,
@ -73,7 +73,10 @@ impl App {
}
///
pub fn draw<B: Backend>(&mut self, f: &mut Frame<B>) {
pub fn draw<B: Backend>(
&mut self,
f: &mut Frame<B>,
) -> Result<()> {
let chunks_main = Layout::default()
.direction(Direction::Vertical)
.constraints(
@ -90,9 +93,9 @@ impl App {
//TODO: macro because of generic draw call
match self.tab {
0 => self.status_tab.draw(f, chunks_main[1]),
1 => self.revlog.draw(f, chunks_main[1]),
2 => self.stashing_tab.draw(f, chunks_main[1]),
0 => self.status_tab.draw(f, chunks_main[1])?,
1 => self.revlog.draw(f, chunks_main[1])?,
2 => self.stashing_tab.draw(f, chunks_main[1])?,
_ => panic!("unknown tab"),
};
@ -103,25 +106,27 @@ impl App {
self.theme,
);
self.draw_popups(f);
self.draw_popups(f)?;
Ok(())
}
///
pub fn event(&mut self, ev: Event) {
pub fn event(&mut self, ev: Event) -> Result<()> {
trace!("event: {:?}", ev);
if self.check_quit(ev) {
return;
return Ok(());
}
let mut flags = NeedsUpdate::empty();
if event_pump(ev, self.components_mut().as_mut_slice()) {
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::TAB_TOGGLE => {
self.toggle_tabs();
self.toggle_tabs()?;
NeedsUpdate::COMMANDS
}
@ -131,42 +136,51 @@ impl App {
flags.insert(new_flags);
}
let new_flags = self.process_queue();
let new_flags = self.process_queue()?;
flags.insert(new_flags);
if flags.contains(NeedsUpdate::ALL) {
self.update();
self.update()?;
}
if flags.contains(NeedsUpdate::DIFF) {
self.status_tab.update_diff();
self.status_tab.update_diff()?;
}
if flags.contains(NeedsUpdate::COMMANDS) {
self.update_commands();
}
Ok(())
}
//TODO: do we need this?
/// forward ticking to components that require it
pub fn update(&mut self) {
pub fn update(&mut self) -> Result<()> {
trace!("update");
self.status_tab.update();
self.revlog.update();
self.stashing_tab.update();
self.status_tab.update()?;
self.revlog.update()?;
self.stashing_tab.update()?;
Ok(())
}
///
pub fn update_git(&mut self, ev: AsyncNotification) {
pub fn update_git(
&mut self,
ev: AsyncNotification,
) -> Result<()> {
trace!("update_git: {:?}", ev);
self.status_tab.update_git(ev);
self.stashing_tab.update_git(ev);
self.status_tab.update_git(ev)?;
self.stashing_tab.update_git(ev)?;
match ev {
AsyncNotification::Diff => (),
AsyncNotification::Log => self.revlog.update(),
AsyncNotification::Log => self.revlog.update()?,
//TODO: is that needed?
AsyncNotification::Status => self.update_commands(),
}
Ok(())
}
///
@ -216,7 +230,7 @@ impl App {
]
}
fn toggle_tabs(&mut self) {
fn toggle_tabs(&mut self) -> Result<()> {
let mut new_tab = self.tab + 1;
{
let tabs = self.get_tabs();
@ -224,13 +238,15 @@ impl App {
for (i, t) in tabs.into_iter().enumerate() {
if new_tab == i {
t.show();
t.show()?;
} else {
t.hide();
}
}
}
self.tab = new_tab;
Ok(())
}
fn update_commands(&mut self) {
@ -239,25 +255,25 @@ impl App {
self.current_commands.sort_by_key(|e| e.order);
}
fn process_queue(&mut self) -> NeedsUpdate {
fn process_queue(&mut self) -> Result<NeedsUpdate> {
let mut flags = NeedsUpdate::empty();
loop {
let front = self.queue.borrow_mut().pop_front();
if let Some(e) = front {
flags.insert(self.process_internal_event(e));
flags.insert(self.process_internal_event(e)?);
} else {
break;
}
}
self.queue.borrow_mut().clear();
flags
Ok(flags)
}
fn process_internal_event(
&mut self,
ev: InternalEvent,
) -> NeedsUpdate {
) -> Result<NeedsUpdate> {
let mut flags = NeedsUpdate::empty();
match ev {
InternalEvent::ResetItem(reset_item) => {
@ -280,7 +296,7 @@ impl App {
}
}
InternalEvent::ConfirmResetItem(reset_item) => {
self.reset.open_for_path(reset_item);
self.reset.open_for_path(reset_item)?;
flags.insert(NeedsUpdate::COMMANDS);
}
InternalEvent::AddHunk(hash) => {
@ -288,9 +304,7 @@ impl App {
self.status_tab.selected_path()
{
if is_stage {
if sync::unstage_hunk(CWD, path, hash)
.unwrap()
{
if sync::unstage_hunk(CWD, path, hash)? {
flags.insert(NeedsUpdate::ALL);
}
} else if sync::stage_hunk(CWD, path, hash)
@ -301,17 +315,17 @@ impl App {
}
}
InternalEvent::ShowErrorMsg(msg) => {
self.msg.show_msg(msg.as_str());
self.msg.show_msg(msg.as_str())?;
flags.insert(NeedsUpdate::ALL);
}
InternalEvent::Update(u) => flags.insert(u),
InternalEvent::OpenCommit => self.commit.show(),
InternalEvent::OpenCommit => self.commit.show()?,
InternalEvent::PopupStashing(_opts) => {
self.stashmsg_popup.show()
self.stashmsg_popup.show()?
}
};
flags
Ok(flags)
}
fn commands(&self, force_all: bool) -> Vec<CommandInfo> {
@ -354,14 +368,19 @@ impl App {
|| self.msg.is_visible()
}
fn draw_popups<B: Backend>(&mut self, f: &mut Frame<B>) {
fn draw_popups<B: Backend>(
&mut self,
f: &mut Frame<B>,
) -> Result<()> {
let size = f.size();
self.commit.draw(f, size);
self.stashmsg_popup.draw(f, size);
self.reset.draw(f, size);
self.help.draw(f, size);
self.msg.draw(f, size);
self.commit.draw(f, size)?;
self.stashmsg_popup.draw(f, size)?;
self.reset.draw(f, size)?;
self.help.draw(f, size)?;
self.msg.draw(f, size)?;
Ok(())
}
fn draw_tabs<B: Backend>(&self, f: &mut Frame<B>, r: Rect) {

View File

@ -10,6 +10,7 @@ use crate::{
strings,
ui::style::Theme,
};
use anyhow::Result;
use asyncgit::{sync, StatusItem, StatusItemType, CWD};
use crossterm::event::Event;
use std::path::Path;
@ -45,8 +46,10 @@ impl ChangesComponent {
}
///
pub fn update(&mut self, list: &[StatusItem]) {
self.files.update(list)
pub fn update(&mut self, list: &[StatusItem]) -> Result<()> {
self.files.update(list)?;
Ok(())
}
///
@ -69,38 +72,39 @@ impl ChangesComponent {
self.files.is_file_seleted()
}
fn index_add_remove(&mut self) -> bool {
fn index_add_remove(&mut self) -> Result<bool> {
if let Some(tree_item) = self.selection() {
if self.is_working_dir {
if let FileTreeItemKind::File(i) = tree_item.kind {
if let Some(status) = i.status {
let path = Path::new(i.path.as_str());
return match status {
match status {
StatusItemType::Deleted => {
sync::stage_addremoved(CWD, path)
.is_ok()
sync::stage_addremoved(CWD, path)?
}
_ => sync::stage_add_file(CWD, path)
.is_ok(),
_ => sync::stage_add_file(CWD, path)?,
};
return Ok(true);
}
} else {
//TODO: check if we can handle the one file case with it aswell
return sync::stage_add_all(
sync::stage_add_all(
CWD,
tree_item.info.full_path.as_str(),
)
.is_ok();
)?;
return Ok(true);
}
} else {
let path =
Path::new(tree_item.info.full_path.as_str());
sync::reset_stage(CWD, path).unwrap();
return true;
sync::reset_stage(CWD, path)?;
return Ok(true);
}
}
false
Ok(false)
}
fn dispatch_reset_workdir(&mut self) -> bool {
@ -121,8 +125,14 @@ impl ChangesComponent {
}
impl DrawableComponent for ChangesComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, r: Rect) {
self.files.draw(f, r)
fn draw<B: Backend>(
&mut self,
f: &mut Frame<B>,
r: Rect,
) -> Result<()> {
self.files.draw(f, r)?;
Ok(())
}
}
@ -166,9 +176,9 @@ impl Component for ChangesComponent {
CommandBlocking::PassingOn
}
fn event(&mut self, ev: Event) -> bool {
if self.files.event(ev) {
return true;
fn event(&mut self, ev: Event) -> Result<bool> {
if self.files.event(ev)? {
return Ok(true);
}
if self.focused() {
@ -181,29 +191,29 @@ impl Component for ChangesComponent {
self.queue
.borrow_mut()
.push_back(InternalEvent::OpenCommit);
true
Ok(true)
}
keys::STATUS_STAGE_FILE => {
if self.index_add_remove() {
if self.index_add_remove()? {
self.queue.borrow_mut().push_back(
InternalEvent::Update(
NeedsUpdate::ALL,
),
);
}
true
Ok(true)
}
keys::STATUS_RESET_FILE
if self.is_working_dir =>
{
self.dispatch_reset_workdir()
Ok(self.dispatch_reset_workdir())
}
_ => false,
_ => Ok(false),
};
}
}
false
Ok(false)
}
fn focused(&self) -> bool {

View File

@ -7,6 +7,7 @@ use crate::{
strings,
ui::style::Theme,
};
use anyhow::Result;
use asyncgit::{sync, CWD};
use crossterm::event::{Event, KeyCode};
use log::error;
@ -20,8 +21,14 @@ pub struct CommitComponent {
}
impl DrawableComponent for CommitComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, rect: Rect) {
self.input.draw(f, rect)
fn draw<B: Backend>(
&mut self,
f: &mut Frame<B>,
rect: Rect,
) -> Result<()> {
self.input.draw(f, rect)?;
Ok(())
}
}
@ -41,25 +48,27 @@ impl Component for CommitComponent {
visibility_blocking(self)
}
fn event(&mut self, ev: Event) -> bool {
fn event(&mut self, ev: Event) -> Result<bool> {
if self.is_visible() {
if self.input.event(ev) {
return true;
if self.input.event(ev)? {
return Ok(true);
}
if let Event::Key(e) = ev {
match e.code {
KeyCode::Enter if self.can_commit() => {
self.commit();
self.commit()?;
}
_ => (),
};
// stop key event propagation
return true;
return Ok(true);
}
}
false
Ok(false)
}
fn is_visible(&self) -> bool {
@ -70,8 +79,10 @@ impl Component for CommitComponent {
self.input.hide()
}
fn show(&mut self) {
self.input.show()
fn show(&mut self) -> Result<()> {
self.input.show()?;
Ok(())
}
}
@ -88,10 +99,10 @@ impl CommitComponent {
}
}
fn commit(&mut self) {
fn commit(&mut self) -> Result<()> {
let mut msg = self.input.get_text().clone();
if let HookResult::NotOk(e) =
sync::hooks_commit_msg(CWD, &mut msg).unwrap()
sync::hooks_commit_msg(CWD, &mut msg)?
{
error!("commit-msg hook error: {}", e);
self.queue.borrow_mut().push_back(
@ -100,7 +111,7 @@ impl CommitComponent {
e
)),
);
return;
return Ok(());
}
if let Err(e) = sync::commit(CWD, &msg) {
@ -111,12 +122,10 @@ impl CommitComponent {
&e
)),
);
return;
return Ok(());
}
if let HookResult::NotOk(e) =
sync::hooks_post_commit(CWD).unwrap()
{
if let HookResult::NotOk(e) = sync::hooks_post_commit(CWD)? {
error!("post-commit hook error: {}", e);
self.queue.borrow_mut().push_back(
InternalEvent::ShowErrorMsg(format!(
@ -132,6 +141,8 @@ impl CommitComponent {
self.queue
.borrow_mut()
.push_back(InternalEvent::Update(NeedsUpdate::ALL));
Ok(())
}
fn can_commit(&self) -> bool {

View File

@ -18,6 +18,8 @@ use tui::{
Frame,
};
use anyhow::Result;
#[derive(Default)]
struct Current {
path: String,
@ -60,13 +62,15 @@ impl DiffComponent {
(self.current.path.clone(), self.current.is_stage)
}
///
pub fn clear(&mut self) {
pub fn clear(&mut self) -> Result<()> {
self.current = Current::default();
self.diff = FileDiff::default();
self.scroll = 0;
self.selected_hunk =
Self::find_selected_hunk(&self.diff, self.scroll);
Self::find_selected_hunk(&self.diff, self.scroll)?;
Ok(())
}
///
pub fn update(
@ -74,7 +78,7 @@ impl DiffComponent {
path: String,
is_stage: bool,
diff: FileDiff,
) {
) -> Result<()> {
let hash = hash(&diff);
if self.current.hash != hash {
@ -87,11 +91,13 @@ impl DiffComponent {
self.scroll = 0;
self.selected_hunk =
Self::find_selected_hunk(&self.diff, self.scroll);
Self::find_selected_hunk(&self.diff, self.scroll)?;
}
Ok(())
}
fn scroll(&mut self, scroll: ScrollType) {
fn scroll(&mut self, scroll: ScrollType) -> Result<()> {
let old = self.scroll;
let scroll_max = self.diff.lines.saturating_sub(1);
@ -113,17 +119,19 @@ impl DiffComponent {
if old != self.scroll {
self.selected_hunk =
Self::find_selected_hunk(&self.diff, self.scroll);
Self::find_selected_hunk(&self.diff, self.scroll)?;
}
Ok(())
}
fn find_selected_hunk(
diff: &FileDiff,
line_selected: u16,
) -> Option<u16> {
) -> Result<Option<u16>> {
let mut line_cursor = 0_u16;
for (i, hunk) in diff.hunks.iter().enumerate() {
let hunk_len = u16::try_from(hunk.lines.len()).unwrap();
let hunk_len = u16::try_from(hunk.lines.len())?;
let hunk_min = line_cursor;
let hunk_max = line_cursor + hunk_len;
@ -131,16 +139,16 @@ impl DiffComponent {
hunk_min <= line_selected && hunk_max > line_selected;
if hunk_selected {
return Some(u16::try_from(i).unwrap());
return Ok(Some(u16::try_from(i)?));
}
line_cursor += hunk_len;
}
None
Ok(None)
}
fn get_text(&self, width: u16, height: u16) -> Vec<Text> {
fn get_text(&self, width: u16, height: u16) -> Result<Vec<Text>> {
let selection = self.scroll;
let height_d2 = height / 2;
let min = self.scroll.saturating_sub(height_d2);
@ -151,15 +159,16 @@ impl DiffComponent {
let mut lines_added = 0_u16;
for (i, hunk) in self.diff.hunks.iter().enumerate() {
let hunk_selected = self
.selected_hunk
.map_or(false, |s| s == u16::try_from(i).unwrap());
let hunk_selected =
self.selected_hunk.map_or(false, |s| {
s == u16::try_from(i).unwrap_or_default()
});
if lines_added >= height {
break;
}
let hunk_len = u16::try_from(hunk.lines.len()).unwrap();
let hunk_len = u16::try_from(hunk.lines.len())?;
let hunk_min = line_cursor;
let hunk_max = line_cursor + hunk_len;
@ -184,7 +193,7 @@ impl DiffComponent {
line_cursor += hunk_len;
}
}
res
Ok(res)
}
fn add_line(
@ -258,25 +267,29 @@ impl DiffComponent {
false
}
fn add_hunk(&self) {
fn add_hunk(&self) -> Result<()> {
if let Some(hunk) = self.selected_hunk {
let hash = self.diff.hunks
[usize::try_from(hunk).unwrap()]
.header_hash;
let hash = self.diff.hunks[usize::from(hunk)].header_hash;
self.queue
.borrow_mut()
.push_back(InternalEvent::AddHunk(hash));
}
Ok(())
}
}
impl DrawableComponent for DiffComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, r: Rect) {
fn draw<B: Backend>(
&mut self,
f: &mut Frame<B>,
r: Rect,
) -> Result<()> {
self.current_height = r.height.saturating_sub(2);
let title =
format!("{}{}", strings::TITLE_DIFF, self.current.path);
f.render_widget(
Paragraph::new(self.get_text(r.width, r.height).iter())
Paragraph::new(self.get_text(r.width, r.height)?.iter())
.block(
Block::default()
.title(title.as_str())
@ -287,6 +300,8 @@ impl DrawableComponent for DiffComponent {
.alignment(Alignment::Left),
r,
);
Ok(())
}
}
@ -325,44 +340,44 @@ impl Component for DiffComponent {
CommandBlocking::PassingOn
}
fn event(&mut self, ev: Event) -> bool {
fn event(&mut self, ev: Event) -> Result<bool> {
if self.focused {
if let Event::Key(e) = ev {
return match e {
keys::MOVE_DOWN => {
self.scroll(ScrollType::Down);
true
self.scroll(ScrollType::Down)?;
Ok(true)
}
keys::SHIFT_DOWN | keys::END => {
self.scroll(ScrollType::End);
true
self.scroll(ScrollType::End)?;
Ok(true)
}
keys::HOME | keys::SHIFT_UP => {
self.scroll(ScrollType::Home);
true
self.scroll(ScrollType::Home)?;
Ok(true)
}
keys::MOVE_UP => {
self.scroll(ScrollType::Up);
true
self.scroll(ScrollType::Up)?;
Ok(true)
}
keys::PAGE_UP => {
self.scroll(ScrollType::PageUp);
true
self.scroll(ScrollType::PageUp)?;
Ok(true)
}
keys::PAGE_DOWN => {
self.scroll(ScrollType::PageDown);
true
self.scroll(ScrollType::PageDown)?;
Ok(true)
}
keys::ENTER => {
self.add_hunk();
true
self.add_hunk()?;
Ok(true)
}
_ => false,
_ => Ok(false),
};
}
}
false
Ok(false)
}
fn focused(&self) -> bool {

View File

@ -12,6 +12,8 @@ use crate::{
strings, ui,
ui::style::Theme,
};
use anyhow::Result;
use asyncgit::{hash, StatusItem, StatusItemType};
use crossterm::event::Event;
use std::{borrow::Cow, convert::From, path::Path};
@ -49,12 +51,14 @@ impl FileTreeComponent {
}
///
pub fn update(&mut self, list: &[StatusItem]) {
pub fn update(&mut self, list: &[StatusItem]) -> Result<()> {
let new_hash = hash(list);
if self.current_hash != new_hash {
self.tree.update(list);
self.tree.update(list)?;
self.current_hash = new_hash;
}
Ok(())
}
///
@ -119,9 +123,8 @@ impl FileTreeComponent {
Self::item_status_char(status_item.status);
let file = Path::new(&status_item.path)
.file_name()
.unwrap()
.to_str()
.unwrap();
.and_then(std::ffi::OsStr::to_str)
.expect("invalid path.");
let txt = if selected {
format!(
@ -188,7 +191,11 @@ impl FileTreeComponent {
}
impl DrawableComponent for FileTreeComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, r: Rect) {
fn draw<B: Backend>(
&mut self,
f: &mut Frame<B>,
r: Rect,
) -> Result<()> {
let selection_offset =
self.tree.tree.items().iter().enumerate().fold(
0,
@ -230,6 +237,8 @@ impl DrawableComponent for FileTreeComponent {
self.focused,
self.theme,
);
Ok(())
}
}
@ -248,34 +257,34 @@ impl Component for FileTreeComponent {
CommandBlocking::PassingOn
}
fn event(&mut self, ev: Event) -> bool {
fn event(&mut self, ev: Event) -> Result<bool> {
if self.focused {
if let Event::Key(e) = ev {
return match e {
keys::MOVE_DOWN => {
self.move_selection(MoveSelection::Down)
Ok(self.move_selection(MoveSelection::Down))
}
keys::MOVE_UP => {
self.move_selection(MoveSelection::Up)
Ok(self.move_selection(MoveSelection::Up))
}
keys::HOME | keys::SHIFT_UP => {
self.move_selection(MoveSelection::Home)
Ok(self.move_selection(MoveSelection::Home))
}
keys::END | keys::SHIFT_DOWN => {
self.move_selection(MoveSelection::End)
Ok(self.move_selection(MoveSelection::End))
}
keys::MOVE_LEFT => {
self.move_selection(MoveSelection::Left)
Ok(self.move_selection(MoveSelection::Left))
}
keys::MOVE_RIGHT => {
self.move_selection(MoveSelection::Right)
Ok(self.move_selection(MoveSelection::Right))
}
_ => false,
_ => Ok(false),
};
}
}
false
Ok(false)
}
fn focused(&self) -> bool {

View File

@ -16,6 +16,8 @@ use tui::{
Frame,
};
use anyhow::Result;
///
pub struct HelpComponent {
cmds: Vec<CommandInfo>,
@ -25,7 +27,11 @@ pub struct HelpComponent {
}
impl DrawableComponent for HelpComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, _rect: Rect) {
fn draw<B: Backend>(
&mut self,
f: &mut Frame<B>,
_rect: Rect,
) -> Result<()> {
if self.visible {
const SIZE: (u16, u16) = (65, 24);
let scroll_threshold = SIZE.1 / 3;
@ -75,6 +81,8 @@ impl DrawableComponent for HelpComponent {
chunks[1],
);
}
Ok(())
}
}
@ -113,7 +121,7 @@ impl Component for HelpComponent {
visibility_blocking(self)
}
fn event(&mut self, ev: Event) -> bool {
fn event(&mut self, ev: Event) -> Result<bool> {
if self.visible {
if let Event::Key(e) = ev {
match e {
@ -124,12 +132,12 @@ impl Component for HelpComponent {
}
}
true
Ok(true)
} else if let Event::Key(keys::OPEN_HELP) = ev {
self.show();
true
self.show()?;
Ok(true)
} else {
false
Ok(false)
}
}
@ -141,8 +149,10 @@ impl Component for HelpComponent {
self.visible = false
}
fn show(&mut self) {
self.visible = true
fn show(&mut self) -> Result<()> {
self.visible = true;
Ok(())
}
}

View File

@ -9,9 +9,11 @@ mod reset;
mod stashmsg;
mod textinput;
mod utils;
use anyhow::Result;
pub use changes::ChangesComponent;
pub use command::{CommandInfo, CommandText};
pub use commit::CommitComponent;
use crossterm::event::Event;
pub use diff::DiffComponent;
pub use filetree::FileTreeComponent;
pub use help::HelpComponent;
@ -20,7 +22,6 @@ pub use reset::ResetComponent;
pub use stashmsg::StashMsgComponent;
pub use utils::filetree::FileTreeItemKind;
use crossterm::event::Event;
use tui::{
backend::Backend,
layout::Alignment,
@ -54,14 +55,14 @@ macro_rules! accessors {
pub fn event_pump(
ev: Event,
components: &mut [&mut dyn Component],
) -> bool {
) -> Result<bool> {
for c in components {
if c.event(ev) {
return true;
if c.event(ev)? {
return Ok(true);
}
}
false
Ok(false)
}
/// helper fn to simplify delegating command
@ -112,7 +113,11 @@ pub fn visibility_blocking<T: Component>(
///
pub trait DrawableComponent {
///
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, rect: Rect);
fn draw<B: Backend>(
&mut self,
f: &mut Frame<B>,
rect: Rect,
) -> Result<()>;
}
/// base component trait
@ -125,7 +130,7 @@ pub trait Component {
) -> CommandBlocking;
/// returns true if event propagation needs to end (event was consumed)
fn event(&mut self, ev: Event) -> bool;
fn event(&mut self, ev: Event) -> Result<bool>;
///
fn focused(&self) -> bool {
@ -140,7 +145,9 @@ pub trait Component {
///
fn hide(&mut self) {}
///
fn show(&mut self) {}
fn show(&mut self) -> Result<()> {
Ok(())
}
}
fn dialog_paragraph<'a, 't, T>(

View File

@ -20,10 +20,16 @@ pub struct MsgComponent {
theme: Theme,
}
use anyhow::Result;
impl DrawableComponent for MsgComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, _rect: Rect) {
fn draw<B: Backend>(
&mut self,
f: &mut Frame<B>,
_rect: Rect,
) -> Result<()> {
if !self.visible {
return;
return Ok(());
}
let txt = vec![Text::Raw(Cow::from(self.msg.as_str()))];
@ -41,6 +47,8 @@ impl DrawableComponent for MsgComponent {
.wrap(true),
area,
);
Ok(())
}
}
@ -59,17 +67,16 @@ impl Component for MsgComponent {
visibility_blocking(self)
}
fn event(&mut self, ev: Event) -> bool {
fn event(&mut self, ev: Event) -> Result<bool> {
if self.visible {
if let Event::Key(e) = ev {
if let keys::CLOSE_MSG = e {
self.hide();
}
}
true
Ok(true)
} else {
false
Ok(false)
}
}
@ -81,8 +88,10 @@ impl Component for MsgComponent {
self.visible = false
}
fn show(&mut self) {
self.visible = true
fn show(&mut self) -> Result<()> {
self.visible = true;
Ok(())
}
}
@ -95,8 +104,10 @@ impl MsgComponent {
}
}
///
pub fn show_msg(&mut self, msg: &str) {
pub fn show_msg(&mut self, msg: &str) -> Result<()> {
self.msg = msg.to_string();
self.show();
self.show()?;
Ok(())
}
}

View File

@ -8,6 +8,7 @@ use crate::{
strings, ui,
ui::style::Theme,
};
use anyhow::Result;
use crossterm::event::{Event, KeyCode};
use std::borrow::Cow;
use strings::commands;
@ -27,7 +28,11 @@ pub struct ResetComponent {
}
impl DrawableComponent for ResetComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, _rect: Rect) {
fn draw<B: Backend>(
&mut self,
f: &mut Frame<B>,
_rect: Rect,
) -> Result<()> {
if self.visible {
let mut txt = Vec::new();
txt.push(Text::Styled(
@ -42,6 +47,8 @@ impl DrawableComponent for ResetComponent {
area,
);
}
Ok(())
}
}
@ -65,25 +72,26 @@ impl Component for ResetComponent {
visibility_blocking(self)
}
fn event(&mut self, ev: Event) -> bool {
fn event(&mut self, ev: Event) -> Result<bool> {
if self.visible {
if let Event::Key(e) = ev {
return match e.code {
KeyCode::Esc => {
self.hide();
true
Ok(true)
}
KeyCode::Enter => {
self.confirm();
true
Ok(true)
}
_ => true,
_ => Ok(true),
};
}
}
false
Ok(false)
}
fn is_visible(&self) -> bool {
@ -94,8 +102,10 @@ impl Component for ResetComponent {
self.visible = false
}
fn show(&mut self) {
self.visible = true
fn show(&mut self) -> Result<()> {
self.visible = true;
Ok(())
}
}
@ -110,9 +120,11 @@ impl ResetComponent {
}
}
///
pub fn open_for_path(&mut self, item: ResetItem) {
pub fn open_for_path(&mut self, item: ResetItem) -> Result<()> {
self.target = Some(item);
self.show();
self.show()?;
Ok(())
}
///
pub fn confirm(&mut self) {

View File

@ -8,6 +8,7 @@ use crate::{
tabs::StashingOptions,
ui::style::Theme,
};
use anyhow::Result;
use asyncgit::{sync, CWD};
use crossterm::event::{Event, KeyCode};
use strings::commands;
@ -20,8 +21,14 @@ pub struct StashMsgComponent {
}
impl DrawableComponent for StashMsgComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, rect: Rect) {
self.input.draw(f, rect)
fn draw<B: Backend>(
&mut self,
f: &mut Frame<B>,
rect: Rect,
) -> Result<()> {
self.input.draw(f, rect)?;
Ok(())
}
}
@ -41,10 +48,10 @@ impl Component for StashMsgComponent {
visibility_blocking(self)
}
fn event(&mut self, ev: Event) -> bool {
fn event(&mut self, ev: Event) -> Result<bool> {
if self.is_visible() {
if self.input.event(ev) {
return true;
if self.input.event(ev)? {
return Ok(true);
}
if let Event::Key(e) = ev {
@ -71,10 +78,10 @@ impl Component for StashMsgComponent {
}
// stop key event propagation
return true;
return Ok(true);
}
}
false
Ok(false)
}
fn is_visible(&self) -> bool {
@ -85,8 +92,10 @@ impl Component for StashMsgComponent {
self.input.hide()
}
fn show(&mut self) {
self.input.show()
fn show(&mut self) -> Result<()> {
self.input.show()?;
Ok(())
}
}

View File

@ -5,6 +5,7 @@ use super::{
use crate::{
components::dialog_paragraph, strings, ui, ui::style::Theme,
};
use anyhow::Result;
use crossterm::event::{Event, KeyCode};
use std::borrow::Cow;
use strings::commands;
@ -53,7 +54,11 @@ impl TextInputComponent {
}
impl DrawableComponent for TextInputComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, _rect: Rect) {
fn draw<B: Backend>(
&mut self,
f: &mut Frame<B>,
_rect: Rect,
) -> Result<()> {
if self.visible {
let txt = if self.msg.is_empty() {
[Text::Styled(
@ -74,6 +79,8 @@ impl DrawableComponent for TextInputComponent {
area,
);
}
Ok(())
}
}
@ -94,27 +101,27 @@ impl Component for TextInputComponent {
visibility_blocking(self)
}
fn event(&mut self, ev: Event) -> bool {
fn event(&mut self, ev: Event) -> Result<bool> {
if self.visible {
if let Event::Key(e) = ev {
match e.code {
KeyCode::Esc => {
self.hide();
return true;
return Ok(true);
}
KeyCode::Char(c) => {
self.msg.push(c);
return true;
return Ok(true);
}
KeyCode::Backspace => {
self.msg.pop();
return true;
return Ok(true);
}
_ => (),
};
}
}
false
Ok(false)
}
fn is_visible(&self) -> bool {
@ -125,7 +132,9 @@ impl Component for TextInputComponent {
self.visible = false
}
fn show(&mut self) {
self.visible = true
fn show(&mut self) -> Result<()> {
self.visible = true;
Ok(())
}
}

View File

@ -6,6 +6,9 @@ use std::{
path::Path,
};
use anyhow::Result;
use std::ffi::OsStr;
/// holds the information shared among all `FileTreeItem` in a `FileTree`
#[derive(Debug, Clone)]
pub struct TreeItemInfo {
@ -49,19 +52,29 @@ pub struct FileTreeItem {
}
impl FileTreeItem {
fn new_file(item: &StatusItem) -> Self {
fn new_file(item: &StatusItem) -> Result<Self> {
let item_path = Path::new(&item.path);
let indent = u8::try_from(
item_path.ancestors().count().saturating_sub(2),
)
.unwrap();
let path = String::from(
item_path.file_name().unwrap().to_str().unwrap(),
);
)?;
Self {
info: TreeItemInfo::new(indent, path, item.path.clone()),
kind: FileTreeItemKind::File(item.clone()),
let name = item_path
.file_name()
.map(OsStr::to_string_lossy)
.map(|x| x.to_string());
match name {
Some(path) => Ok(Self {
info: TreeItemInfo::new(
indent,
path,
item.path.clone(),
),
kind: FileTreeItemKind::File(item.clone()),
}),
None => {
Err(anyhow::anyhow!("invalid file name {:?}", item))
}
}
}
@ -69,22 +82,27 @@ impl FileTreeItem {
path: &Path,
path_string: String,
collapsed: bool,
) -> Self {
) -> Result<Self> {
let indent =
u8::try_from(path.ancestors().count().saturating_sub(2))
.unwrap();
let path = String::from(
path.components()
.last()
.unwrap()
.as_os_str()
.to_str()
.unwrap(),
);
u8::try_from(path.ancestors().count().saturating_sub(2))?;
Self {
info: TreeItemInfo::new(indent, path, path_string),
kind: FileTreeItemKind::Path(PathCollapsed(collapsed)),
match path
.components()
.last()
.map(std::path::Component::as_os_str)
.map(OsStr::to_string_lossy)
.map(String::from)
{
Some(path) => Ok(Self {
info: TreeItemInfo::new(indent, path, path_string),
kind: FileTreeItemKind::Path(PathCollapsed(
collapsed,
)),
}),
None => Err(anyhow::anyhow!(
"failed to create item from path"
)),
}
}
}
@ -121,7 +139,7 @@ impl FileTreeItems {
pub(crate) fn new(
list: &[StatusItem],
collapsed: &BTreeSet<&String>,
) -> Self {
) -> Result<Self> {
let mut nodes = Vec::with_capacity(list.len());
let mut paths_added = BTreeSet::new();
@ -134,13 +152,13 @@ impl FileTreeItems {
&mut nodes,
&mut paths_added,
&collapsed,
);
)?;
}
nodes.push(FileTreeItem::new_file(&e));
nodes.push(FileTreeItem::new_file(&e)?);
}
Self(nodes)
Ok(Self(nodes))
}
///
@ -178,7 +196,7 @@ impl FileTreeItems {
nodes: &mut Vec<FileTreeItem>,
paths_added: &mut BTreeSet<&'a Path>,
collapsed: &BTreeSet<&String>,
) {
) -> Result<()> {
let mut ancestors =
{ item_path.ancestors().skip(1).collect::<Vec<_>>() };
ancestors.reverse();
@ -194,10 +212,12 @@ impl FileTreeItems {
c,
path_string,
is_collapsed,
));
)?);
}
}
}
Ok(())
}
}
@ -235,7 +255,8 @@ mod tests {
"file.txt", //
]);
let res = FileTreeItems::new(&items, &BTreeSet::new());
let res =
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
assert_eq!(
res.0,
@ -255,7 +276,8 @@ mod tests {
"file2.txt", //
]);
let res = FileTreeItems::new(&items, &BTreeSet::new());
let res =
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
assert_eq!(res.0.len(), 2);
assert_eq!(res.0[1].info.path, items[1].path);
@ -268,6 +290,7 @@ mod tests {
]);
let res = FileTreeItems::new(&items, &BTreeSet::new())
.unwrap()
.0
.iter()
.map(|i| i.info.full_path.clone())
@ -285,7 +308,8 @@ mod tests {
"a/b/file.txt", //
]);
let list = FileTreeItems::new(&items, &BTreeSet::new());
let list =
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
let mut res = list
.0
.iter()
@ -303,7 +327,8 @@ mod tests {
"a.txt", //
]);
let list = FileTreeItems::new(&items, &BTreeSet::new());
let list =
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
let mut res = list
.0
.iter()
@ -322,6 +347,7 @@ mod tests {
]);
let res = FileTreeItems::new(&items, &BTreeSet::new())
.unwrap()
.0
.iter()
.map(|i| i.info.full_path.clone())
@ -350,7 +376,8 @@ mod tests {
"a/b/d", //
]),
&BTreeSet::new(),
);
)
.unwrap();
assert_eq!(
res.find_parent_index(&String::from("a/b/c"), 3),

View File

@ -1,6 +1,7 @@
use super::filetree::{
FileTreeItem, FileTreeItemKind, FileTreeItems, PathCollapsed,
};
use anyhow::Result;
use asyncgit::StatusItem;
use std::{cmp, collections::BTreeSet};
@ -35,14 +36,14 @@ impl SelectionChange {
impl StatusTree {
/// update tree with a new list, try to retain selection and collapse states
pub fn update(&mut self, list: &[StatusItem]) {
pub fn update(&mut self, list: &[StatusItem]) -> Result<()> {
let last_collapsed = self.all_collapsed();
let last_selection =
self.selected_item().map(|e| e.info.full_path);
let last_selection_index = self.selection.unwrap_or(0);
self.tree = FileTreeItems::new(list, &last_collapsed);
self.tree = FileTreeItems::new(list, &last_collapsed)?;
self.selection =
if let Some(ref last_selection) = last_selection {
self.find_last_selection(
@ -56,6 +57,8 @@ impl StatusTree {
};
self.update_visibility(None, 0, true);
Ok(())
}
///
@ -348,7 +351,7 @@ mod tests {
]);
let mut res = StatusTree::default();
res.update(&items);
res.update(&items).unwrap();
assert!(res.move_selection(MoveSelection::Down));
@ -362,11 +365,11 @@ mod tests {
#[test]
fn test_keep_selected_item() {
let mut res = StatusTree::default();
res.update(&string_vec_to_status(&["b"]));
res.update(&string_vec_to_status(&["b"])).unwrap();
assert_eq!(res.selection, Some(0));
res.update(&string_vec_to_status(&["a", "b"]));
res.update(&string_vec_to_status(&["a", "b"])).unwrap();
assert_eq!(res.selection, Some(1));
}
@ -374,10 +377,10 @@ mod tests {
#[test]
fn test_keep_selected_index() {
let mut res = StatusTree::default();
res.update(&string_vec_to_status(&["a", "b"]));
res.update(&string_vec_to_status(&["a", "b"])).unwrap();
res.selection = Some(1);
res.update(&string_vec_to_status(&["d", "c", "a"]));
res.update(&string_vec_to_status(&["d", "c", "a"])).unwrap();
assert_eq!(res.selection, Some(1));
}
@ -387,7 +390,8 @@ mod tests {
res.update(&string_vec_to_status(&[
"a/b", //
"c",
]));
]))
.unwrap();
res.collapse("a", 0);
@ -409,7 +413,8 @@ mod tests {
"a/b", //
"c", //
"d",
]));
]))
.unwrap();
assert_eq!(
res.all_collapsed().iter().collect::<Vec<_>>(),
@ -440,7 +445,7 @@ mod tests {
//3 d
let mut res = StatusTree::default();
res.update(&items);
res.update(&items).unwrap();
res.collapse(&String::from("a/b"), 1);
@ -485,7 +490,7 @@ mod tests {
//4 d
let mut res = StatusTree::default();
res.update(&items);
res.update(&items).unwrap();
res.collapse(&String::from("b"), 1);
res.collapse(&String::from("a"), 0);
@ -528,7 +533,7 @@ mod tests {
//3 c
let mut res = StatusTree::default();
res.update(&items);
res.update(&items).unwrap();
res.collapse(&String::from("a"), 0);
@ -558,7 +563,7 @@ mod tests {
//3 d
let mut res = StatusTree::default();
res.update(&items);
res.update(&items).unwrap();
res.collapse(&String::from("a/b"), 1);
@ -616,7 +621,7 @@ mod tests {
//3 d
let mut res = StatusTree::default();
res.update(&items);
res.update(&items).unwrap();
res.collapse(&String::from("a/b"), 1);
res.selection = Some(1);

View File

@ -4,7 +4,9 @@
//https://github.com/crossterm-rs/crossterm/issues/432
#![allow(clippy::cargo::multiple_crate_versions)]
#![deny(clippy::pedantic)]
#![deny(clippy::result_unwrap_used)]
#![allow(clippy::module_name_repetitions)]
use anyhow::{anyhow, Result};
mod app;
mod components;
@ -26,7 +28,7 @@ use crossterm::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen,
LeaveAlternateScreen,
},
ExecutableCommand, Result,
ExecutableCommand,
};
use log::error;
use scopeguard::defer;
@ -50,7 +52,7 @@ static TICK_INTERVAL: Duration = Duration::from_secs(5);
static SPINNER_INTERVAL: Duration = Duration::from_millis(50);
fn main() -> Result<()> {
setup_logging();
setup_logging()?;
if invalid_path() {
eprintln!("invalid git path\nplease run gitui inside of a git repository");
@ -62,7 +64,7 @@ fn main() -> Result<()> {
shutdown_terminal().expect("shutdown failed");
}
set_panic_handlers();
set_panic_handlers()?;
let mut terminal = start_terminal(io::stdout())?;
@ -74,7 +76,7 @@ fn main() -> Result<()> {
let ticker = tick(TICK_INTERVAL);
let spinner_ticker = tick(SPINNER_INTERVAL);
app.update();
app.update()?;
draw(&mut terminal, &mut app)?;
let mut spinner = Spinner::default();
@ -85,7 +87,7 @@ fn main() -> Result<()> {
&rx_git,
&ticker,
&spinner_ticker,
);
)?;
{
scope_time!("loop");
@ -94,9 +96,9 @@ fn main() -> Result<()> {
for e in events {
match e {
QueueEvent::InputEvent(ev) => app.event(ev),
QueueEvent::Tick => app.update(),
QueueEvent::GitEvent(ev) => app.update_git(ev),
QueueEvent::InputEvent(ev) => app.event(ev)?,
QueueEvent::Tick => app.update()?,
QueueEvent::GitEvent(ev) => app.update_git(ev)?,
QueueEvent::SpinnerUpdate => {
needs_draw = false;
spinner.update()
@ -135,7 +137,11 @@ fn draw<B: Backend>(
terminal: &mut Terminal<B>,
app: &mut App,
) -> io::Result<()> {
terminal.draw(|mut f| app.draw(&mut f))
terminal.draw(|mut f| {
if let Err(e) = app.draw(&mut f) {
log::error!("failed to draw: {:?}", e)
}
})
}
fn invalid_path() -> bool {
@ -147,7 +153,7 @@ fn select_event(
rx_git: &Receiver<AsyncNotification>,
rx_ticker: &Receiver<Instant>,
rx_spinner: &Receiver<Instant>,
) -> Vec<QueueEvent> {
) -> Result<Vec<QueueEvent>> {
let mut events: Vec<QueueEvent> = Vec::new();
let mut sel = Select::new();
@ -172,10 +178,9 @@ fn select_event(
.recv(rx_spinner)
.map(|_| events.push(QueueEvent::SpinnerUpdate)),
_ => panic!("unknown select source"),
}
.unwrap();
}?;
events
Ok(events)
}
fn start_terminal<W: Write>(
@ -189,28 +194,31 @@ fn start_terminal<W: Write>(
Ok(terminal)
}
#[must_use]
pub fn get_app_config_path() -> PathBuf {
let mut path = dirs::cache_dir().unwrap();
fn get_app_config_path() -> Result<PathBuf> {
let mut path = dirs::cache_dir()
.ok_or_else(|| anyhow!("failed to find os cache dir."))?;
path.push("gitui");
fs::create_dir_all(&path).unwrap();
path
fs::create_dir_all(&path)?;
Ok(path)
}
fn setup_logging() {
fn setup_logging() -> Result<()> {
if env::var("GITUI_LOGGING").is_ok() {
let mut path = get_app_config_path();
let mut path = get_app_config_path()?;
path.push("gitui.log");
let _ = WriteLogger::init(
LevelFilter::Trace,
Config::default(),
File::create(path).unwrap(),
File::create(path)?,
);
}
Ok(())
}
fn set_panic_handlers() {
fn set_panic_handlers() -> Result<()> {
// regular panic handler
panic::set_hook(Box::new(|e| {
let backtrace = Backtrace::new();
@ -220,12 +228,11 @@ fn set_panic_handlers() {
}));
// global threadpool
rayon_core::ThreadPoolBuilder::new()
Ok(rayon_core::ThreadPoolBuilder::new()
.panic_handler(|e| {
error!("thread panic: {:?}", e);
panic!(e)
})
.num_threads(4)
.build_global()
.unwrap();
.build_global()?)
}

View File

@ -30,7 +30,9 @@ pub fn start_polling_thread() -> Receiver<Vec<QueueEvent>> {
} else {
MIN_POLL_DURATION
};
if let Some(e) = poll(timeout) {
if let Some(e) =
poll(timeout).expect("failed to pull events.")
{
batch.push(QueueEvent::InputEvent(e));
}
@ -48,11 +50,10 @@ pub fn start_polling_thread() -> Receiver<Vec<QueueEvent>> {
}
///
fn poll(dur: Duration) -> Option<Event> {
if event::poll(dur).unwrap() {
let event = event::read().unwrap();
Some(event)
fn poll(dur: Duration) -> anyhow::Result<Option<Event>> {
if event::poll(dur)? {
Ok(Some(event::read()?))
} else {
None
Ok(None)
}
}

View File

@ -10,6 +10,7 @@ use crate::{
ui::calc_scroll_top,
ui::style::Theme,
};
use anyhow::Result;
use asyncgit::{sync, AsyncLog, AsyncNotification, FetchStatus, CWD};
use crossbeam_channel::Sender;
use crossterm::event::Event;
@ -68,48 +69,51 @@ impl Revlog {
}
///
pub fn update(&mut self) {
pub fn update(&mut self) -> Result<()> {
if self.visible {
let log_changed =
self.git_log.fetch().unwrap() == FetchStatus::Started;
self.git_log.fetch()? == FetchStatus::Started;
self.count_total = self.git_log.count().unwrap();
self.count_total = self.git_log.count()?;
if self
.items
.needs_data(self.selection, self.selection_max())
|| log_changed
{
self.fetch_commits();
self.fetch_commits()?;
}
if self.tags.is_empty() {
self.tags = sync::get_tags(CWD).unwrap();
self.tags = sync::get_tags(CWD)?;
}
}
Ok(())
}
fn fetch_commits(&mut self) {
fn fetch_commits(&mut self) -> Result<()> {
let want_min = self.selection.saturating_sub(SLICE_SIZE / 2);
let commits = sync::get_commits_info(
CWD,
&self.git_log.get_slice(want_min, SLICE_SIZE).unwrap(),
&self.git_log.get_slice(want_min, SLICE_SIZE)?,
self.current_size.0.into(),
);
if let Ok(commits) = commits {
self.items.set_items(want_min, commits);
}
Ok(())
}
fn move_selection(&mut self, scroll: ScrollType) {
fn move_selection(&mut self, scroll: ScrollType) -> Result<()> {
self.update_scroll_speed();
#[allow(clippy::cast_possible_truncation)]
let speed_int = usize::try_from(self.scroll_state.1 as i64)
.unwrap()
.max(1);
let speed_int =
usize::try_from(self.scroll_state.1 as i64)?.max(1);
let page_offset =
usize::from(self.current_size.1).saturating_sub(1);
@ -134,7 +138,9 @@ impl Revlog {
self.selection =
cmp::min(self.selection, self.selection_max());
self.update();
self.update()?;
Ok(())
}
fn update_scroll_speed(&mut self) {
@ -242,7 +248,11 @@ impl Revlog {
}
impl DrawableComponent for Revlog {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect) {
fn draw<B: Backend>(
&mut self,
f: &mut Frame<B>,
area: Rect,
) -> Result<()> {
self.current_size = (
area.width.saturating_sub(2),
area.height.saturating_sub(2),
@ -276,44 +286,46 @@ impl DrawableComponent for Revlog {
.alignment(Alignment::Left),
area,
);
Ok(())
}
}
impl Component for Revlog {
fn event(&mut self, ev: Event) -> bool {
fn event(&mut self, ev: Event) -> Result<bool> {
if self.visible {
if let Event::Key(k) = ev {
return match k {
keys::MOVE_UP => {
self.move_selection(ScrollType::Up);
true
self.move_selection(ScrollType::Up)?;
Ok(true)
}
keys::MOVE_DOWN => {
self.move_selection(ScrollType::Down);
true
self.move_selection(ScrollType::Down)?;
Ok(true)
}
keys::SHIFT_UP | keys::HOME => {
self.move_selection(ScrollType::Home);
true
self.move_selection(ScrollType::Home)?;
Ok(true)
}
keys::SHIFT_DOWN | keys::END => {
self.move_selection(ScrollType::End);
true
self.move_selection(ScrollType::End)?;
Ok(true)
}
keys::PAGE_UP => {
self.move_selection(ScrollType::PageUp);
true
self.move_selection(ScrollType::PageUp)?;
Ok(true)
}
keys::PAGE_DOWN => {
self.move_selection(ScrollType::PageDown);
true
self.move_selection(ScrollType::PageDown)?;
Ok(true)
}
_ => false,
_ => Ok(false),
};
}
}
false
Ok(false)
}
fn commands(
@ -343,9 +355,11 @@ impl Component for Revlog {
self.git_log.set_background();
}
fn show(&mut self) {
fn show(&mut self) -> Result<()> {
self.visible = true;
self.items.clear();
self.update();
self.update()?;
Ok(())
}
}

View File

@ -10,6 +10,7 @@ use crate::{
strings,
ui::style::Theme,
};
use anyhow::Result;
use asyncgit::{
sync::status::StatusType, AsyncNotification, AsyncStatus,
StatusParams,
@ -66,15 +67,15 @@ impl Stashing {
}
///
pub fn update(&mut self) {
pub fn update(&mut self) -> Result<()> {
if self.visible {
self.git_status
.fetch(StatusParams::new(
StatusType::Both,
self.options.stash_untracked,
))
.unwrap();
self.git_status.fetch(StatusParams::new(
StatusType::Both,
self.options.stash_untracked,
))?;
}
Ok(())
}
///
@ -83,13 +84,18 @@ impl Stashing {
}
///
pub fn update_git(&mut self, ev: AsyncNotification) {
pub fn update_git(
&mut self,
ev: AsyncNotification,
) -> Result<()> {
if self.visible {
if let AsyncNotification::Status = ev {
let status = self.git_status.last().unwrap();
self.index.update(&status.items);
let status = self.git_status.last()?;
self.index.update(&status.items)?;
}
}
Ok(())
}
fn get_option_text(&self) -> Vec<Text> {
@ -127,7 +133,7 @@ impl DrawableComponent for Stashing {
&mut self,
f: &mut tui::Frame<B>,
rect: tui::layout::Rect,
) {
) -> Result<()> {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(
@ -153,7 +159,9 @@ impl DrawableComponent for Stashing {
right_chunks[0],
);
self.index.draw(f, chunks[0]);
self.index.draw(f, chunks[0])?;
Ok(())
}
}
@ -184,10 +192,10 @@ impl Component for Stashing {
visibility_blocking(self)
}
fn event(&mut self, ev: crossterm::event::Event) -> bool {
fn event(&mut self, ev: crossterm::event::Event) -> Result<bool> {
if self.visible {
if event_pump(ev, self.components_mut().as_mut_slice()) {
return true;
if event_pump(ev, self.components_mut().as_mut_slice())? {
return Ok(true);
}
if let Event::Key(k) = ev {
@ -199,26 +207,26 @@ impl Component for Stashing {
),
);
true
Ok(true)
}
keys::STASHING_TOGGLE_INDEX => {
self.options.keep_index =
!self.options.keep_index;
self.update();
true
self.update()?;
Ok(true)
}
keys::STASHING_TOGGLE_UNTRACKED => {
self.options.stash_untracked =
!self.options.stash_untracked;
self.update();
true
self.update()?;
Ok(true)
}
_ => false,
_ => Ok(false),
};
}
}
false
Ok(false)
}
fn is_visible(&self) -> bool {
@ -229,8 +237,9 @@ impl Component for Stashing {
self.visible = false;
}
fn show(&mut self) {
fn show(&mut self) -> Result<()> {
self.visible = true;
self.update();
self.update()?;
Ok(())
}
}

View File

@ -10,6 +10,7 @@ use crate::{
strings,
ui::style::Theme,
};
use anyhow::Result;
use asyncgit::{
sync::status::StatusType, AsyncDiff, AsyncNotification,
AsyncStatus, DiffParams, StatusParams,
@ -52,7 +53,7 @@ impl DrawableComponent for Status {
&mut self,
f: &mut tui::Frame<B>,
rect: tui::layout::Rect,
) {
) -> Result<()> {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(
@ -89,9 +90,11 @@ impl DrawableComponent for Status {
)
.split(chunks[0]);
self.index_wd.draw(f, left_chunks[0]);
self.index.draw(f, left_chunks[1]);
self.diff.draw(f, chunks[1]);
self.index_wd.draw(f, left_chunks[0])?;
self.index.draw(f, left_chunks[1])?;
self.diff.draw(f, chunks[1])?;
Ok(())
}
}
@ -137,7 +140,7 @@ impl Status {
}
}
fn switch_focus(&mut self, f: Focus) -> bool {
fn switch_focus(&mut self, f: Focus) -> Result<bool> {
if self.focus != f {
self.focus = f;
@ -158,12 +161,12 @@ impl Status {
}
};
self.update_diff();
self.update_diff()?;
return true;
return Ok(true);
}
false
Ok(false)
}
fn set_diff_target(&mut self, target: DiffTarget) {
@ -189,19 +192,18 @@ impl Status {
}
///
pub fn update(&mut self) {
pub fn update(&mut self) -> Result<()> {
if self.is_visible() {
self.git_diff.refresh().unwrap();
self.git_status_workdir
.fetch(StatusParams::new(
StatusType::WorkingDir,
true,
))
.unwrap();
self.git_diff.refresh()?;
self.git_status_workdir.fetch(StatusParams::new(
StatusType::WorkingDir,
true,
))?;
self.git_status_stage
.fetch(StatusParams::new(StatusType::Stage, true))
.unwrap();
.fetch(StatusParams::new(StatusType::Stage, true))?;
}
Ok(())
}
///
@ -212,52 +214,59 @@ impl Status {
}
///
pub fn update_git(&mut self, ev: AsyncNotification) {
pub fn update_git(
&mut self,
ev: AsyncNotification,
) -> Result<()> {
match ev {
AsyncNotification::Diff => self.update_diff(),
AsyncNotification::Status => self.update_status(),
AsyncNotification::Diff => self.update_diff()?,
AsyncNotification::Status => self.update_status()?,
_ => (),
}
Ok(())
}
fn update_status(&mut self) {
let status = self.git_status_stage.last().unwrap();
self.index.update(&status.items);
fn update_status(&mut self) -> Result<()> {
let status = self.git_status_stage.last()?;
self.index.update(&status.items)?;
let status = self.git_status_workdir.last().unwrap();
self.index_wd.update(&status.items);
let status = self.git_status_workdir.last()?;
self.index_wd.update(&status.items)?;
self.update_diff();
self.update_diff()?;
Ok(())
}
///
pub fn update_diff(&mut self) {
pub fn update_diff(&mut self) -> Result<()> {
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().unwrap()
{
if let Some((params, last)) = self.git_diff.last()? {
if params == diff_params {
self.diff.update(path, is_stage, last);
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).unwrap()
self.git_diff.request(diff_params)?
{
self.diff.update(path, is_stage, diff);
self.diff.update(path, is_stage, diff)?;
} else {
self.diff.clear();
self.diff.clear()?;
}
}
} else {
self.diff.clear();
self.diff.clear()?;
}
Ok(())
}
}
@ -322,10 +331,10 @@ impl Component for Status {
visibility_blocking(self)
}
fn event(&mut self, ev: crossterm::event::Event) -> bool {
fn event(&mut self, ev: crossterm::event::Event) -> Result<bool> {
if self.visible {
if event_pump(ev, self.components_mut().as_mut_slice()) {
return true;
if event_pump(ev, self.components_mut().as_mut_slice())? {
return Ok(true);
}
if let Event::Key(k) = ev {
@ -345,12 +354,12 @@ impl Component for Status {
DiffTarget::WorkingDir => Focus::WorkDir,
})
}
_ => false,
_ => Ok(false),
};
}
}
false
Ok(false)
}
fn is_visible(&self) -> bool {
@ -361,7 +370,9 @@ impl Component for Status {
self.visible = false;
}
fn show(&mut self) {
fn show(&mut self) -> Result<()> {
self.visible = true;
Ok(())
}
}

View File

@ -1,4 +1,5 @@
use crate::get_app_config_path;
use anyhow::Result;
use asyncgit::{DiffLineType, StatusItemType};
use ron::{
de::from_bytes,
@ -180,44 +181,41 @@ impl Theme {
)
}
fn save(&self) -> Result<(), std::io::Error> {
let theme_file = Self::get_theme_file();
fn save(&self) -> Result<()> {
let theme_file = Self::get_theme_file()?;
let mut file = File::create(theme_file)?;
let data = to_string_pretty(self, PrettyConfig::default())
.map_err(|_| std::io::Error::from_raw_os_error(100))?;
let data = to_string_pretty(self, PrettyConfig::default())?;
file.write_all(data.as_bytes())?;
Ok(())
}
fn get_theme_file() -> PathBuf {
let app_home = get_app_config_path();
app_home.join("theme.ron")
fn get_theme_file() -> Result<PathBuf> {
let app_home = get_app_config_path()?;
Ok(app_home.join("theme.ron"))
}
fn read_file(
theme_file: PathBuf,
) -> Result<Theme, std::io::Error> {
if theme_file.exists() {
let mut f = File::open(theme_file)?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;
fn read_file(theme_file: PathBuf) -> Result<Theme> {
let mut f = File::open(theme_file)?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;
Ok(from_bytes(&buffer)?)
}
Ok(from_bytes(&buffer).map_err(|_| {
std::io::Error::from_raw_os_error(100)
})?)
fn init_internal() -> Result<Theme> {
let file = Theme::get_theme_file()?;
if file.exists() {
Ok(Theme::read_file(file)?)
} else {
Err(std::io::Error::from_raw_os_error(100))
let def = Theme::default();
if def.save().is_err() {
log::warn!("failed to store default theme to disk.")
}
Ok(def)
}
}
pub fn init() -> Theme {
if let Ok(x) = Theme::read_file(Theme::get_theme_file()) {
x
} else {
let res = Self::default();
res.save().unwrap_or_default();
res
}
Theme::init_internal().unwrap_or_default()
}
}