Skip to content
23 changes: 23 additions & 0 deletions src/app/input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use crate::{app::App, input::context::InputContext};

impl App {
/// Projects App state into the context needed by the input mapper.
pub fn input_context(&self) -> InputContext {
InputContext {
current_screen: self.state.navigation.current_screen.clone(),
popup_open: self.state.popup.is_some(),
edit_config_editing: self
.state
.config_state
.edit_config
.as_ref()
.is_some_and(|edit_config| edit_config.is_editing()),
preview_fullscreen: self
.state
.lore
.details
.as_ref()
.is_some_and(|details| details.preview_fullscreen),
}
}
}
2 changes: 2 additions & 0 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
pub mod commands;
pub mod errors;
pub mod input;
pub mod screens;
pub mod state;
pub mod updates;
pub mod view_model;

use color_eyre::eyre::{bail, eyre};
Expand Down
64 changes: 64 additions & 0 deletions src/app/updates.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use ratatui::{prelude::Backend, Terminal};

use crate::{
app::{screens::CurrentScreen, App},
loading_screen,
};

impl App {
/// Processes app-driven updates that are not direct user input.
pub async fn process_system_updates<B>(
&mut self,
mut terminal: Terminal<B>,
) -> color_eyre::Result<Terminal<B>>
where
B: Backend + Send + 'static,
{
match self.state.navigation.current_screen {
CurrentScreen::MailingListSelection => {
if self
.state
.lore
.mailing_list_selection
.mailing_lists
.is_empty()
{
terminal = loading_screen! {
terminal, "Fetching mailing lists" => {
self.refresh_mailing_lists().await
}
};
}
}
CurrentScreen::LatestPatchsets => {
let patchsets_state = self.state.lore.latest_patchsets.as_ref().unwrap();

if patchsets_state.processed_patchsets_count() == 0 {
let target_list = patchsets_state.target_list().to_string();
terminal = loading_screen! {
terminal,
format!("Fetching patchsets from {}", target_list) => {
self.fetch_latest_current_page().await
}
};

self.state.lore.mailing_list_selection.clear_target_list();
}
}
CurrentScreen::BookmarkedPatchsets => {
if self
.state
.user_state
.bookmarked_patchsets
.bookmarked_patchsets
.is_empty()
{
self.set_current_screen(CurrentScreen::MailingListSelection);
}
}
_ => {}
}

Ok(terminal)
}
}
21 changes: 9 additions & 12 deletions src/handler/bookmarked.rs
Original file line number Diff line number Diff line change
@@ -1,47 +1,44 @@
use ratatui::{
crossterm::event::{KeyCode, KeyEvent},
prelude::Backend,
Terminal,
};
use ratatui::{prelude::Backend, Terminal};

use std::ops::ControlFlow;

use crate::{
app::{screens::CurrentScreen, App, B4Result},
input::event::InputEvent,
loading_screen,
ui::popup::{help::HelpPopUpBuilder, info_popup::InfoPopUp, PopUp},
};

pub async fn handle_bookmarked_patchsets<B>(
app: &mut App,
key: KeyEvent,
input: InputEvent,
mut terminal: Terminal<B>,
) -> color_eyre::Result<ControlFlow<(), Terminal<B>>>
where
B: Backend + Send + 'static,
{
match key.code {
KeyCode::Char('?') => {
match input {
InputEvent::OpenHelp => {
let popup = generate_help_popup();
app.state.popup = Some(popup);
}
KeyCode::Esc | KeyCode::Char('q') => {
InputEvent::Back => {
app.state.user_state.bookmarked_patchsets.patchset_index = 0;
app.set_current_screen(CurrentScreen::MailingListSelection);
}
KeyCode::Char('j') | KeyCode::Down => {
InputEvent::NavigateDown => {
app.state
.user_state
.bookmarked_patchsets
.select_below_patchset();
}
KeyCode::Char('k') | KeyCode::Up => {
InputEvent::NavigateUp => {
app.state
.user_state
.bookmarked_patchsets
.select_above_patchset();
}
KeyCode::Enter => {
InputEvent::OpenPatchsetDetails => {
terminal = loading_screen! {
terminal,
"Loading patchset" => {
Expand Down
122 changes: 47 additions & 75 deletions src/handler/details_actions.rs
Original file line number Diff line number Diff line change
@@ -1,123 +1,87 @@
use ratatui::{
backend::Backend,
crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
Terminal,
};

use std::time::Duration;
use ratatui::{backend::Backend, Terminal};

use crate::{
app::{screens::CurrentScreen, App},
infrastructure::terminal::{setup_user_io, teardown_user_io},
input::{
event::{InputEvent, ScrollAmount},
terminal_source::{wait_for_enter_press, CrosstermEventSource},
},
ui::popup::{help::HelpPopUpBuilder, review_trailers::ReviewTrailersPopUp, PopUp},
};

use super::wait_key_press;

pub async fn handle_patchset_details<B: Backend>(
app: &mut App,
key: KeyEvent,
input: InputEvent,
terminal: &mut Terminal<B>,
) -> color_eyre::Result<()> {
let patchset_details_and_actions = app.state.lore.details.as_mut().unwrap();

if key.modifiers.contains(KeyModifiers::SHIFT) {
match key.code {
KeyCode::Char('G') => patchset_details_and_actions.go_to_last_line(),
KeyCode::Char('R') => {
patchset_details_and_actions.toggle_reply_with_reviewed_by_action(true);
}
_ => {}
}
return Ok(());
}

if key.modifiers.contains(KeyModifiers::CONTROL) {
// TODO: Get preview sub-window height w/out coupling it to UI
let terminal_height = terminal.size().unwrap().height as usize;
match key.code {
KeyCode::Char('b') => {
patchset_details_and_actions.preview_scroll_up(terminal_height);
}
KeyCode::Char('f') => {
patchset_details_and_actions.preview_scroll_down(terminal_height);
}
KeyCode::Char('u') => {
patchset_details_and_actions.preview_scroll_up(terminal_height / 2);
}
KeyCode::Char('d') => {
patchset_details_and_actions.preview_scroll_down(terminal_height / 2);
}
KeyCode::Char('t') => {
let popup =
ReviewTrailersPopUp::generate_trailers_popup(patchset_details_and_actions);
app.state.popup = Some(popup);
}
_ => {}
}
return Ok(());
}

match key.code {
KeyCode::Char('?') => {
match input {
InputEvent::OpenHelp => {
let popup = generate_help_popup();
app.state.popup = Some(popup);
}
KeyCode::Esc | KeyCode::Char('q') => {
InputEvent::Back => {
let ps_da_clone = patchset_details_and_actions.last_screen.clone();
app.set_current_screen(ps_da_clone);
app.reset_details_actions();
}
KeyCode::Char('a') => {
InputEvent::ToggleApply => {
patchset_details_and_actions.toggle_apply_action();
}
KeyCode::Char('j') | KeyCode::Down => {
patchset_details_and_actions.preview_scroll_down(1);
InputEvent::PreviewScrollDown(amount) => {
let lines = preview_scroll_lines(amount, terminal);
patchset_details_and_actions.preview_scroll_down(lines);
}
KeyCode::Char('k') | KeyCode::Up => {
patchset_details_and_actions.preview_scroll_up(1);
InputEvent::PreviewScrollUp(amount) => {
let lines = preview_scroll_lines(amount, terminal);
patchset_details_and_actions.preview_scroll_up(lines);
}
KeyCode::Char('h') | KeyCode::Left => {
InputEvent::PreviewPanLeft => {
patchset_details_and_actions.preview_pan_left();
}
KeyCode::Char('l') | KeyCode::Right => {
InputEvent::PreviewPanRight => {
patchset_details_and_actions.preview_pan_right();
}
KeyCode::Char('0') => {
InputEvent::PreviewGoToBeginningOfLine => {
patchset_details_and_actions.go_to_beg_of_line();
}
KeyCode::Char('g') => {
if let Ok(true) = wait_key_press('g', Duration::from_millis(500)) {
patchset_details_and_actions.go_to_first_line();
}
InputEvent::PreviewGoToFirstLine => {
patchset_details_and_actions.go_to_first_line();
}
InputEvent::PreviewGoToLastLine => {
patchset_details_and_actions.go_to_last_line();
}
KeyCode::Char('f') => {
InputEvent::TogglePreviewFullscreen => {
patchset_details_and_actions.toggle_preview_fullscreen();
}
KeyCode::Char('n') => {
InputEvent::PreviewNext => {
patchset_details_and_actions.preview_next_patch();
}
KeyCode::Char('p') => {
InputEvent::PreviewPrevious => {
patchset_details_and_actions.preview_previous_patch();
}
KeyCode::Char('b') => {
InputEvent::ToggleBookmark => {
patchset_details_and_actions.toggle_bookmark_action();
}
KeyCode::Char('r') => {
InputEvent::ToggleReplyWithReviewedBy => {
patchset_details_and_actions.toggle_reply_with_reviewed_by_action(false);
}
KeyCode::Enter => {
InputEvent::ToggleReplyWithReviewedByAll => {
patchset_details_and_actions.toggle_reply_with_reviewed_by_action(true);
}
InputEvent::ShowReviewTrailers => {
let popup = ReviewTrailersPopUp::generate_trailers_popup(patchset_details_and_actions);
app.state.popup = Some(popup);
}
InputEvent::ConsolidatePatchsetActions => {
if patchset_details_and_actions.actions_require_user_io() {
setup_user_io(terminal)?;
app.consolidate_patchset_actions().await?;
println!("\nPress ENTER continue...");
loop {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Enter {
break;
}
}
}
let mut event_source = CrosstermEventSource;
wait_for_enter_press(&mut event_source)?;
teardown_user_io(terminal)?;
} else {
app.consolidate_patchset_actions().await?;
Expand All @@ -129,6 +93,14 @@ pub async fn handle_patchset_details<B: Backend>(
Ok(())
}

fn preview_scroll_lines<B: Backend>(amount: ScrollAmount, terminal: &Terminal<B>) -> usize {
match amount {
ScrollAmount::Line => 1,
ScrollAmount::HalfPage => terminal.size().unwrap().height as usize / 2,
ScrollAmount::Page => terminal.size().unwrap().height as usize,
}
}

pub fn generate_help_popup() -> Box<dyn PopUp> {
let popup = HelpPopUpBuilder::new()
.title("Patchset Details and Actions")
Expand Down
27 changes: 13 additions & 14 deletions src/handler/edit_config.rs
Original file line number Diff line number Diff line change
@@ -1,48 +1,47 @@
use ratatui::crossterm::event::{KeyCode, KeyEvent};

use crate::{
app::{screens::CurrentScreen, App},
input::event::InputEvent,
ui::popup::{help::HelpPopUpBuilder, PopUp},
};

pub fn handle_edit_config(app: &mut App, key: KeyEvent) -> color_eyre::Result<()> {
pub fn handle_edit_config(app: &mut App, input: InputEvent) -> color_eyre::Result<()> {
if let Some(edit_config_state) = app.state.config_state.edit_config.as_mut() {
match edit_config_state.is_editing() {
true => match key.code {
KeyCode::Esc => {
true => match input {
InputEvent::CancelConfigEdit => {
edit_config_state.clear_edit();
edit_config_state.toggle_editing();
}
KeyCode::Backspace => {
InputEvent::Backspace => {
edit_config_state.backspace_edit();
}
KeyCode::Char(ch) => {
InputEvent::TextInput(ch) => {
edit_config_state.append_edit(ch);
}
KeyCode::Enter => {
InputEvent::StageConfigEdit => {
edit_config_state.stage_edit();
edit_config_state.clear_edit();
edit_config_state.toggle_editing();
}
_ => {}
},
false => match key.code {
KeyCode::Char('?') => {
false => match input {
InputEvent::OpenHelp => {
let popup = generate_help_popup();
app.state.popup = Some(popup);
}
KeyCode::Esc | KeyCode::Char('q') => {
InputEvent::SaveConfig => {
app.consolidate_edit_config()?;
app.reset_edit_config();
app.set_current_screen(CurrentScreen::MailingListSelection);
}
KeyCode::Enter => {
InputEvent::EditConfigField => {
edit_config_state.toggle_editing();
}
KeyCode::Char('j') | KeyCode::Down => {
InputEvent::NavigateDown => {
edit_config_state.highlight_next();
}
KeyCode::Char('k') | KeyCode::Up => {
InputEvent::NavigateUp => {
edit_config_state.highlight_prev();
}
_ => {}
Expand Down
Loading