Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ serde_with = "3.12.0"
shell-words = "1.1.0"
tempdir = "0.3.7"
thiserror = "2.0.11"
throbber-widgets-tui = "0.9.0"
toml = "0.8.19"
tracing = { version = "0.1.41", features = ["attributes"] }
tracing-chrome = "0.7.2"
Expand Down
49 changes: 35 additions & 14 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,25 @@ fn run_app<B: Backend>(
commander: &mut Commander,
) -> Result<()> {
let mut start_time = Instant::now();
let mut drawing_popup = false;
loop {
// Draw
let mut terminal_draw_res = Ok(());
terminal.draw(|f| {
// Update current tab
let update_span = trace_span!("update");
terminal_draw_res = update_span.in_scope(|| -> Result<()> {
// Update popup if present
if let Some(popup) = app.popup.as_mut() {
if let Some(component_action) = popup.update(commander)? {
app.handle_action(component_action, commander)?;
}
drawing_popup = true;
} else {
drawing_popup = false;
}

// Update current tab
if let Some(component_action) =
app.get_or_init_current_tab(commander)?.update(commander)?
{
Expand Down Expand Up @@ -196,29 +208,38 @@ fn run_app<B: Backend>(

// Input
let input_spawn = trace_span!("input");
let event = loop {

// if drawing a loader, wait for events for 100ms or redraw
// if not drawing a loader, block and wait for events
let should_read_event = if drawing_popup {
event::poll(std::time::Duration::from_millis(100))?
} else {
true
};

if should_read_event {
match event::read()? {
event::Event::FocusLost => continue,
Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
..
}) => continue,
event => break event,
}
};

start_time = Instant::now();
event => {
start_time = Instant::now();

let should_stop = input_spawn.in_scope(|| -> Result<bool> {
if app.input(event, commander)? {
return Ok(true);
}
let should_stop = input_spawn.in_scope(|| -> Result<bool> {
if app.input(event, commander)? {
return Ok(true);
}

Ok(false)
})?;
Ok(false)
})?;

if should_stop {
return Ok(());
if should_stop {
return Ok(());
}
}
}
}
}
}
Expand Down
124 changes: 124 additions & 0 deletions src/ui/loader_popup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use ansi_to_tui::IntoText;
use anyhow::Result;
use ratatui::{
Frame,
crossterm::event::Event,
layout::Rect,
style::{Color, Style},
widgets::{Block, BorderType, Clear},
};
use std::sync::mpsc::{self, Receiver, Sender};
use std::thread;
use std::time::{Duration, Instant};
use throbber_widgets_tui::{Throbber, ThrobberState};

use crate::{
ComponentInputResult,
commander::{CommandError, Commander},
ui::{Component, ComponentAction, message_popup::MessagePopup, utils::centered_rect_fixed},
};

type OperationResult = Result<String, CommandError>;

pub struct LoaderPopup {
operation_name: String,
result_rx: Receiver<OperationResult>,
throbber_state: ThrobberState,
completed: bool,
last_animation_update: Instant,
}

impl LoaderPopup {
pub fn new<F>(operation_name: String, operation: F) -> Self
where
F: FnOnce() -> OperationResult + Send + 'static,
{
let (tx, rx): (Sender<OperationResult>, Receiver<OperationResult>) = mpsc::channel();

// Spawn thread to run the operation
thread::spawn(move || {
let result = operation();
tx.send(result)
});

Self {
operation_name,
result_rx: rx,
throbber_state: ThrobberState::default(),
completed: false,
last_animation_update: Instant::now(),
}
}
}

impl Component for LoaderPopup {
fn update(&mut self, _commander: &mut Commander) -> Result<Option<ComponentAction>> {
if let Ok(result) = self.result_rx.try_recv() {
self.completed = true;

match result {
Ok(output) if !output.is_empty() => {
return Ok(Some(ComponentAction::Multiple(vec![
ComponentAction::SetPopup(Some(Box::new(MessagePopup {
title: format!("{} message", self.operation_name).into(),
messages: output.into_text()?,
text_align: None,
}))),
ComponentAction::RefreshTab(),
])));
}
Ok(_) => {
return Ok(Some(ComponentAction::Multiple(vec![
ComponentAction::SetPopup(None),
ComponentAction::RefreshTab(),
])));
}
Err(err) => {
return Ok(Some(ComponentAction::SetPopup(Some(Box::new(
MessagePopup {
title: format!("{} error", self.operation_name).into(),
messages: err.into_text("")?,
text_align: None,
},
)))));
}
}
}

if self.last_animation_update.elapsed() >= Duration::from_millis(100) {
self.throbber_state.calc_next();
self.last_animation_update = Instant::now();
}

Ok(None)
}

fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> {
let block = Block::bordered()
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Green));

let label = format!("{}...", self.operation_name);
let content_width = 2 + label.len() as u16;
let content_height = 1;

let popup_width = content_width + 2;
let popup_height = content_height + 2;

let popup_area = centered_rect_fixed(area, popup_width, popup_height);
f.render_widget(Clear, popup_area);
f.render_widget(&block, popup_area);

let inner = block.inner(popup_area);

let throbber = Throbber::default().label(label).style(Style::default());
f.render_stateful_widget(throbber, inner, &mut self.throbber_state);

Ok(())
}

fn input(&mut self, _commander: &mut Commander, _event: Event) -> Result<ComponentInputResult> {
// Block all input while loading
Ok(ComponentInputResult::Handled)
}
}
64 changes: 18 additions & 46 deletions src/ui/log_tab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::{
Component, ComponentAction,
bookmark_set_popup::BookmarkSetPopup,
help_popup::HelpPopup,
loader_popup::LoaderPopup,
message_popup::MessagePopup,
panel::DetailsPanel,
panel::LogPanel,
Expand Down Expand Up @@ -370,56 +371,27 @@ impl<'a> LogTab<'a> {
all_bookmarks,
allow_new,
} => {
match commander.git_push(all_bookmarks, allow_new, &self.head.commit_id) {
Ok(result) if !result.is_empty() => {
return Ok(ComponentInputResult::HandledAction(
ComponentAction::SetPopup(Some(Box::new(MessagePopup {
title: "Push message".into(),
messages: result.into_text()?,
text_align: None,
}))),
));
}
Err(err) => {
return Ok(ComponentInputResult::HandledAction(
ComponentAction::SetPopup(Some(Box::new(MessagePopup {
title: "Push error".into(),
messages: err.into_text("")?,
text_align: None,
}))),
));
}
_ => (),
}
let commit_id = self.head.commit_id.clone();
let commander_clone = Commander::new(&commander.env);

self.log_panel.refresh_log_output(commander);
self.refresh_head_output(commander);
let loader = LoaderPopup::new("Pushing".to_string(), move || {
commander_clone.git_push(all_bookmarks, allow_new, &commit_id)
});

return Ok(ComponentInputResult::HandledAction(
ComponentAction::SetPopup(Some(Box::new(loader))),
));
}
LogTabEvent::Fetch { all_remotes } => {
match commander.git_fetch(all_remotes) {
Ok(result) if !result.is_empty() => {
return Ok(ComponentInputResult::HandledAction(
ComponentAction::SetPopup(Some(Box::new(MessagePopup {
title: "Fetch message".into(),
messages: result.into_text()?,
text_align: None,
}))),
));
}
Err(err) => {
return Ok(ComponentInputResult::HandledAction(
ComponentAction::SetPopup(Some(Box::new(MessagePopup {
title: "Fetch error".into(),
messages: err.into_text("")?,
text_align: None,
}))),
));
}
_ => (),
}
let commander_clone = Commander::new(&commander.env);

self.log_panel.refresh_log_output(commander);
self.refresh_head_output(commander);
let loader = LoaderPopup::new("Fetching".to_string(), move || {
commander_clone.git_fetch(all_remotes)
});

return Ok(ComponentInputResult::HandledAction(
ComponentAction::SetPopup(Some(Box::new(loader))),
));
}
LogTabEvent::OpenHelp => {
return Ok(ComponentInputResult::HandledAction(
Expand Down
1 change: 1 addition & 0 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod command_log_tab;
pub mod command_popup;
pub mod files_tab;
pub mod help_popup;
pub mod loader_popup;
pub mod log_tab;
pub mod message_popup;
pub mod panel;
Expand Down
25 changes: 3 additions & 22 deletions src/ui/rebase_popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use ratatui::{
Frame,
crossterm::event::Event,
layout::{Alignment, Rect},
prelude::{Buffer, Constraint, Direction, Layout, Size},
prelude::{Buffer, Constraint, Direction, Layout},
style::{Color, Style, Stylize},
text::{Line, Span, Text},
widgets::{Block, BorderType, Clear, Paragraph, StatefulWidget},
Expand All @@ -35,7 +35,7 @@ use crate::{
ComponentInputResult,
commander::{Commander, log::Head},
keybinds::rebase_popup::{CutOption, PasteOption, PopupAction},
ui::Component,
ui::{Component, utils::centered_rect_fixed},
};

type Keybinds = crate::keybinds::rebase_popup::Keybinds;
Expand Down Expand Up @@ -65,13 +65,7 @@ impl RebasePopup {
/// Collect all the rendering code that would have been in
/// log_tab.rs/draw
pub fn render_widget(&mut self, frame: &mut Frame) {
let area = center_rect(
frame.area(),
Size {
width: 32,
height: 12,
},
);
let area = centered_rect_fixed(frame.area(), 32, 12);
self.draw(frame, area)
.expect("Expected drawing without failues");
}
Expand Down Expand Up @@ -212,19 +206,6 @@ impl Component for RebasePopup {
}
}

/****************************************************************/
// TODO(@peso): Move this function to ui::utils

/// Find a rect of the given size at the center of an outside rect
fn center_rect(outside: Rect, area: Size) -> Rect {
Rect {
x: outside.x + (outside.width - area.width) / 2,
y: outside.y + (outside.height - area.height) / 2,
width: area.width,
height: area.height,
}
}

/****************************************************************/
// TODO(@peso): Move this widget to a separate file

Expand Down
13 changes: 13 additions & 0 deletions src/ui/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ pub fn centered_rect_line_height(r: Rect, percent_x: u16, lines_y: u16) -> Rect
.split(popup_layout[1])[1]
}

/// Center a rect of fixed width and height within an outside rect
pub fn centered_rect_fixed(area: Rect, width: u16, height: u16) -> Rect {
let x = area.x + (area.width.saturating_sub(width)) / 2;
let y = area.y + (area.height.saturating_sub(height)) / 2;

Rect {
x,
y,
width: width.min(area.width),
height: height.min(area.height),
}
}

/// replaces tabs in a string by spaces
///
/// ratatui doesn't work well displaying tabs, so any
Expand Down
Loading