Skip to content
Draft
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
7 changes: 1 addition & 6 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod commands;
pub mod errors;
pub mod input;
pub mod render_snapshot;
pub mod screens;
pub mod state;
pub mod updates;
Expand Down Expand Up @@ -428,10 +429,4 @@ impl App {
pub fn set_current_screen(&mut self, new_current_screen: CurrentScreen) {
self.state.navigation.current_screen = new_current_screen;
}

/// Borrows state for one UI frame without passing [`App`] into `ui/`.
#[must_use]
pub fn to_view_model(&self) -> AppViewModel<'_> {
AppViewModel { state: &self.state }
}
}
23 changes: 23 additions & 0 deletions src/app/render_snapshot.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use super::{App, AppState, AppViewModel};

/// Owned application state snapshot for terminal actor drawing.
#[derive(Clone)]
pub struct AppRenderSnapshot {
state: AppState,
}

impl AppRenderSnapshot {
pub fn new(state: AppState) -> Self {
Self { state }
}

pub fn to_view_model(&self) -> AppViewModel<'_> {
AppViewModel { state: &self.state }
}
}

impl App {
pub fn render_snapshot(&self) -> AppRenderSnapshot {
AppRenderSnapshot::new(self.state.clone())
}
}
1 change: 1 addition & 0 deletions src/app/screens/bookmarked.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::lore::domain::patch::Patch;

#[derive(Clone)]
pub struct BookmarkedPatchsetsState {
pub bookmarked_patchsets: Vec<Patch>,
pub patchset_index: usize,
Expand Down
1 change: 1 addition & 0 deletions src/app/screens/details_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::{

use super::CurrentScreen;

#[derive(Clone)]
pub struct PatchsetDetailsState {
pub representative_patch: Patch,
/// Raw patches as plain text files
Expand Down
4 changes: 2 additions & 2 deletions src/app/screens/edit_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::{collections::HashMap, fmt::Display};

use crate::config::{ConfigSnapshot, ConfigUpdateDraft};

#[derive(Debug, Getters)]
#[derive(Clone, Debug, Getters)]
pub struct EditConfigState {
#[getter(skip)]
config_buffer: HashMap<EditableConfig, String>,
Expand Down Expand Up @@ -137,7 +137,7 @@ impl EditConfigState {
}
}

#[derive(Debug, Hash, Eq, PartialEq)]
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
enum EditableConfig {
PageSize,
CacheDir,
Expand Down
1 change: 1 addition & 0 deletions src/app/screens/latest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::lore::{
domain::patch::Patch,
};

#[derive(Clone)]
pub struct LatestPatchsetsState {
target_list: String,
page_number: usize,
Expand Down
1 change: 1 addition & 0 deletions src/app/screens/mail_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::lore::{
domain::mailing_list::MailingList,
};

#[derive(Clone)]
pub struct MailingListSelectionState {
pub mailing_lists: Vec<MailingList>,
pub target_list: String,
Expand Down
5 changes: 5 additions & 0 deletions src/app/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,34 @@ use crate::{
};

/// Navigation-only state: which screen is active.
#[derive(Clone)]
pub struct NavigationState {
pub current_screen: CurrentScreen,
}

/// Lore-related UI: mailing list picker, feed, patchset details.
#[derive(Clone)]
pub struct LoreUiState {
pub mailing_list_selection: MailingListSelectionState,
pub latest_patchsets: Option<LatestPatchsetsState>,
pub details: Option<PatchsetDetailsState>,
}

/// User-owned Lore data (bookmarks and review markers).
#[derive(Clone)]
pub struct UserLoreState {
pub bookmarked_patchsets: BookmarkedPatchsetsState,
pub reviewed_patchsets: HashMap<String, HashSet<usize>>,
}

/// Edit-config screen state (transient form).
#[derive(Clone)]
pub struct ConfigUiState {
pub edit_config: Option<EditConfigState>,
}

/// All application state grouped for the future App actor.
#[derive(Clone)]
pub struct AppState {
pub navigation: NavigationState,
pub lore: LoreUiState,
Expand Down
34 changes: 13 additions & 21 deletions src/app/updates.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
use ratatui::{prelude::Backend, Terminal};

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

impl App {
/// Processes app-driven updates that are not direct user input.
pub async fn process_system_updates<B>(
pub async fn process_system_updates(
&mut self,
mut terminal: Terminal<B>,
) -> color_eyre::Result<Terminal<B>>
where
B: Backend + Send + 'static,
{
loading: &mut dyn LoadingIndicator,
) -> color_eyre::Result<()> {
match self.state.navigation.current_screen {
CurrentScreen::MailingListSelection => {
if self
Expand All @@ -23,24 +18,21 @@ impl App {
.mailing_lists
.is_empty()
{
terminal = loading_screen! {
terminal, "Fetching mailing lists" => {
self.refresh_mailing_lists().await
}
};
loading.start("Fetching mailing lists".to_string());
let result = self.refresh_mailing_lists().await;
loading.stop()?;
result?;
}
}
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
}
};
loading.start(format!("Fetching patchsets from {target_list}"));
let result = self.fetch_latest_current_page().await;
loading.stop()?;
result?;

self.state.lore.mailing_list_selection.clear_target_list();
}
Expand All @@ -59,6 +51,6 @@ impl App {
_ => {}
}

Ok(terminal)
Ok(())
}
}
3 changes: 2 additions & 1 deletion src/app/view_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

use super::state::AppState;

/// References into [`AppState`] for one Ratatui frame. Built via [`super::App::to_view_model`].
/// References into [`AppState`] for one Ratatui frame. Built via
/// [`super::render_snapshot::AppRenderSnapshot::to_view_model`].
#[derive(Clone, Copy)]
pub struct AppViewModel<'a> {
pub state: &'a AppState,
Expand Down
45 changes: 32 additions & 13 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use clap::Parser;
use color_eyre::eyre::eyre;
use ratatui::{prelude::Backend, Terminal};

use std::ops::ControlFlow;

use crate::{config::ConfigSnapshot, infrastructure::terminal::restore};
use crate::config::ConfigSnapshot;

#[derive(Debug, Parser)]
#[command(version, about)]
Expand All @@ -15,19 +14,11 @@ pub struct Cli {
}

impl Cli {
/// Resolves the command line arguments and applies the necessary changes to the terminal and app
/// Resolves command line arguments that may finish before the TUI starts.
///
/// Some arguments may finish the program early (returning `ControlFlow::Break`)
pub fn resolve<B: Backend>(
&self,
terminal: Terminal<B>,
config: &ConfigSnapshot,
) -> ControlFlow<color_eyre::Result<()>, Terminal<B>> {
pub fn resolve(&self, config: &ConfigSnapshot) -> ControlFlow<color_eyre::Result<()>, ()> {
if self.show_configs {
drop(terminal);
if let Err(err) = restore() {
return ControlFlow::Break(Err(eyre!(err)));
}
match serde_json::to_string_pretty(&config) {
Err(err) => return ControlFlow::Break(Err(eyre!(err))),
Ok(config) => println!("patch-hub configurations:\n{config}"),
Expand All @@ -36,6 +27,34 @@ impl Cli {
return ControlFlow::Break(Ok(()));
}

ControlFlow::Continue(terminal)
ControlFlow::Continue(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::config::ConfigState;

#[test]
fn resolve_continues_when_no_early_cli_action_is_requested() {
let cli = Cli {
show_configs: false,
};
let config = ConfigState::default().to_snapshot();

let result = cli.resolve(&config);

assert!(matches!(result, ControlFlow::Continue(())));
}

#[test]
fn resolve_finishes_after_printing_configs() {
let cli = Cli { show_configs: true };
let config = ConfigState::default().to_snapshot();

let result = cli.resolve(&config);

assert!(matches!(result, ControlFlow::Break(Ok(()))));
}
}
57 changes: 23 additions & 34 deletions src/handler/bookmarked.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
use ratatui::{prelude::Backend, Terminal};

use std::ops::ControlFlow;

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

pub async fn handle_bookmarked_patchsets<B>(
pub async fn handle_bookmarked_patchsets(
app: &mut App,
input: InputEvent,
mut terminal: Terminal<B>,
) -> color_eyre::Result<ControlFlow<(), Terminal<B>>>
where
B: Backend + Send + 'static,
{
loading: &mut dyn LoadingIndicator,
) -> color_eyre::Result<()> {
match input {
InputEvent::OpenHelp => {
let popup = generate_help_popup();
Expand All @@ -39,34 +32,30 @@ where
.select_above_patchset();
}
InputEvent::OpenPatchsetDetails => {
terminal = loading_screen! {
terminal,
"Loading patchset" => {
let result = app.open_patchset_details().await;
if result.is_ok() {
// If a patchset has been bookmarked UI, this means that
// b4 was successful in fetching it, so it shouldn't be
// necessary to handle this, but we can't assume that a
// patchset in this list was bookmarked through the UI
match result.unwrap() {
B4Result::PatchFound => {
app.set_current_screen(CurrentScreen::PatchsetDetails);
}
B4Result::PatchNotFound(err_cause) => {
app.state.popup = Some(InfoPopUp::generate_info_popup(
"Error",&format!("The selected patchset couldn't be retrieved.\nReason: {err_cause}\nPlease choose another patchset.")
));
app.set_current_screen(CurrentScreen::BookmarkedPatchsets);
}
}
loading.start("Loading patchset".to_string());
let result = app.open_patchset_details().await;
loading.stop()?;
if result.is_ok() {
// If a patchset has been bookmarked UI, this means that
// b4 was successful in fetching it, so it shouldn't be
// necessary to handle this, but we can't assume that a
// patchset in this list was bookmarked through the UI
match result.unwrap() {
B4Result::PatchFound => {
app.set_current_screen(CurrentScreen::PatchsetDetails);
}
B4Result::PatchNotFound(err_cause) => {
app.state.popup = Some(InfoPopUp::generate_info_popup(
"Error",&format!("The selected patchset couldn't be retrieved.\nReason: {err_cause}\nPlease choose another patchset.")
));
app.set_current_screen(CurrentScreen::BookmarkedPatchsets);
}
color_eyre::eyre::Ok(())
}
};
}
}
_ => {}
}
Ok(ControlFlow::Continue(terminal))
Ok(())
}

pub fn generate_help_popup() -> Box<dyn PopUp> {
Expand Down
Loading