From be5a2cb2ba23592b7a3fe9d48919fc64238cb962 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 27 Jun 2025 18:56:52 +0530 Subject: [PATCH 01/14] refactor: rename `app` module to `model` and `App` struct to `Model` The `App` struct serves as the model in the MVC pattern. The proposed MVVM refactor will continue using it as the primary Model. Rename `App` to `Model` to better reflect it's purpose. Signed-off-by: Ivin Joel Abraham --- src/cli.rs | 2 +- src/handler/bookmarked.rs | 22 ++++----- src/handler/details_actions.rs | 20 ++++---- src/handler/edit_config.rs | 16 +++---- src/handler/latest.rs | 20 ++++---- src/handler/mail_list.rs | 38 +++++++-------- src/handler/mod.rs | 47 ++++++++++--------- .../logging/garbage_collector.rs | 2 +- src/infrastructure/logging/mod.rs | 2 +- src/main.rs | 10 ++-- src/{app => model}/config/mod.rs | 0 src/{app => model}/config/tests.rs | 0 src/{app => model}/cover_renderer.rs | 0 src/{app => model}/mod.rs | 6 +-- src/{app => model}/patch_renderer.rs | 0 src/{app => model}/screens/bookmarked.rs | 0 src/{app => model}/screens/details_actions.rs | 2 +- src/{app => model}/screens/edit_config.rs | 2 +- src/{app => model}/screens/latest.rs | 0 src/{app => model}/screens/mail_list.rs | 0 src/{app => model}/screens/mod.rs | 0 src/ui/bookmarked.rs | 2 +- src/ui/details_actions.rs | 29 +++++++----- src/ui/edit_config.rs | 14 +++--- src/ui/latest.rs | 20 ++++---- src/ui/mail_list.rs | 24 +++++----- src/ui/mod.rs | 20 ++++---- src/ui/navigation_bar.rs | 16 +++---- src/ui/popup/review_trailers.rs | 2 +- 29 files changed, 162 insertions(+), 154 deletions(-) rename src/{app => model}/config/mod.rs (100%) rename src/{app => model}/config/tests.rs (100%) rename src/{app => model}/cover_renderer.rs (100%) rename src/{app => model}/mod.rs (99%) rename src/{app => model}/patch_renderer.rs (100%) rename src/{app => model}/screens/bookmarked.rs (100%) rename src/{app => model}/screens/details_actions.rs (99%) rename src/{app => model}/screens/edit_config.rs (99%) rename src/{app => model}/screens/latest.rs (100%) rename src/{app => model}/screens/mail_list.rs (100%) rename src/{app => model}/screens/mod.rs (100%) diff --git a/src/cli.rs b/src/cli.rs index 39deb5d9..88fdd076 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,7 +4,7 @@ use ratatui::{prelude::Backend, Terminal}; use std::ops::ControlFlow; -use crate::{app::config::Config, infrastructure::terminal::restore}; +use crate::{infrastructure::terminal::restore, model::config::Config}; #[derive(Debug, Parser)] #[command(version, about)] diff --git a/src/handler/bookmarked.rs b/src/handler/bookmarked.rs index 54ddd75c..35c99766 100644 --- a/src/handler/bookmarked.rs +++ b/src/handler/bookmarked.rs @@ -7,14 +7,14 @@ use ratatui::{ use std::ops::ControlFlow; use crate::{ - app::{screens::CurrentScreen, App}, loading_screen, lore::lore_session::B4Result, + model::{screens::CurrentScreen, Model}, ui::popup::{help::HelpPopUpBuilder, info_popup::InfoPopUp, PopUp}, }; pub fn handle_bookmarked_patchsets( - app: &mut App, + model: &mut Model, key: KeyEvent, mut terminal: Terminal, ) -> color_eyre::Result>> @@ -24,23 +24,23 @@ where match key.code { KeyCode::Char('?') => { let popup = generate_help_popup(); - app.popup = Some(popup); + model.popup = Some(popup); } KeyCode::Esc | KeyCode::Char('q') => { - app.bookmarked_patchsets.patchset_index = 0; - app.set_current_screen(CurrentScreen::MailingListSelection); + model.bookmarked_patchsets.patchset_index = 0; + model.set_current_screen(CurrentScreen::MailingListSelection); } KeyCode::Char('j') | KeyCode::Down => { - app.bookmarked_patchsets.select_below_patchset(); + model.bookmarked_patchsets.select_below_patchset(); } KeyCode::Char('k') | KeyCode::Up => { - app.bookmarked_patchsets.select_above_patchset(); + model.bookmarked_patchsets.select_above_patchset(); } KeyCode::Enter => { terminal = loading_screen! { terminal, "Loading patchset" => { - let result = app.init_details_actions(); + let result = model.init_details_actions(); 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 @@ -48,13 +48,13 @@ where // patchset in this list was bookmarked through the UI match result.unwrap() { B4Result::PatchFound(_) => { - app.set_current_screen(CurrentScreen::PatchsetDetails); + model.set_current_screen(CurrentScreen::PatchsetDetails); } B4Result::PatchNotFound(err_cause) => { - app.popup = Some(InfoPopUp::generate_info_popup( + model.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); + model.set_current_screen(CurrentScreen::BookmarkedPatchsets); } } } diff --git a/src/handler/details_actions.rs b/src/handler/details_actions.rs index 26a072b2..7da0474d 100644 --- a/src/handler/details_actions.rs +++ b/src/handler/details_actions.rs @@ -7,19 +7,19 @@ use ratatui::{ use std::time::Duration; use crate::{ - app::{screens::CurrentScreen, App}, infrastructure::terminal::{setup_user_io, teardown_user_io}, + model::{screens::CurrentScreen, Model}, ui::popup::{help::HelpPopUpBuilder, review_trailers::ReviewTrailersPopUp, PopUp}, }; use super::wait_key_press; pub fn handle_patchset_details( - app: &mut App, + model: &mut Model, key: KeyEvent, terminal: &mut Terminal, ) -> color_eyre::Result<()> { - let patchset_details_and_actions = app.details_actions.as_mut().unwrap(); + let patchset_details_and_actions = model.details_actions.as_mut().unwrap(); if key.modifiers.contains(KeyModifiers::SHIFT) { match key.code { @@ -51,7 +51,7 @@ pub fn handle_patchset_details( KeyCode::Char('t') => { let popup = ReviewTrailersPopUp::generate_trailers_popup(patchset_details_and_actions); - app.popup = Some(popup); + model.popup = Some(popup); } _ => {} } @@ -61,12 +61,12 @@ pub fn handle_patchset_details( match key.code { KeyCode::Char('?') => { let popup = generate_help_popup(); - app.popup = Some(popup); + model.popup = Some(popup); } KeyCode::Esc | KeyCode::Char('q') => { let ps_da_clone = patchset_details_and_actions.last_screen.clone(); - app.set_current_screen(ps_da_clone); - app.reset_details_actions(); + model.set_current_screen(ps_da_clone); + model.reset_details_actions(); } KeyCode::Char('a') => { patchset_details_and_actions.toggle_apply_action(); @@ -109,7 +109,7 @@ pub fn handle_patchset_details( KeyCode::Enter => { if patchset_details_and_actions.actions_require_user_io() { setup_user_io(terminal)?; - app.consolidate_patchset_actions()?; + model.consolidate_patchset_actions()?; println!("\nPress ENTER continue..."); loop { if let Event::Key(key) = event::read()? { @@ -120,9 +120,9 @@ pub fn handle_patchset_details( } teardown_user_io(terminal)?; } else { - app.consolidate_patchset_actions()?; + model.consolidate_patchset_actions()?; } - app.set_current_screen(CurrentScreen::PatchsetDetails); + model.set_current_screen(CurrentScreen::PatchsetDetails); } _ => {} } diff --git a/src/handler/edit_config.rs b/src/handler/edit_config.rs index f0f2f158..991caa41 100644 --- a/src/handler/edit_config.rs +++ b/src/handler/edit_config.rs @@ -1,12 +1,12 @@ use ratatui::crossterm::event::{KeyCode, KeyEvent}; use crate::{ - app::{screens::CurrentScreen, App}, + model::{screens::CurrentScreen, Model}, ui::popup::{help::HelpPopUpBuilder, PopUp}, }; -pub fn handle_edit_config(app: &mut App, key: KeyEvent) -> color_eyre::Result<()> { - if let Some(edit_config_state) = app.edit_config.as_mut() { +pub fn handle_edit_config(model: &mut Model, key: KeyEvent) -> color_eyre::Result<()> { + if let Some(edit_config_state) = model.edit_config.as_mut() { match edit_config_state.is_editing() { true => match key.code { KeyCode::Esc => { @@ -29,13 +29,13 @@ pub fn handle_edit_config(app: &mut App, key: KeyEvent) -> color_eyre::Result<() false => match key.code { KeyCode::Char('?') => { let popup = generate_help_popup(); - app.popup = Some(popup); + model.popup = Some(popup); } KeyCode::Esc | KeyCode::Char('q') => { - app.consolidate_edit_config(); - app.config.save_patch_hub_config()?; - app.reset_edit_config(); - app.set_current_screen(CurrentScreen::MailingListSelection); + model.consolidate_edit_config(); + model.config.save_patch_hub_config()?; + model.reset_edit_config(); + model.set_current_screen(CurrentScreen::MailingListSelection); } KeyCode::Enter => { edit_config_state.toggle_editing(); diff --git a/src/handler/latest.rs b/src/handler/latest.rs index 74a035da..cb01fc25 100644 --- a/src/handler/latest.rs +++ b/src/handler/latest.rs @@ -7,30 +7,30 @@ use ratatui::{ use std::ops::ControlFlow; use crate::{ - app::{screens::CurrentScreen, App}, loading_screen, lore::lore_session::B4Result, + model::{screens::CurrentScreen, Model}, ui::popup::{help::HelpPopUpBuilder, info_popup::InfoPopUp, PopUp}, }; pub fn handle_latest_patchsets( - app: &mut App, + model: &mut Model, key: KeyEvent, mut terminal: Terminal, ) -> color_eyre::Result>> where B: Backend + Send + 'static, { - let latest_patchsets = app.latest_patchsets.as_mut().unwrap(); + let latest_patchsets = model.latest_patchsets.as_mut().unwrap(); match key.code { KeyCode::Char('?') => { let popup = generate_help_popup(); - app.popup = Some(popup); + model.popup = Some(popup); } KeyCode::Esc | KeyCode::Char('q') => { - app.reset_latest_patchsets(); - app.set_current_screen(CurrentScreen::MailingListSelection); + model.reset_latest_patchsets(); + model.set_current_screen(CurrentScreen::MailingListSelection); } KeyCode::Char('j') | KeyCode::Down => { latest_patchsets.select_below_patchset(); @@ -55,17 +55,17 @@ where terminal = loading_screen! { terminal, "Loading patchset" => { - let result = app.init_details_actions(); + let result = model.init_details_actions(); if result.is_ok() { match result.unwrap() { B4Result::PatchFound(_) => { - app.set_current_screen(CurrentScreen::PatchsetDetails); + model.set_current_screen(CurrentScreen::PatchsetDetails); } B4Result::PatchNotFound(err_cause) => { - app.popup = Some(InfoPopUp::generate_info_popup( + model.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::LatestPatchsets); + model.set_current_screen(CurrentScreen::LatestPatchsets); } } } diff --git a/src/handler/mail_list.rs b/src/handler/mail_list.rs index d5a0ffc4..48097948 100644 --- a/src/handler/mail_list.rs +++ b/src/handler/mail_list.rs @@ -7,13 +7,13 @@ use ratatui::{ use std::ops::ControlFlow; use crate::{ - app::{screens::CurrentScreen, App}, loading_screen, + model::{screens::CurrentScreen, Model}, ui::popup::{help::HelpPopUpBuilder, PopUp}, }; pub fn handle_mailing_list_selection( - app: &mut App, + model: &mut Model, key: KeyEvent, mut terminal: Terminal, ) -> color_eyre::Result>> @@ -23,12 +23,12 @@ where match key.code { KeyCode::Char('?') => { let popup = generate_help_popup(); - app.popup = Some(popup); + model.popup = Some(popup); } KeyCode::Enter => { - if app.mailing_list_selection.has_valid_target_list() { - app.init_latest_patchsets(); - let list_name = app + if model.mailing_list_selection.has_valid_target_list() { + model.init_latest_patchsets(); + let list_name = model .latest_patchsets .as_ref() .unwrap() @@ -39,11 +39,11 @@ where terminal, format!("Fetching patchsets from {}", list_name) => { let result = - app.latest_patchsets.as_mut().unwrap() + model.latest_patchsets.as_mut().unwrap() .fetch_current_page(); if result.is_ok() { - app.mailing_list_selection.clear_target_list(); - app.set_current_screen(CurrentScreen::LatestPatchsets); + model.mailing_list_selection.clear_target_list(); + model.set_current_screen(CurrentScreen::LatestPatchsets); } result } @@ -54,35 +54,35 @@ where terminal = loading_screen! { terminal, "Refreshing lists" => { - app.mailing_list_selection + model.mailing_list_selection .refresh_available_mailing_lists() } }; } KeyCode::F(2) => { - app.init_edit_config(); - app.set_current_screen(CurrentScreen::EditConfig); + model.init_edit_config(); + model.set_current_screen(CurrentScreen::EditConfig); } KeyCode::F(1) => { - if !app.bookmarked_patchsets.bookmarked_patchsets.is_empty() { - app.mailing_list_selection.clear_target_list(); - app.set_current_screen(CurrentScreen::BookmarkedPatchsets); + if !model.bookmarked_patchsets.bookmarked_patchsets.is_empty() { + model.mailing_list_selection.clear_target_list(); + model.set_current_screen(CurrentScreen::BookmarkedPatchsets); } } KeyCode::Backspace => { - app.mailing_list_selection.remove_last_target_list_char(); + model.mailing_list_selection.remove_last_target_list_char(); } KeyCode::Esc => { return Ok(ControlFlow::Break(())); } KeyCode::Char(ch) => { - app.mailing_list_selection.push_char_to_target_list(ch); + model.mailing_list_selection.push_char_to_target_list(ch); } KeyCode::Down => { - app.mailing_list_selection.highlight_below_list(); + model.mailing_list_selection.highlight_below_list(); } KeyCode::Up => { - app.mailing_list_selection.highlight_above_list(); + model.mailing_list_selection.highlight_above_list(); } _ => {} } diff --git a/src/handler/mod.rs b/src/handler/mod.rs index feaf6658..b0a54909 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -16,8 +16,8 @@ use std::{ }; use crate::{ - app::{screens::CurrentScreen, App}, loading_screen, + model::{screens::CurrentScreen, Model}, ui::draw_ui, }; @@ -29,56 +29,59 @@ use mail_list::handle_mailing_list_selection; fn key_handling( mut terminal: Terminal, - app: &mut App, + model: &mut Model, key: KeyEvent, ) -> color_eyre::Result>> where B: Backend + Send + 'static, { - if let Some(popup) = app.popup.as_mut() { + if let Some(popup) = model.popup.as_mut() { if matches!(key.code, KeyCode::Esc | KeyCode::Char('q')) { - app.popup = None; + model.popup = None; } else { popup.handle(key)?; } } else { - match app.current_screen { + match model.current_screen { CurrentScreen::MailingListSelection => { - return handle_mailing_list_selection(app, key, terminal); + return handle_mailing_list_selection(model, key, terminal); } CurrentScreen::BookmarkedPatchsets => { - return handle_bookmarked_patchsets(app, key, terminal); + return handle_bookmarked_patchsets(model, key, terminal); } CurrentScreen::PatchsetDetails => { - handle_patchset_details(app, key, &mut terminal)?; + handle_patchset_details(model, key, &mut terminal)?; } CurrentScreen::EditConfig => { - handle_edit_config(app, key)?; + handle_edit_config(model, key)?; } CurrentScreen::LatestPatchsets => { - return handle_latest_patchsets(app, key, terminal); + return handle_latest_patchsets(model, key, terminal); } } } Ok(ControlFlow::Continue(terminal)) } -fn logic_handling(mut terminal: Terminal, app: &mut App) -> color_eyre::Result> +fn logic_handling( + mut terminal: Terminal, + model: &mut Model, +) -> color_eyre::Result> where B: Backend + Send + 'static, { - match app.current_screen { + match model.current_screen { CurrentScreen::MailingListSelection => { - if app.mailing_list_selection.mailing_lists.is_empty() { + if model.mailing_list_selection.mailing_lists.is_empty() { terminal = loading_screen! { terminal, "Fetching mailing lists" => { - app.mailing_list_selection.refresh_available_mailing_lists() + model.mailing_list_selection.refresh_available_mailing_lists() } }; } } CurrentScreen::LatestPatchsets => { - let patchsets_state = app.latest_patchsets.as_mut().unwrap(); + let patchsets_state = model.latest_patchsets.as_mut().unwrap(); let target_list = patchsets_state.target_list().to_string(); if patchsets_state.processed_patchsets_count() == 0 { terminal = loading_screen! { @@ -88,12 +91,12 @@ where } }; - app.mailing_list_selection.clear_target_list(); + model.mailing_list_selection.clear_target_list(); } } CurrentScreen::BookmarkedPatchsets => { - if app.bookmarked_patchsets.bookmarked_patchsets.is_empty() { - app.set_current_screen(CurrentScreen::MailingListSelection); + if model.bookmarked_patchsets.bookmarked_patchsets.is_empty() { + model.set_current_screen(CurrentScreen::MailingListSelection); } } _ => {} @@ -102,14 +105,14 @@ where Ok(terminal) } -pub fn run_app(mut terminal: Terminal, mut app: App) -> color_eyre::Result<()> +pub fn run_app(mut terminal: Terminal, mut model: Model) -> color_eyre::Result<()> where B: Backend + Send + 'static, { loop { - terminal = logic_handling(terminal, &mut app)?; + terminal = logic_handling(terminal, &mut model)?; - terminal.draw(|f| draw_ui(f, &app))?; + terminal.draw(|f| draw_ui(f, &model))?; // *IMPORTANT*: Uncommenting the if below makes `patch-hub` not block // until an event is captured. We should only do it when (if ever) we @@ -120,7 +123,7 @@ where if key.kind == KeyEventKind::Release { continue; } - match key_handling(terminal, &mut app, key)? { + match key_handling(terminal, &mut model, key)? { ControlFlow::Continue(t) => terminal = t, ControlFlow::Break(_) => return Ok(()), } diff --git a/src/infrastructure/logging/garbage_collector.rs b/src/infrastructure/logging/garbage_collector.rs index e06892b4..be02368b 100644 --- a/src/infrastructure/logging/garbage_collector.rs +++ b/src/infrastructure/logging/garbage_collector.rs @@ -2,7 +2,7 @@ //! //! This module is responsible for cleaning up the log files. -use crate::app::config::Config; +use crate::model::config::Config; use super::Logger; diff --git a/src/infrastructure/logging/mod.rs b/src/infrastructure/logging/mod.rs index e7ceb67e..e7bccac8 100644 --- a/src/infrastructure/logging/mod.rs +++ b/src/infrastructure/logging/mod.rs @@ -8,7 +8,7 @@ use std::{ io::Write, }; -use crate::app::config::Config; +use crate::model::config::Config; const LATEST_LOG_FILENAME: &str = "latest.log"; diff --git a/src/main.rs b/src/main.rs index 1db14403..998a44b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,11 @@ -mod app; mod cli; mod handler; mod infrastructure; mod lore; mod macros; +mod model; mod ui; -use app::{config::Config, App}; use clap::Parser; use cli::Cli; use color_eyre::eyre::bail; @@ -15,6 +14,7 @@ use infrastructure::{ logging::Logger, terminal::{init, restore}, }; +use model::{config::Config, Model}; use std::ops::ControlFlow; fn main() -> color_eyre::Result<()> { @@ -31,13 +31,13 @@ fn main() -> color_eyre::Result<()> { ControlFlow::Continue(t) => terminal = t, } - let app = App::new(config)?; - if !app.check_external_deps() { + let model = Model::new(config)?; + if !model.check_external_deps() { Logger::error("patch-hub cannot be executed because some dependencies are missing"); bail!("patch-hub cannot be executed because some dependencies are missing, check logs for more information"); } - run_app(terminal, app)?; + run_app(terminal, model)?; restore()?; Logger::info("patch-hub finished"); diff --git a/src/app/config/mod.rs b/src/model/config/mod.rs similarity index 100% rename from src/app/config/mod.rs rename to src/model/config/mod.rs diff --git a/src/app/config/tests.rs b/src/model/config/tests.rs similarity index 100% rename from src/app/config/tests.rs rename to src/model/config/tests.rs diff --git a/src/app/cover_renderer.rs b/src/model/cover_renderer.rs similarity index 100% rename from src/app/cover_renderer.rs rename to src/model/cover_renderer.rs diff --git a/src/app/mod.rs b/src/model/mod.rs similarity index 99% rename from src/app/mod.rs rename to src/model/mod.rs index 23a64389..552e83ba 100644 --- a/src/app/mod.rs +++ b/src/model/mod.rs @@ -34,7 +34,7 @@ use screens::{ /// Type that represents the overall state of the application. It can be viewed /// as the **Model** component of `patch-hub`. -pub struct App { +pub struct Model { /// The current active screen pub current_screen: CurrentScreen, /// Screen to navigate and select the mailing lists archived on Lore @@ -56,7 +56,7 @@ pub struct App { pub popup: Option>, } -impl App { +impl Model { /// Creates a new instance of `App`. It dynamically loads configurations /// based on precedence (see [crate::app::Config::build]), app data /// (available mailing lists, bookmarked patchsets, reviewed patchsets), and @@ -84,7 +84,7 @@ impl App { Logger::info("patch-hub started"); garbage_collector::collect_garbage(&config); - Ok(App { + Ok(Model { current_screen: CurrentScreen::MailingListSelection, mailing_list_selection: MailingListSelection { mailing_lists: mailing_lists.clone(), diff --git a/src/app/patch_renderer.rs b/src/model/patch_renderer.rs similarity index 100% rename from src/app/patch_renderer.rs rename to src/model/patch_renderer.rs diff --git a/src/app/screens/bookmarked.rs b/src/model/screens/bookmarked.rs similarity index 100% rename from src/app/screens/bookmarked.rs rename to src/model/screens/bookmarked.rs diff --git a/src/app/screens/details_actions.rs b/src/model/screens/details_actions.rs similarity index 99% rename from src/app/screens/details_actions.rs rename to src/model/screens/details_actions.rs index a3807cdd..856f55dc 100644 --- a/src/app/screens/details_actions.rs +++ b/src/model/screens/details_actions.rs @@ -8,12 +8,12 @@ use std::{ }; use crate::{ - app::config::{Config, KernelTree}, lore::{ lore_api_client::BlockingLoreAPIClient, lore_session, patch::{Author, Patch}, }, + model::config::{Config, KernelTree}, }; use super::CurrentScreen; diff --git a/src/app/screens/edit_config.rs b/src/model/screens/edit_config.rs similarity index 99% rename from src/app/screens/edit_config.rs rename to src/model/screens/edit_config.rs index 8714f1ca..64316865 100644 --- a/src/app/screens/edit_config.rs +++ b/src/model/screens/edit_config.rs @@ -3,7 +3,7 @@ use derive_getters::Getters; use std::{collections::HashMap, fmt::Display, path::Path}; -use crate::app::config::Config; +use crate::model::config::Config; #[derive(Debug, Getters)] pub struct EditConfig { diff --git a/src/app/screens/latest.rs b/src/model/screens/latest.rs similarity index 100% rename from src/app/screens/latest.rs rename to src/model/screens/latest.rs diff --git a/src/app/screens/mail_list.rs b/src/model/screens/mail_list.rs similarity index 100% rename from src/app/screens/mail_list.rs rename to src/model/screens/mail_list.rs diff --git a/src/app/screens/mod.rs b/src/model/screens/mod.rs similarity index 100% rename from src/app/screens/mod.rs rename to src/model/screens/mod.rs diff --git a/src/ui/bookmarked.rs b/src/ui/bookmarked.rs index a6a9ccfe..f194c78b 100644 --- a/src/ui/bookmarked.rs +++ b/src/ui/bookmarked.rs @@ -6,7 +6,7 @@ use ratatui::{ Frame, }; -use crate::app::screens::bookmarked::BookmarkedPatchsets; +use crate::model::screens::bookmarked::BookmarkedPatchsets; pub fn render_main(f: &mut Frame, bookmarked_patchsets: &BookmarkedPatchsets, chunk: Rect) { let patchset_index = bookmarked_patchsets.patchset_index; diff --git a/src/ui/details_actions.rs b/src/ui/details_actions.rs index 92a4020d..75b75937 100644 --- a/src/ui/details_actions.rs +++ b/src/ui/details_actions.rs @@ -6,9 +6,9 @@ use ratatui::{ Frame, }; -use crate::app::{ +use crate::model::{ screens::details_actions::{DetailsActions, PatchsetAction}, - App, + Model, }; /// Returns a `Line` type that represents a line containing stats about reply @@ -47,8 +47,13 @@ fn review_trailers_details(details_actions: &DetailsActions) -> Line<'static> { ]) } -fn render_details_and_actions(f: &mut Frame, app: &App, details_chunk: Rect, actions_chunk: Rect) { - let patchset_details_and_actions = app.details_actions.as_ref().unwrap(); +fn render_details_and_actions( + f: &mut Frame, + model: &Model, + details_chunk: Rect, + actions_chunk: Rect, +) { + let patchset_details_and_actions = model.details_actions.as_ref().unwrap(); let mut staged_to_reply = String::new(); if let Some(true) = patchset_details_and_actions @@ -199,8 +204,8 @@ fn render_details_and_actions(f: &mut Frame, app: &App, details_chunk: Rect, act f.render_widget(patchset_actions, actions_chunk); } -fn render_preview(f: &mut Frame, app: &App, chunk: Rect) { - let patchset_details_and_actions = app.details_actions.as_ref().unwrap(); +fn render_preview(f: &mut Frame, model: &Model, chunk: Rect) { + let patchset_details_and_actions = model.details_actions.as_ref().unwrap(); let preview_index = patchset_details_and_actions.preview_index; @@ -210,7 +215,7 @@ fn render_preview(f: &mut Frame, app: &App, chunk: Rect) { .href; let mut preview_title = String::from(" Preview "); if matches!( - app.reviewed_patchsets.get(representative_patch_message_id), + model.reviewed_patchsets.get(representative_patch_message_id), Some(successful_indexes) if successful_indexes.contains(&preview_index) ) { preview_title = " Preview [REVIEWED-BY] ".to_string(); @@ -242,11 +247,11 @@ fn render_preview(f: &mut Frame, app: &App, chunk: Rect) { f.render_widget(patch_preview, chunk); } -pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { - let patchset_details_and_actions = app.details_actions.as_ref().unwrap(); +pub fn render_main(f: &mut Frame, model: &Model, chunk: Rect) { + let patchset_details_and_actions = model.details_actions.as_ref().unwrap(); if patchset_details_and_actions.preview_fullscreen { - render_preview(f, app, chunk); + render_preview(f, model, chunk); } else { let chunks = Layout::default() .direction(Direction::Horizontal) @@ -260,11 +265,11 @@ pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { render_details_and_actions( f, - app, + model, details_and_actions_chunks[0], details_and_actions_chunks[1], ); - render_preview(f, app, chunks[1]); + render_preview(f, model, chunks[1]); } } diff --git a/src/ui/edit_config.rs b/src/ui/edit_config.rs index 3dbb2867..799b936d 100644 --- a/src/ui/edit_config.rs +++ b/src/ui/edit_config.rs @@ -6,10 +6,10 @@ use ratatui::{ Frame, }; -use crate::{app::App, infrastructure::logging::Logger}; +use crate::{infrastructure::logging::Logger, model::Model}; -pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { - let edit_config = app.edit_config.as_ref().unwrap(); +pub fn render_main(f: &mut Frame, model: &Model, chunk: Rect) { + let edit_config = model.edit_config.as_ref().unwrap(); let mut constraints = Vec::new(); for _ in 0..(chunk.height / 3) { @@ -63,8 +63,8 @@ pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { } } -pub fn mode_footer_text(app: &App) -> Vec { - let edit_config_state = app.edit_config.as_ref().unwrap(); +pub fn mode_footer_text(model: &Model) -> Vec { + let edit_config_state = model.edit_config.as_ref().unwrap(); vec![if edit_config_state.is_editing() { Span::styled("Editing...", Style::default().fg(Color::LightYellow)) } else { @@ -72,8 +72,8 @@ pub fn mode_footer_text(app: &App) -> Vec { }] } -pub fn keys_hint(app: &App) -> Span { - let edit_config_state = app.edit_config.as_ref().unwrap(); +pub fn keys_hint(model: &Model) -> Span { + let edit_config_state = model.edit_config.as_ref().unwrap(); match edit_config_state.is_editing() { true => Span::styled( "(ESC) cancel | (ENTER) confirm", diff --git a/src/ui/latest.rs b/src/ui/latest.rs index e6776366..3d1b1a64 100644 --- a/src/ui/latest.rs +++ b/src/ui/latest.rs @@ -6,21 +6,21 @@ use ratatui::{ Frame, }; -use crate::{app::App, lore::patch::Patch}; +use crate::{lore::patch::Patch, model::Model}; -pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { - let page_number = app.latest_patchsets.as_ref().unwrap().page_number(); - let patchset_index = app.latest_patchsets.as_ref().unwrap().patchset_index(); +pub fn render_main(f: &mut Frame, model: &Model, chunk: Rect) { + let page_number = model.latest_patchsets.as_ref().unwrap().page_number(); + let patchset_index = model.latest_patchsets.as_ref().unwrap().patchset_index(); let mut list_items = Vec::::new(); - let patch_feed_page: Vec<&Patch> = app + let patch_feed_page: Vec<&Patch> = model .latest_patchsets .as_ref() .unwrap() .get_current_patch_feed_page() .unwrap(); - let mut index: usize = (page_number - 1) * app.config.page_size(); + let mut index: usize = (page_number - 1) * model.config.page_size(); for patch in patch_feed_page { let patch_title = format!("{:width$}", patch.title(), width = 70); let patch_title = format!("{:.width$}", patch_title, width = 70); @@ -61,18 +61,18 @@ pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { let mut list_state = ListState::default(); list_state.select(Some( - patchset_index - (page_number - 1) * app.config.page_size(), + patchset_index - (page_number - 1) * model.config.page_size(), )); f.render_stateful_widget(list, chunk, &mut list_state); } -pub fn mode_footer_text(app: &App) -> Vec { +pub fn mode_footer_text(model: &Model) -> Vec { vec![Span::styled( format!( "Latest Patchsets from {} (page {})", - &app.latest_patchsets.as_ref().unwrap().target_list(), - &app.latest_patchsets.as_ref().unwrap().page_number() + &model.latest_patchsets.as_ref().unwrap().target_list(), + &model.latest_patchsets.as_ref().unwrap().page_number() ), Style::default().fg(Color::Green), )] diff --git a/src/ui/mail_list.rs b/src/ui/mail_list.rs index 16686db1..91f4c5af 100644 --- a/src/ui/mail_list.rs +++ b/src/ui/mail_list.rs @@ -6,13 +6,13 @@ use ratatui::{ Frame, }; -use crate::app::App; +use crate::model::Model; -pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { - let highlighted_list_index = app.mailing_list_selection.highlighted_list_index; +pub fn render_main(f: &mut Frame, model: &Model, chunk: Rect) { + let highlighted_list_index = model.mailing_list_selection.highlighted_list_index; let mut list_items = Vec::::new(); - for mailing_list in &app.mailing_list_selection.possible_mailing_lists { + for mailing_list in &model.mailing_list_selection.possible_mailing_lists { list_items.push(ListItem::new( Line::from(vec![ Span::styled( @@ -50,35 +50,35 @@ pub fn render_main(f: &mut Frame, app: &App, chunk: Rect) { f.render_stateful_widget(list, chunk, &mut list_state); } -pub fn mode_footer_text(app: &App) -> Vec { +pub fn mode_footer_text(model: &Model) -> Vec { let mut text_area = Span::default(); - if app.mailing_list_selection.target_list.is_empty() { + if model.mailing_list_selection.target_list.is_empty() { text_area = Span::styled("type the target list", Style::default().fg(Color::DarkGray)) } else { - for mailing_list in &app.mailing_list_selection.mailing_lists { + for mailing_list in &model.mailing_list_selection.mailing_lists { if mailing_list .name() - .eq(&app.mailing_list_selection.target_list) + .eq(&model.mailing_list_selection.target_list) { text_area = Span::styled( - &app.mailing_list_selection.target_list, + &model.mailing_list_selection.target_list, Style::default().fg(Color::Green), ); break; } else if mailing_list .name() - .starts_with(&app.mailing_list_selection.target_list) + .starts_with(&model.mailing_list_selection.target_list) { text_area = Span::styled( - &app.mailing_list_selection.target_list, + &model.mailing_list_selection.target_list, Style::default().fg(Color::LightCyan), ); } } if text_area.content.is_empty() { text_area = Span::styled( - &app.mailing_list_selection.target_list, + &model.mailing_list_selection.target_list, Style::default().fg(Color::Red), ); } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index ed528754..cdfab167 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -15,9 +15,9 @@ use ratatui::{ Frame, }; -use crate::app::{screens::CurrentScreen, App}; +use crate::model::{screens::CurrentScreen, Model}; -pub fn draw_ui(f: &mut Frame, app: &App) { +pub fn draw_ui(f: &mut Frame, model: &Model) { // Clear the whole screen for sanitizing reasons f.render_widget(Clear, f.area()); @@ -32,19 +32,19 @@ pub fn draw_ui(f: &mut Frame, app: &App) { render_title(f, chunks[0]); - match app.current_screen { - CurrentScreen::MailingListSelection => mail_list::render_main(f, app, chunks[1]), + match model.current_screen { + CurrentScreen::MailingListSelection => mail_list::render_main(f, model, chunks[1]), CurrentScreen::BookmarkedPatchsets => { - bookmarked::render_main(f, &app.bookmarked_patchsets, chunks[1]) + bookmarked::render_main(f, &model.bookmarked_patchsets, chunks[1]) } - CurrentScreen::LatestPatchsets => latest::render_main(f, app, chunks[1]), - CurrentScreen::PatchsetDetails => details_actions::render_main(f, app, chunks[1]), - CurrentScreen::EditConfig => edit_config::render_main(f, app, chunks[1]), + CurrentScreen::LatestPatchsets => latest::render_main(f, model, chunks[1]), + CurrentScreen::PatchsetDetails => details_actions::render_main(f, model, chunks[1]), + CurrentScreen::EditConfig => edit_config::render_main(f, model, chunks[1]), } - navigation_bar::render(f, app, chunks[2]); + navigation_bar::render(f, model, chunks[2]); - app.popup.as_ref().inspect(|p| { + model.popup.as_ref().inspect(|p| { let (x, y) = p.dimensions(); let rect = centered_rect(x, y, f.area()); p.render(f, rect); diff --git a/src/ui/navigation_bar.rs b/src/ui/navigation_bar.rs index 47dd6a10..58bfd390 100644 --- a/src/ui/navigation_bar.rs +++ b/src/ui/navigation_bar.rs @@ -5,29 +5,29 @@ use ratatui::{ Frame, }; -use crate::app::{screens::CurrentScreen, App}; +use crate::model::{screens::CurrentScreen, Model}; use super::{bookmarked, details_actions, edit_config, latest, mail_list}; -pub fn render(f: &mut Frame, app: &App, chunk: Rect) { - let mode_footer_text = match app.current_screen { - CurrentScreen::MailingListSelection => mail_list::mode_footer_text(app), +pub fn render(f: &mut Frame, model: &Model, chunk: Rect) { + let mode_footer_text = match model.current_screen { + CurrentScreen::MailingListSelection => mail_list::mode_footer_text(model), CurrentScreen::BookmarkedPatchsets => bookmarked::mode_footer_text(), - CurrentScreen::LatestPatchsets => latest::mode_footer_text(app), + CurrentScreen::LatestPatchsets => latest::mode_footer_text(model), CurrentScreen::PatchsetDetails => details_actions::mode_footer_text(), - CurrentScreen::EditConfig => edit_config::mode_footer_text(app), + CurrentScreen::EditConfig => edit_config::mode_footer_text(model), }; let mode_footer = Paragraph::new(Line::from(mode_footer_text)) .block(Block::default().borders(Borders::ALL)) .centered(); let current_keys_hint = { - match app.current_screen { + match model.current_screen { CurrentScreen::MailingListSelection => mail_list::keys_hint(), CurrentScreen::BookmarkedPatchsets => bookmarked::keys_hint(), CurrentScreen::LatestPatchsets => latest::keys_hint(), CurrentScreen::PatchsetDetails => details_actions::keys_hint(), - CurrentScreen::EditConfig => edit_config::keys_hint(app), + CurrentScreen::EditConfig => edit_config::keys_hint(model), } }; diff --git a/src/ui/popup/review_trailers.rs b/src/ui/popup/review_trailers.rs index 5123e3e5..af919ad6 100644 --- a/src/ui/popup/review_trailers.rs +++ b/src/ui/popup/review_trailers.rs @@ -8,7 +8,7 @@ use ratatui::{ use std::collections::HashSet; -use crate::{app::screens::details_actions::DetailsActions, lore::patch::Author}; +use crate::{lore::patch::Author, model::screens::details_actions::DetailsActions}; use super::PopUp; From e589f5824372cbe6996e82fe52cd24036a47dd63 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sat, 28 Jun 2025 22:35:44 +0530 Subject: [PATCH 02/14] refactor: rename `CurrentScreen` enum to `View` to improve clarity The `View` is a crucial part of MVVM that will handle the UI. Rename the struct from `CurrentScreen`, which conveys logic, to simply `View` that will reflect it's purpose in the architecture. Signed-off-by: Ivin Joel Abraham --- src/handler/bookmarked.rs | 8 ++++---- src/handler/details_actions.rs | 4 ++-- src/handler/edit_config.rs | 4 ++-- src/handler/latest.rs | 8 ++++---- src/handler/mail_list.rs | 8 ++++---- src/handler/mod.rs | 20 ++++++++++---------- src/model/mod.rs | 12 ++++++------ src/model/screens/details_actions.rs | 4 ++-- src/model/screens/mod.rs | 2 +- src/ui/mod.rs | 12 ++++++------ src/ui/navigation_bar.rs | 22 +++++++++++----------- 11 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/handler/bookmarked.rs b/src/handler/bookmarked.rs index 35c99766..1f8e38ad 100644 --- a/src/handler/bookmarked.rs +++ b/src/handler/bookmarked.rs @@ -9,7 +9,7 @@ use std::ops::ControlFlow; use crate::{ loading_screen, lore::lore_session::B4Result, - model::{screens::CurrentScreen, Model}, + model::{screens::View, Model}, ui::popup::{help::HelpPopUpBuilder, info_popup::InfoPopUp, PopUp}, }; @@ -28,7 +28,7 @@ where } KeyCode::Esc | KeyCode::Char('q') => { model.bookmarked_patchsets.patchset_index = 0; - model.set_current_screen(CurrentScreen::MailingListSelection); + model.set_current_screen(View::MailingListSelection); } KeyCode::Char('j') | KeyCode::Down => { model.bookmarked_patchsets.select_below_patchset(); @@ -48,13 +48,13 @@ where // patchset in this list was bookmarked through the UI match result.unwrap() { B4Result::PatchFound(_) => { - model.set_current_screen(CurrentScreen::PatchsetDetails); + model.set_current_screen(View::PatchsetDetails); } B4Result::PatchNotFound(err_cause) => { model.popup = Some(InfoPopUp::generate_info_popup( "Error",&format!("The selected patchset couldn't be retrieved.\nReason: {err_cause}\nPlease choose another patchset.") )); - model.set_current_screen(CurrentScreen::BookmarkedPatchsets); + model.set_current_screen(View::BookmarkedPatchsets); } } } diff --git a/src/handler/details_actions.rs b/src/handler/details_actions.rs index 7da0474d..172c7a14 100644 --- a/src/handler/details_actions.rs +++ b/src/handler/details_actions.rs @@ -8,7 +8,7 @@ use std::time::Duration; use crate::{ infrastructure::terminal::{setup_user_io, teardown_user_io}, - model::{screens::CurrentScreen, Model}, + model::{screens::View, Model}, ui::popup::{help::HelpPopUpBuilder, review_trailers::ReviewTrailersPopUp, PopUp}, }; @@ -122,7 +122,7 @@ pub fn handle_patchset_details( } else { model.consolidate_patchset_actions()?; } - model.set_current_screen(CurrentScreen::PatchsetDetails); + model.set_current_screen(View::PatchsetDetails); } _ => {} } diff --git a/src/handler/edit_config.rs b/src/handler/edit_config.rs index 991caa41..f2f818d2 100644 --- a/src/handler/edit_config.rs +++ b/src/handler/edit_config.rs @@ -1,7 +1,7 @@ use ratatui::crossterm::event::{KeyCode, KeyEvent}; use crate::{ - model::{screens::CurrentScreen, Model}, + model::{screens::View, Model}, ui::popup::{help::HelpPopUpBuilder, PopUp}, }; @@ -35,7 +35,7 @@ pub fn handle_edit_config(model: &mut Model, key: KeyEvent) -> color_eyre::Resul model.consolidate_edit_config(); model.config.save_patch_hub_config()?; model.reset_edit_config(); - model.set_current_screen(CurrentScreen::MailingListSelection); + model.set_current_screen(View::MailingListSelection); } KeyCode::Enter => { edit_config_state.toggle_editing(); diff --git a/src/handler/latest.rs b/src/handler/latest.rs index cb01fc25..639ddc61 100644 --- a/src/handler/latest.rs +++ b/src/handler/latest.rs @@ -9,7 +9,7 @@ use std::ops::ControlFlow; use crate::{ loading_screen, lore::lore_session::B4Result, - model::{screens::CurrentScreen, Model}, + model::{screens::View, Model}, ui::popup::{help::HelpPopUpBuilder, info_popup::InfoPopUp, PopUp}, }; @@ -30,7 +30,7 @@ where } KeyCode::Esc | KeyCode::Char('q') => { model.reset_latest_patchsets(); - model.set_current_screen(CurrentScreen::MailingListSelection); + model.set_current_screen(View::MailingListSelection); } KeyCode::Char('j') | KeyCode::Down => { latest_patchsets.select_below_patchset(); @@ -59,13 +59,13 @@ where if result.is_ok() { match result.unwrap() { B4Result::PatchFound(_) => { - model.set_current_screen(CurrentScreen::PatchsetDetails); + model.set_current_screen(View::PatchsetDetails); } B4Result::PatchNotFound(err_cause) => { model.popup = Some(InfoPopUp::generate_info_popup( "Error",&format!("The selected patchset couldn't be retrieved.\nReason: {err_cause}\nPlease choose another patchset.") )); - model.set_current_screen(CurrentScreen::LatestPatchsets); + model.set_current_screen(View::LatestPatchsets); } } } diff --git a/src/handler/mail_list.rs b/src/handler/mail_list.rs index 48097948..aa5003c3 100644 --- a/src/handler/mail_list.rs +++ b/src/handler/mail_list.rs @@ -8,7 +8,7 @@ use std::ops::ControlFlow; use crate::{ loading_screen, - model::{screens::CurrentScreen, Model}, + model::{screens::View, Model}, ui::popup::{help::HelpPopUpBuilder, PopUp}, }; @@ -43,7 +43,7 @@ where .fetch_current_page(); if result.is_ok() { model.mailing_list_selection.clear_target_list(); - model.set_current_screen(CurrentScreen::LatestPatchsets); + model.set_current_screen(View::LatestPatchsets); } result } @@ -61,12 +61,12 @@ where } KeyCode::F(2) => { model.init_edit_config(); - model.set_current_screen(CurrentScreen::EditConfig); + model.set_current_screen(View::EditConfig); } KeyCode::F(1) => { if !model.bookmarked_patchsets.bookmarked_patchsets.is_empty() { model.mailing_list_selection.clear_target_list(); - model.set_current_screen(CurrentScreen::BookmarkedPatchsets); + model.set_current_screen(View::BookmarkedPatchsets); } } KeyCode::Backspace => { diff --git a/src/handler/mod.rs b/src/handler/mod.rs index b0a54909..e0fbb168 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -17,7 +17,7 @@ use std::{ use crate::{ loading_screen, - model::{screens::CurrentScreen, Model}, + model::{screens::View, Model}, ui::draw_ui, }; @@ -43,19 +43,19 @@ where } } else { match model.current_screen { - CurrentScreen::MailingListSelection => { + View::MailingListSelection => { return handle_mailing_list_selection(model, key, terminal); } - CurrentScreen::BookmarkedPatchsets => { + View::BookmarkedPatchsets => { return handle_bookmarked_patchsets(model, key, terminal); } - CurrentScreen::PatchsetDetails => { + View::PatchsetDetails => { handle_patchset_details(model, key, &mut terminal)?; } - CurrentScreen::EditConfig => { + View::EditConfig => { handle_edit_config(model, key)?; } - CurrentScreen::LatestPatchsets => { + View::LatestPatchsets => { return handle_latest_patchsets(model, key, terminal); } } @@ -71,7 +71,7 @@ where B: Backend + Send + 'static, { match model.current_screen { - CurrentScreen::MailingListSelection => { + View::MailingListSelection => { if model.mailing_list_selection.mailing_lists.is_empty() { terminal = loading_screen! { terminal, "Fetching mailing lists" => { @@ -80,7 +80,7 @@ where }; } } - CurrentScreen::LatestPatchsets => { + View::LatestPatchsets => { let patchsets_state = model.latest_patchsets.as_mut().unwrap(); let target_list = patchsets_state.target_list().to_string(); if patchsets_state.processed_patchsets_count() == 0 { @@ -94,9 +94,9 @@ where model.mailing_list_selection.clear_target_list(); } } - CurrentScreen::BookmarkedPatchsets => { + View::BookmarkedPatchsets => { if model.bookmarked_patchsets.bookmarked_patchsets.is_empty() { - model.set_current_screen(CurrentScreen::MailingListSelection); + model.set_current_screen(View::MailingListSelection); } } _ => {} diff --git a/src/model/mod.rs b/src/model/mod.rs index 552e83ba..7117f412 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -29,14 +29,14 @@ use screens::{ edit_config::EditConfig, latest::LatestPatchsets, mail_list::MailingListSelection, - CurrentScreen, + View, }; /// Type that represents the overall state of the application. It can be viewed /// as the **Model** component of `patch-hub`. pub struct Model { /// The current active screen - pub current_screen: CurrentScreen, + pub current_screen: View, /// Screen to navigate and select the mailing lists archived on Lore pub mailing_list_selection: MailingListSelection, /// Screen with listing patchsets that were previously bookmarked @@ -85,7 +85,7 @@ impl Model { garbage_collector::collect_garbage(&config); Ok(Model { - current_screen: CurrentScreen::MailingListSelection, + current_screen: View::MailingListSelection, mailing_list_selection: MailingListSelection { mailing_lists: mailing_lists.clone(), target_list: String::new(), @@ -147,10 +147,10 @@ impl Model { let mut acked_by = Vec::new(); match &self.current_screen { - CurrentScreen::BookmarkedPatchsets => { + View::BookmarkedPatchsets => { representative_patch = self.bookmarked_patchsets.get_selected_patchset(); } - CurrentScreen::LatestPatchsets => { + View::LatestPatchsets => { representative_patch = self .latest_patchsets .as_ref() @@ -392,7 +392,7 @@ impl Model { } /// Change the current active screen in [App::current_screen]. - pub fn set_current_screen(&mut self, new_current_screen: CurrentScreen) { + pub fn set_current_screen(&mut self, new_current_screen: View) { self.current_screen = new_current_screen; } diff --git a/src/model/screens/details_actions.rs b/src/model/screens/details_actions.rs index 856f55dc..2df83b22 100644 --- a/src/model/screens/details_actions.rs +++ b/src/model/screens/details_actions.rs @@ -16,7 +16,7 @@ use crate::{ model::config::{Config, KernelTree}, }; -use super::CurrentScreen; +use super::View; pub struct DetailsActions { pub representative_patch: Patch, @@ -44,7 +44,7 @@ pub struct DetailsActions { pub tested_by: Vec>, /// For each patch, a set of `Authors` that appear in `Acked-by` trailers pub acked_by: Vec>, - pub last_screen: CurrentScreen, + pub last_screen: View, pub lore_api_client: BlockingLoreAPIClient, } diff --git a/src/model/screens/mod.rs b/src/model/screens/mod.rs index 4fb235bf..a39799d1 100644 --- a/src/model/screens/mod.rs +++ b/src/model/screens/mod.rs @@ -5,7 +5,7 @@ pub mod latest; pub mod mail_list; #[derive(Debug, Clone, PartialEq)] -pub enum CurrentScreen { +pub enum View { MailingListSelection, BookmarkedPatchsets, LatestPatchsets, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index cdfab167..d855d6ec 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -15,7 +15,7 @@ use ratatui::{ Frame, }; -use crate::model::{screens::CurrentScreen, Model}; +use crate::model::{screens::View, Model}; pub fn draw_ui(f: &mut Frame, model: &Model) { // Clear the whole screen for sanitizing reasons @@ -33,13 +33,13 @@ pub fn draw_ui(f: &mut Frame, model: &Model) { render_title(f, chunks[0]); match model.current_screen { - CurrentScreen::MailingListSelection => mail_list::render_main(f, model, chunks[1]), - CurrentScreen::BookmarkedPatchsets => { + View::MailingListSelection => mail_list::render_main(f, model, chunks[1]), + View::BookmarkedPatchsets => { bookmarked::render_main(f, &model.bookmarked_patchsets, chunks[1]) } - CurrentScreen::LatestPatchsets => latest::render_main(f, model, chunks[1]), - CurrentScreen::PatchsetDetails => details_actions::render_main(f, model, chunks[1]), - CurrentScreen::EditConfig => edit_config::render_main(f, model, chunks[1]), + View::LatestPatchsets => latest::render_main(f, model, chunks[1]), + View::PatchsetDetails => details_actions::render_main(f, model, chunks[1]), + View::EditConfig => edit_config::render_main(f, model, chunks[1]), } navigation_bar::render(f, model, chunks[2]); diff --git a/src/ui/navigation_bar.rs b/src/ui/navigation_bar.rs index 58bfd390..d9b394ec 100644 --- a/src/ui/navigation_bar.rs +++ b/src/ui/navigation_bar.rs @@ -5,17 +5,17 @@ use ratatui::{ Frame, }; -use crate::model::{screens::CurrentScreen, Model}; +use crate::model::{screens::View, Model}; use super::{bookmarked, details_actions, edit_config, latest, mail_list}; pub fn render(f: &mut Frame, model: &Model, chunk: Rect) { let mode_footer_text = match model.current_screen { - CurrentScreen::MailingListSelection => mail_list::mode_footer_text(model), - CurrentScreen::BookmarkedPatchsets => bookmarked::mode_footer_text(), - CurrentScreen::LatestPatchsets => latest::mode_footer_text(model), - CurrentScreen::PatchsetDetails => details_actions::mode_footer_text(), - CurrentScreen::EditConfig => edit_config::mode_footer_text(model), + View::MailingListSelection => mail_list::mode_footer_text(model), + View::BookmarkedPatchsets => bookmarked::mode_footer_text(), + View::LatestPatchsets => latest::mode_footer_text(model), + View::PatchsetDetails => details_actions::mode_footer_text(), + View::EditConfig => edit_config::mode_footer_text(model), }; let mode_footer = Paragraph::new(Line::from(mode_footer_text)) .block(Block::default().borders(Borders::ALL)) @@ -23,11 +23,11 @@ pub fn render(f: &mut Frame, model: &Model, chunk: Rect) { let current_keys_hint = { match model.current_screen { - CurrentScreen::MailingListSelection => mail_list::keys_hint(), - CurrentScreen::BookmarkedPatchsets => bookmarked::keys_hint(), - CurrentScreen::LatestPatchsets => latest::keys_hint(), - CurrentScreen::PatchsetDetails => details_actions::keys_hint(), - CurrentScreen::EditConfig => edit_config::keys_hint(model), + View::MailingListSelection => mail_list::keys_hint(), + View::BookmarkedPatchsets => bookmarked::keys_hint(), + View::LatestPatchsets => latest::keys_hint(), + View::PatchsetDetails => details_actions::keys_hint(), + View::EditConfig => edit_config::keys_hint(model), } }; From 0fb67e648bee6f03f2c0265d3eb7c549f86efbbb Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sat, 28 Jun 2025 23:30:07 +0530 Subject: [PATCH 03/14] refactor: introduce ViewModel trait The MVVM refactor will require a trait for viewmodels to establish a reliable interface. Introduce a basic implementation of the trait with no usage. Signed-off-by: Ivin Joel Abraham --- src/viewmodels/mod.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/viewmodels/mod.rs diff --git a/src/viewmodels/mod.rs b/src/viewmodels/mod.rs new file mode 100644 index 00000000..46c2d4c5 --- /dev/null +++ b/src/viewmodels/mod.rs @@ -0,0 +1,36 @@ +use ratatui::crossterm::event::Event; +#[allow(dead_code)] +/// A trait representing a view model that can handle key events and expose its state. +/// +/// # Associated Types +/// - `State`: The type representing the state of the view model. Must have a `'static` lifetime. +/// +/// # Required Methods +/// - `handle_key`: Handles a key event, potentially mutating the view model. +/// - `state`: Returns a reference to the current state. +pub trait ViewModel { + /// Handles a key event, potentially mutating the view model. + /// + /// # Arguments + /// + /// * `event` - The event to handle. + fn handle_key(&mut self, event: Event); + + /// Returns a reference to the current state. + fn state(&self) -> ViewModelState; +} + +/// `ViewModelState` is an enum intended to represent the various possible +/// state structs used by different screens in the application. Each variant +/// of this enum should wrap the state struct corresponding to a specific screen, +/// allowing for type-safe handling and dynamic dispatch of view model states. +/// +/// Extend this enum by adding variants for each screen's state struct, for example: +/// +/// ``` +/// enum ViewModelState { +/// HomeScreen(HomeScreenState), +/// SettingsScreen(SettingsScreenState), +/// // Add more variants as needed +/// } +pub enum ViewModelState {} From 3c044a962bb94a89e02fa7203a46ca70c323251f Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sun, 29 Jun 2025 08:58:16 +0530 Subject: [PATCH 04/14] refactor: derive `Eq`, `Hash` and `Copy` for View To use as a key in a hashmap, a type must implement `Eq` and `Hash`. Additionally, `Copy` is required as well to be used in the .entry() method of Hashmap. Derive `Eq`, `Copy` and `Hash` on View to use it as a key in Hashmaps. Signed-off-by: Ivin Joel Abraham Signed-off-by: Ivin Joel Abraham --- src/handler/details_actions.rs | 2 +- src/model/mod.rs | 2 +- src/model/screens/mod.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/handler/details_actions.rs b/src/handler/details_actions.rs index 172c7a14..7d9172b0 100644 --- a/src/handler/details_actions.rs +++ b/src/handler/details_actions.rs @@ -64,7 +64,7 @@ pub fn handle_patchset_details( model.popup = Some(popup); } KeyCode::Esc | KeyCode::Char('q') => { - let ps_da_clone = patchset_details_and_actions.last_screen.clone(); + let ps_da_clone = patchset_details_and_actions.last_screen; model.set_current_screen(ps_da_clone); model.reset_details_actions(); } diff --git a/src/model/mod.rs b/src/model/mod.rs index 7117f412..354dcdcb 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -255,7 +255,7 @@ impl Model { reviewed_by, tested_by, acked_by, - last_screen: self.current_screen.clone(), + last_screen: self.current_screen, lore_api_client: self.lore_api_client.clone(), patchset_path, }); diff --git a/src/model/screens/mod.rs b/src/model/screens/mod.rs index a39799d1..9529c20c 100644 --- a/src/model/screens/mod.rs +++ b/src/model/screens/mod.rs @@ -4,7 +4,7 @@ pub mod edit_config; pub mod latest; pub mod mail_list; -#[derive(Debug, Clone, PartialEq)] +#[derive(Copy, Debug, Clone, PartialEq, Eq, Hash)] pub enum View { MailingListSelection, BookmarkedPatchsets, From 68dac469fbeea89497365edd69ae8700df3851ef Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sat, 9 Aug 2025 12:32:42 +0530 Subject: [PATCH 05/14] feat(View): add stub implementation of draw_screen The View component in MVVM handles UI layout and construction. Though it is currently in the model directory, the `View` enum will be our View component. Future commits will move this to it's own directory. Add a stub implementation of a method that can draw the current screen. Signed-off-by: Ivin Joel Abraham --- src/model/screens/mod.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/model/screens/mod.rs b/src/model/screens/mod.rs index 9529c20c..de9568cd 100644 --- a/src/model/screens/mod.rs +++ b/src/model/screens/mod.rs @@ -12,3 +12,16 @@ pub enum View { PatchsetDetails, EditConfig, } + +impl View { + #[allow(dead_code)] + pub fn draw_screen(&self) { + match self { + View::MailingListSelection => todo!(), + View::BookmarkedPatchsets => todo!(), + View::LatestPatchsets => todo!(), + View::PatchsetDetails => todo!(), + View::EditConfig => todo!(), + } + } +} From 87d08c580ce899bccc383caa9df0a8f94146c669 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sat, 9 Aug 2025 12:46:31 +0530 Subject: [PATCH 06/14] fix(ViewModel): change handle_key parameter to handle KeyEvent The previous implementation, `key_handling`, used to match `KeyEvent` and not `Event`. Correct the mistake in the new version. Additionally, remove the comments that were getting outdated quickly due to rapid development. Signed-off-by: Ivin Joel Abraham --- src/viewmodels/mod.rs | 33 ++++----------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/src/viewmodels/mod.rs b/src/viewmodels/mod.rs index 46c2d4c5..3b669f11 100644 --- a/src/viewmodels/mod.rs +++ b/src/viewmodels/mod.rs @@ -1,36 +1,11 @@ -use ratatui::crossterm::event::Event; +mod mailing_list_viewmodel; + +use ratatui::crossterm::event::KeyEvent; #[allow(dead_code)] -/// A trait representing a view model that can handle key events and expose its state. -/// -/// # Associated Types -/// - `State`: The type representing the state of the view model. Must have a `'static` lifetime. -/// -/// # Required Methods -/// - `handle_key`: Handles a key event, potentially mutating the view model. -/// - `state`: Returns a reference to the current state. pub trait ViewModel { - /// Handles a key event, potentially mutating the view model. - /// - /// # Arguments - /// - /// * `event` - The event to handle. - fn handle_key(&mut self, event: Event); + fn handle_key(&self, event: KeyEvent); - /// Returns a reference to the current state. fn state(&self) -> ViewModelState; } -/// `ViewModelState` is an enum intended to represent the various possible -/// state structs used by different screens in the application. Each variant -/// of this enum should wrap the state struct corresponding to a specific screen, -/// allowing for type-safe handling and dynamic dispatch of view model states. -/// -/// Extend this enum by adding variants for each screen's state struct, for example: -/// -/// ``` -/// enum ViewModelState { -/// HomeScreen(HomeScreenState), -/// SettingsScreen(SettingsScreenState), -/// // Add more variants as needed -/// } pub enum ViewModelState {} From 0fd1aaaead005bd358bb9841d8ba31cfceebc4ea Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sat, 9 Aug 2025 12:51:16 +0530 Subject: [PATCH 07/14] feat(App): introduce a central manager The multiple components in the proposed MVVM architecture require a central manager to orchestrate event handling, controlling and orchestration. Introduce an `App` to be the central manager with draft implementations for methods. Signed-off-by: Ivin Joel Abraham --- src/app.rs | 68 +++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 2 ++ src/viewmodels/mod.rs | 2 -- 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 src/app.rs diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 00000000..884fb9a1 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,68 @@ +use crate::viewmodels::ViewModel; +use std::collections::HashMap; + +use color_eyre::eyre::Result; +use ratatui::crossterm::event::{self, Event, KeyEventKind}; + +use crate::model::{screens::View, Model}; + +#[allow(dead_code)] +pub struct App { + model: Model, + current_view: View, + viewmodels: HashMap>, +} + +impl App { + #[allow(dead_code)] + pub fn new(model: Model) -> color_eyre::Result { + Ok(App { + model, + current_view: View::MailingListSelection, + viewmodels: HashMap::new(), + }) + } + + #[allow(dead_code)] + pub fn get_current_view(&self) -> View { + self.current_view + } + + #[allow(dead_code)] + pub fn get_current_viewmodel(&mut self) -> &mut Box { + self.viewmodels + .entry(self.current_view) + .or_insert_with(|| match self.current_view { + View::MailingListSelection => { + todo!() + } + View::BookmarkedPatchsets => { + todo!() + } + View::LatestPatchsets => { + todo!() + } + View::PatchsetDetails => { + todo!() + } + View::EditConfig => { + todo!() + } + }) + } + + #[allow(dead_code)] + pub fn run(&mut self) -> Result<()> { + loop { + self.get_current_view().draw_screen(); + + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Release { + continue; + } + + self.get_current_viewmodel().handle_key(key); + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 998a44b3..64543f2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod app; mod cli; mod handler; mod infrastructure; @@ -5,6 +6,7 @@ mod lore; mod macros; mod model; mod ui; +mod viewmodels; use clap::Parser; use cli::Cli; diff --git a/src/viewmodels/mod.rs b/src/viewmodels/mod.rs index 3b669f11..598f0a00 100644 --- a/src/viewmodels/mod.rs +++ b/src/viewmodels/mod.rs @@ -1,5 +1,3 @@ -mod mailing_list_viewmodel; - use ratatui::crossterm::event::KeyEvent; #[allow(dead_code)] pub trait ViewModel { From 9b6203b4f2e685d24aa5016c241f5c9d57519d84 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sat, 9 Aug 2025 18:52:35 +0530 Subject: [PATCH 08/14] refactor(View): rename `ui` directory to `view` The `ui` directory contains the functions that build the screen which belongs to the View component in the MVVM architecture. Replace the old name with `views`. Signed-off-by: Ivin Joel Abraham --- src/handler/bookmarked.rs | 2 +- src/handler/details_actions.rs | 2 +- src/handler/edit_config.rs | 2 +- src/handler/latest.rs | 2 +- src/handler/mail_list.rs | 2 +- src/handler/mod.rs | 2 +- src/macros.rs | 2 +- src/main.rs | 2 +- src/model/mod.rs | 2 +- src/{ui => views}/bookmarked.rs | 0 src/{ui => views}/details_actions.rs | 0 src/{ui => views}/edit_config.rs | 0 src/{ui => views}/latest.rs | 0 src/{ui => views}/loading_screen.rs | 0 src/{ui => views}/mail_list.rs | 0 src/{ui => views}/mod.rs | 0 src/{ui => views}/navigation_bar.rs | 0 src/{ui => views}/popup/help.rs | 0 src/{ui => views}/popup/info_popup.rs | 0 src/{ui => views}/popup/mod.rs | 0 src/{ui => views}/popup/review_trailers.rs | 0 21 files changed, 9 insertions(+), 9 deletions(-) rename src/{ui => views}/bookmarked.rs (100%) rename src/{ui => views}/details_actions.rs (100%) rename src/{ui => views}/edit_config.rs (100%) rename src/{ui => views}/latest.rs (100%) rename src/{ui => views}/loading_screen.rs (100%) rename src/{ui => views}/mail_list.rs (100%) rename src/{ui => views}/mod.rs (100%) rename src/{ui => views}/navigation_bar.rs (100%) rename src/{ui => views}/popup/help.rs (100%) rename src/{ui => views}/popup/info_popup.rs (100%) rename src/{ui => views}/popup/mod.rs (100%) rename src/{ui => views}/popup/review_trailers.rs (100%) diff --git a/src/handler/bookmarked.rs b/src/handler/bookmarked.rs index 1f8e38ad..dd8399d9 100644 --- a/src/handler/bookmarked.rs +++ b/src/handler/bookmarked.rs @@ -10,7 +10,7 @@ use crate::{ loading_screen, lore::lore_session::B4Result, model::{screens::View, Model}, - ui::popup::{help::HelpPopUpBuilder, info_popup::InfoPopUp, PopUp}, + views::popup::{help::HelpPopUpBuilder, info_popup::InfoPopUp, PopUp}, }; pub fn handle_bookmarked_patchsets( diff --git a/src/handler/details_actions.rs b/src/handler/details_actions.rs index 7d9172b0..3b316316 100644 --- a/src/handler/details_actions.rs +++ b/src/handler/details_actions.rs @@ -9,7 +9,7 @@ use std::time::Duration; use crate::{ infrastructure::terminal::{setup_user_io, teardown_user_io}, model::{screens::View, Model}, - ui::popup::{help::HelpPopUpBuilder, review_trailers::ReviewTrailersPopUp, PopUp}, + views::popup::{help::HelpPopUpBuilder, review_trailers::ReviewTrailersPopUp, PopUp}, }; use super::wait_key_press; diff --git a/src/handler/edit_config.rs b/src/handler/edit_config.rs index f2f818d2..5c515693 100644 --- a/src/handler/edit_config.rs +++ b/src/handler/edit_config.rs @@ -2,7 +2,7 @@ use ratatui::crossterm::event::{KeyCode, KeyEvent}; use crate::{ model::{screens::View, Model}, - ui::popup::{help::HelpPopUpBuilder, PopUp}, + views::popup::{help::HelpPopUpBuilder, PopUp}, }; pub fn handle_edit_config(model: &mut Model, key: KeyEvent) -> color_eyre::Result<()> { diff --git a/src/handler/latest.rs b/src/handler/latest.rs index 639ddc61..c66974bf 100644 --- a/src/handler/latest.rs +++ b/src/handler/latest.rs @@ -10,7 +10,7 @@ use crate::{ loading_screen, lore::lore_session::B4Result, model::{screens::View, Model}, - ui::popup::{help::HelpPopUpBuilder, info_popup::InfoPopUp, PopUp}, + views::popup::{help::HelpPopUpBuilder, info_popup::InfoPopUp, PopUp}, }; pub fn handle_latest_patchsets( diff --git a/src/handler/mail_list.rs b/src/handler/mail_list.rs index aa5003c3..cfe2c2b8 100644 --- a/src/handler/mail_list.rs +++ b/src/handler/mail_list.rs @@ -9,7 +9,7 @@ use std::ops::ControlFlow; use crate::{ loading_screen, model::{screens::View, Model}, - ui::popup::{help::HelpPopUpBuilder, PopUp}, + views::popup::{help::HelpPopUpBuilder, PopUp}, }; pub fn handle_mailing_list_selection( diff --git a/src/handler/mod.rs b/src/handler/mod.rs index e0fbb168..2112d9a7 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -18,7 +18,7 @@ use std::{ use crate::{ loading_screen, model::{screens::View, Model}, - ui::draw_ui, + views::draw_ui, }; use bookmarked::handle_bookmarked_patchsets; diff --git a/src/macros.rs b/src/macros.rs index 9664121a..1862b308 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -26,7 +26,7 @@ macro_rules! loading_screen { let handle = std::thread::spawn(move || { while loading_clone.load(std::sync::atomic::Ordering::Relaxed) { - terminal = $crate::ui::loading_screen::render(terminal, $title); + terminal = $crate::views::loading_screen::render(terminal, $title); std::thread::sleep(std::time::Duration::from_millis(200)); } diff --git a/src/main.rs b/src/main.rs index 64543f2c..5b2f68bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,8 +5,8 @@ mod infrastructure; mod lore; mod macros; mod model; -mod ui; mod viewmodels; +mod views; use clap::Parser; use cli::Cli; diff --git a/src/model/mod.rs b/src/model/mod.rs index 354dcdcb..606befdf 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -17,7 +17,7 @@ use crate::{ lore_session::{self, B4Result}, patch::{Author, Patch}, }, - ui::popup::{info_popup::InfoPopUp, PopUp}, + views::popup::{info_popup::InfoPopUp, PopUp}, }; use config::Config; diff --git a/src/ui/bookmarked.rs b/src/views/bookmarked.rs similarity index 100% rename from src/ui/bookmarked.rs rename to src/views/bookmarked.rs diff --git a/src/ui/details_actions.rs b/src/views/details_actions.rs similarity index 100% rename from src/ui/details_actions.rs rename to src/views/details_actions.rs diff --git a/src/ui/edit_config.rs b/src/views/edit_config.rs similarity index 100% rename from src/ui/edit_config.rs rename to src/views/edit_config.rs diff --git a/src/ui/latest.rs b/src/views/latest.rs similarity index 100% rename from src/ui/latest.rs rename to src/views/latest.rs diff --git a/src/ui/loading_screen.rs b/src/views/loading_screen.rs similarity index 100% rename from src/ui/loading_screen.rs rename to src/views/loading_screen.rs diff --git a/src/ui/mail_list.rs b/src/views/mail_list.rs similarity index 100% rename from src/ui/mail_list.rs rename to src/views/mail_list.rs diff --git a/src/ui/mod.rs b/src/views/mod.rs similarity index 100% rename from src/ui/mod.rs rename to src/views/mod.rs diff --git a/src/ui/navigation_bar.rs b/src/views/navigation_bar.rs similarity index 100% rename from src/ui/navigation_bar.rs rename to src/views/navigation_bar.rs diff --git a/src/ui/popup/help.rs b/src/views/popup/help.rs similarity index 100% rename from src/ui/popup/help.rs rename to src/views/popup/help.rs diff --git a/src/ui/popup/info_popup.rs b/src/views/popup/info_popup.rs similarity index 100% rename from src/ui/popup/info_popup.rs rename to src/views/popup/info_popup.rs diff --git a/src/ui/popup/mod.rs b/src/views/popup/mod.rs similarity index 100% rename from src/ui/popup/mod.rs rename to src/views/popup/mod.rs diff --git a/src/ui/popup/review_trailers.rs b/src/views/popup/review_trailers.rs similarity index 100% rename from src/ui/popup/review_trailers.rs rename to src/views/popup/review_trailers.rs From 878dae50a425854951fd830819b04ec9c788d301 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sat, 9 Aug 2025 18:55:00 +0530 Subject: [PATCH 09/14] feat(View): introduce State parameter for `draw_screen` Drawing a screen would require information about the screen's state, which shouldn't be available in the View itself. Pass state to the `draw_screen` function. Signed-off-by: Ivin Joel Abraham --- src/app.rs | 5 +++-- src/model/screens/mod.rs | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app.rs b/src/app.rs index 884fb9a1..859de78e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -51,10 +51,11 @@ impl App { }) } - #[allow(dead_code)] + #[allow(dead_code, unreachable_code)] pub fn run(&mut self) -> Result<()> { loop { - self.get_current_view().draw_screen(); + self.get_current_view() + .draw_screen(self.get_current_viewmodel().state()); if let Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Release { diff --git a/src/model/screens/mod.rs b/src/model/screens/mod.rs index de9568cd..3a7ce04f 100644 --- a/src/model/screens/mod.rs +++ b/src/model/screens/mod.rs @@ -1,3 +1,5 @@ +use crate::viewmodels::ViewModelState; + pub mod bookmarked; pub mod details_actions; pub mod edit_config; @@ -14,8 +16,8 @@ pub enum View { } impl View { - #[allow(dead_code)] - pub fn draw_screen(&self) { + #[allow(dead_code, unused_variables)] + pub fn draw_screen(&self, state: ViewModelState) { match self { View::MailingListSelection => todo!(), View::BookmarkedPatchsets => todo!(), From 3a76e8f6004cabb9fb14b235fded1ad15e5b819c Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sat, 9 Aug 2025 19:05:51 +0530 Subject: [PATCH 10/14] refactor(View): move View implementation to View directory Signed-off-by: Ivin Joel Abraham --- src/app.rs | 4 ++-- src/handler/bookmarked.rs | 7 +++++-- src/handler/details_actions.rs | 7 +++++-- src/handler/edit_config.rs | 7 +++++-- src/handler/latest.rs | 7 +++++-- src/handler/mail_list.rs | 3 ++- src/handler/mod.rs | 4 ++-- src/model/mod.rs | 6 ++++-- src/model/screens/details_actions.rs | 3 +-- src/model/screens/mod.rs | 24 ------------------------ src/views/mod.rs | 25 ++++++++++++++++++++++++- src/views/navigation_bar.rs | 2 +- 12 files changed, 56 insertions(+), 43 deletions(-) diff --git a/src/app.rs b/src/app.rs index 859de78e..239cf9eb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,10 +1,10 @@ -use crate::viewmodels::ViewModel; +use crate::{viewmodels::ViewModel, views::View}; use std::collections::HashMap; use color_eyre::eyre::Result; use ratatui::crossterm::event::{self, Event, KeyEventKind}; -use crate::model::{screens::View, Model}; +use crate::model::Model; #[allow(dead_code)] pub struct App { diff --git a/src/handler/bookmarked.rs b/src/handler/bookmarked.rs index dd8399d9..cd283769 100644 --- a/src/handler/bookmarked.rs +++ b/src/handler/bookmarked.rs @@ -9,8 +9,11 @@ use std::ops::ControlFlow; use crate::{ loading_screen, lore::lore_session::B4Result, - model::{screens::View, Model}, - views::popup::{help::HelpPopUpBuilder, info_popup::InfoPopUp, PopUp}, + model::Model, + views::{ + popup::{help::HelpPopUpBuilder, info_popup::InfoPopUp, PopUp}, + View, + }, }; pub fn handle_bookmarked_patchsets( diff --git a/src/handler/details_actions.rs b/src/handler/details_actions.rs index 3b316316..97e8a33d 100644 --- a/src/handler/details_actions.rs +++ b/src/handler/details_actions.rs @@ -8,8 +8,11 @@ use std::time::Duration; use crate::{ infrastructure::terminal::{setup_user_io, teardown_user_io}, - model::{screens::View, Model}, - views::popup::{help::HelpPopUpBuilder, review_trailers::ReviewTrailersPopUp, PopUp}, + model::Model, + views::{ + popup::{help::HelpPopUpBuilder, review_trailers::ReviewTrailersPopUp, PopUp}, + View, + }, }; use super::wait_key_press; diff --git a/src/handler/edit_config.rs b/src/handler/edit_config.rs index 5c515693..6f7f1e4f 100644 --- a/src/handler/edit_config.rs +++ b/src/handler/edit_config.rs @@ -1,8 +1,11 @@ use ratatui::crossterm::event::{KeyCode, KeyEvent}; use crate::{ - model::{screens::View, Model}, - views::popup::{help::HelpPopUpBuilder, PopUp}, + model::Model, + views::{ + popup::{help::HelpPopUpBuilder, PopUp}, + View, + }, }; pub fn handle_edit_config(model: &mut Model, key: KeyEvent) -> color_eyre::Result<()> { diff --git a/src/handler/latest.rs b/src/handler/latest.rs index c66974bf..39c6ce7f 100644 --- a/src/handler/latest.rs +++ b/src/handler/latest.rs @@ -9,8 +9,11 @@ use std::ops::ControlFlow; use crate::{ loading_screen, lore::lore_session::B4Result, - model::{screens::View, Model}, - views::popup::{help::HelpPopUpBuilder, info_popup::InfoPopUp, PopUp}, + model::Model, + views::{ + popup::{help::HelpPopUpBuilder, info_popup::InfoPopUp, PopUp}, + View, + }, }; pub fn handle_latest_patchsets( diff --git a/src/handler/mail_list.rs b/src/handler/mail_list.rs index cfe2c2b8..863c7586 100644 --- a/src/handler/mail_list.rs +++ b/src/handler/mail_list.rs @@ -1,3 +1,4 @@ +use crate::views::View; use ratatui::{ crossterm::event::{KeyCode, KeyEvent}, prelude::Backend, @@ -8,7 +9,7 @@ use std::ops::ControlFlow; use crate::{ loading_screen, - model::{screens::View, Model}, + model::Model, views::popup::{help::HelpPopUpBuilder, PopUp}, }; diff --git a/src/handler/mod.rs b/src/handler/mod.rs index 2112d9a7..9b448c15 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -17,8 +17,8 @@ use std::{ use crate::{ loading_screen, - model::{screens::View, Model}, - views::draw_ui, + model::Model, + views::{draw_ui, View}, }; use bookmarked::handle_bookmarked_patchsets; diff --git a/src/model/mod.rs b/src/model/mod.rs index 606befdf..2f4edc53 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -17,7 +17,10 @@ use crate::{ lore_session::{self, B4Result}, patch::{Author, Patch}, }, - views::popup::{info_popup::InfoPopUp, PopUp}, + views::{ + popup::{info_popup::InfoPopUp, PopUp}, + View, + }, }; use config::Config; @@ -29,7 +32,6 @@ use screens::{ edit_config::EditConfig, latest::LatestPatchsets, mail_list::MailingListSelection, - View, }; /// Type that represents the overall state of the application. It can be viewed diff --git a/src/model/screens/details_actions.rs b/src/model/screens/details_actions.rs index 2df83b22..e1c8de4c 100644 --- a/src/model/screens/details_actions.rs +++ b/src/model/screens/details_actions.rs @@ -14,10 +14,9 @@ use crate::{ patch::{Author, Patch}, }, model::config::{Config, KernelTree}, + views::View, }; -use super::View; - pub struct DetailsActions { pub representative_patch: Patch, /// Raw patches as plain text files diff --git a/src/model/screens/mod.rs b/src/model/screens/mod.rs index 3a7ce04f..ead9434c 100644 --- a/src/model/screens/mod.rs +++ b/src/model/screens/mod.rs @@ -1,29 +1,5 @@ -use crate::viewmodels::ViewModelState; - pub mod bookmarked; pub mod details_actions; pub mod edit_config; pub mod latest; pub mod mail_list; - -#[derive(Copy, Debug, Clone, PartialEq, Eq, Hash)] -pub enum View { - MailingListSelection, - BookmarkedPatchsets, - LatestPatchsets, - PatchsetDetails, - EditConfig, -} - -impl View { - #[allow(dead_code, unused_variables)] - pub fn draw_screen(&self, state: ViewModelState) { - match self { - View::MailingListSelection => todo!(), - View::BookmarkedPatchsets => todo!(), - View::LatestPatchsets => todo!(), - View::PatchsetDetails => todo!(), - View::EditConfig => todo!(), - } - } -} diff --git a/src/views/mod.rs b/src/views/mod.rs index d855d6ec..7221e2ce 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -15,8 +15,31 @@ use ratatui::{ Frame, }; -use crate::model::{screens::View, Model}; +use crate::model::Model; +use crate::viewmodels::ViewModelState; + +#[derive(Copy, Debug, Clone, PartialEq, Eq, Hash)] +pub enum View { + MailingListSelection, + BookmarkedPatchsets, + LatestPatchsets, + PatchsetDetails, + EditConfig, +} + +impl View { + #[allow(dead_code, unused_variables)] + pub fn draw_screen(&self, state: ViewModelState) { + match self { + View::MailingListSelection => todo!(), + View::BookmarkedPatchsets => todo!(), + View::LatestPatchsets => todo!(), + View::PatchsetDetails => todo!(), + View::EditConfig => todo!(), + } + } +} pub fn draw_ui(f: &mut Frame, model: &Model) { // Clear the whole screen for sanitizing reasons f.render_widget(Clear, f.area()); diff --git a/src/views/navigation_bar.rs b/src/views/navigation_bar.rs index d9b394ec..292def74 100644 --- a/src/views/navigation_bar.rs +++ b/src/views/navigation_bar.rs @@ -5,7 +5,7 @@ use ratatui::{ Frame, }; -use crate::model::{screens::View, Model}; +use crate::{model::Model, views::View}; use super::{bookmarked, details_actions, edit_config, latest, mail_list}; From 4449cf9af1a8903f7185769ffe6d42ffa3b8da9c Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sat, 9 Aug 2025 19:12:03 +0530 Subject: [PATCH 11/14] refactor(View): rename `MailingListSelection` to `MailingLists` The `View` variant `MailingListSelection` is slightly unconventional as other variants that include a similar selection list such as `LatestPatchsets` does not emphasize on the "Selection" aspect of the View. Rename the variant to better match with the others. Signed-off-by: Ivin Joel Abraham --- src/app.rs | 4 ++-- src/handler/bookmarked.rs | 2 +- src/handler/edit_config.rs | 2 +- src/handler/latest.rs | 2 +- src/handler/mod.rs | 6 +++--- src/model/mod.rs | 2 +- src/views/mod.rs | 6 +++--- src/views/navigation_bar.rs | 4 ++-- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/app.rs b/src/app.rs index 239cf9eb..7d65e626 100644 --- a/src/app.rs +++ b/src/app.rs @@ -18,7 +18,7 @@ impl App { pub fn new(model: Model) -> color_eyre::Result { Ok(App { model, - current_view: View::MailingListSelection, + current_view: View::MailingLists, viewmodels: HashMap::new(), }) } @@ -33,7 +33,7 @@ impl App { self.viewmodels .entry(self.current_view) .or_insert_with(|| match self.current_view { - View::MailingListSelection => { + View::MailingLists => { todo!() } View::BookmarkedPatchsets => { diff --git a/src/handler/bookmarked.rs b/src/handler/bookmarked.rs index cd283769..60a20026 100644 --- a/src/handler/bookmarked.rs +++ b/src/handler/bookmarked.rs @@ -31,7 +31,7 @@ where } KeyCode::Esc | KeyCode::Char('q') => { model.bookmarked_patchsets.patchset_index = 0; - model.set_current_screen(View::MailingListSelection); + model.set_current_screen(View::MailingLists); } KeyCode::Char('j') | KeyCode::Down => { model.bookmarked_patchsets.select_below_patchset(); diff --git a/src/handler/edit_config.rs b/src/handler/edit_config.rs index 6f7f1e4f..a10ec3f7 100644 --- a/src/handler/edit_config.rs +++ b/src/handler/edit_config.rs @@ -38,7 +38,7 @@ pub fn handle_edit_config(model: &mut Model, key: KeyEvent) -> color_eyre::Resul model.consolidate_edit_config(); model.config.save_patch_hub_config()?; model.reset_edit_config(); - model.set_current_screen(View::MailingListSelection); + model.set_current_screen(View::MailingLists); } KeyCode::Enter => { edit_config_state.toggle_editing(); diff --git a/src/handler/latest.rs b/src/handler/latest.rs index 39c6ce7f..9f09290d 100644 --- a/src/handler/latest.rs +++ b/src/handler/latest.rs @@ -33,7 +33,7 @@ where } KeyCode::Esc | KeyCode::Char('q') => { model.reset_latest_patchsets(); - model.set_current_screen(View::MailingListSelection); + model.set_current_screen(View::MailingLists); } KeyCode::Char('j') | KeyCode::Down => { latest_patchsets.select_below_patchset(); diff --git a/src/handler/mod.rs b/src/handler/mod.rs index 9b448c15..86b4e799 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -43,7 +43,7 @@ where } } else { match model.current_screen { - View::MailingListSelection => { + View::MailingLists => { return handle_mailing_list_selection(model, key, terminal); } View::BookmarkedPatchsets => { @@ -71,7 +71,7 @@ where B: Backend + Send + 'static, { match model.current_screen { - View::MailingListSelection => { + View::MailingLists => { if model.mailing_list_selection.mailing_lists.is_empty() { terminal = loading_screen! { terminal, "Fetching mailing lists" => { @@ -96,7 +96,7 @@ where } View::BookmarkedPatchsets => { if model.bookmarked_patchsets.bookmarked_patchsets.is_empty() { - model.set_current_screen(View::MailingListSelection); + model.set_current_screen(View::MailingLists); } } _ => {} diff --git a/src/model/mod.rs b/src/model/mod.rs index 2f4edc53..561c04f1 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -87,7 +87,7 @@ impl Model { garbage_collector::collect_garbage(&config); Ok(Model { - current_screen: View::MailingListSelection, + current_screen: View::MailingLists, mailing_list_selection: MailingListSelection { mailing_lists: mailing_lists.clone(), target_list: String::new(), diff --git a/src/views/mod.rs b/src/views/mod.rs index 7221e2ce..6020b473 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -21,7 +21,7 @@ use crate::viewmodels::ViewModelState; #[derive(Copy, Debug, Clone, PartialEq, Eq, Hash)] pub enum View { - MailingListSelection, + MailingLists, BookmarkedPatchsets, LatestPatchsets, PatchsetDetails, @@ -32,7 +32,7 @@ impl View { #[allow(dead_code, unused_variables)] pub fn draw_screen(&self, state: ViewModelState) { match self { - View::MailingListSelection => todo!(), + View::MailingLists => todo!(), View::BookmarkedPatchsets => todo!(), View::LatestPatchsets => todo!(), View::PatchsetDetails => todo!(), @@ -56,7 +56,7 @@ pub fn draw_ui(f: &mut Frame, model: &Model) { render_title(f, chunks[0]); match model.current_screen { - View::MailingListSelection => mail_list::render_main(f, model, chunks[1]), + View::MailingLists => mail_list::render_main(f, model, chunks[1]), View::BookmarkedPatchsets => { bookmarked::render_main(f, &model.bookmarked_patchsets, chunks[1]) } diff --git a/src/views/navigation_bar.rs b/src/views/navigation_bar.rs index 292def74..0fb99906 100644 --- a/src/views/navigation_bar.rs +++ b/src/views/navigation_bar.rs @@ -11,7 +11,7 @@ use super::{bookmarked, details_actions, edit_config, latest, mail_list}; pub fn render(f: &mut Frame, model: &Model, chunk: Rect) { let mode_footer_text = match model.current_screen { - View::MailingListSelection => mail_list::mode_footer_text(model), + View::MailingLists => mail_list::mode_footer_text(model), View::BookmarkedPatchsets => bookmarked::mode_footer_text(), View::LatestPatchsets => latest::mode_footer_text(model), View::PatchsetDetails => details_actions::mode_footer_text(), @@ -23,7 +23,7 @@ pub fn render(f: &mut Frame, model: &Model, chunk: Rect) { let current_keys_hint = { match model.current_screen { - View::MailingListSelection => mail_list::keys_hint(), + View::MailingLists => mail_list::keys_hint(), View::BookmarkedPatchsets => bookmarked::keys_hint(), View::LatestPatchsets => latest::keys_hint(), View::PatchsetDetails => details_actions::keys_hint(), From a133e95016ca2824d338c596c74cb51736e79065 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sat, 9 Aug 2025 19:43:30 +0530 Subject: [PATCH 12/14] refactor(View): pass `state` as reference to `draw_screen` `draw_screen` should only be reading the state when rendering and any changes should be done through the viewmodel i.e ownership should belong to the viewmodel. Signed-off-by: Ivin Joel Abraham --- src/app.rs | 2 +- src/views/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index 7d65e626..808be818 100644 --- a/src/app.rs +++ b/src/app.rs @@ -55,7 +55,7 @@ impl App { pub fn run(&mut self) -> Result<()> { loop { self.get_current_view() - .draw_screen(self.get_current_viewmodel().state()); + .draw_screen(&self.get_current_viewmodel().state()); if let Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Release { diff --git a/src/views/mod.rs b/src/views/mod.rs index 6020b473..d7711f09 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -30,7 +30,7 @@ pub enum View { impl View { #[allow(dead_code, unused_variables)] - pub fn draw_screen(&self, state: ViewModelState) { + pub fn draw_screen(&self, state: &ViewModelState) { match self { View::MailingLists => todo!(), View::BookmarkedPatchsets => todo!(), From b2e9f6cf5d612a5d28c024982765eab4d2080c03 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sat, 9 Aug 2025 22:09:49 +0530 Subject: [PATCH 13/14] feat(View): pass terminal reference to draw_screen The ratatui Terminal instance is ultimately required to draw on the screen. Hence, pass it to the View when required. Signed-off-by: Ivin Joel Abraham --- src/app.rs | 13 ++++++++----- src/views/mod.rs | 5 +++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/app.rs b/src/app.rs index 808be818..aa2ddfe3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,4 @@ -use crate::{viewmodels::ViewModel, views::View}; +use crate::{infrastructure::terminal::Tui, viewmodels::ViewModel, views::View}; use std::collections::HashMap; use color_eyre::eyre::Result; @@ -11,15 +11,17 @@ pub struct App { model: Model, current_view: View, viewmodels: HashMap>, + terminal: Tui, } impl App { #[allow(dead_code)] - pub fn new(model: Model) -> color_eyre::Result { + pub fn new(model: Model, terminal: Tui) -> color_eyre::Result { Ok(App { model, current_view: View::MailingLists, viewmodels: HashMap::new(), + terminal, }) } @@ -51,11 +53,12 @@ impl App { }) } - #[allow(dead_code, unreachable_code)] + #[allow(dead_code, unreachable_code, unused_variables)] pub fn run(&mut self) -> Result<()> { loop { - self.get_current_view() - .draw_screen(&self.get_current_viewmodel().state()); + let state = &self.get_current_viewmodel().state(); + let current_view = self.get_current_view(); + current_view.draw_screen(&mut self.terminal, state); if let Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Release { diff --git a/src/views/mod.rs b/src/views/mod.rs index d7711f09..a41aa68d 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -15,7 +15,7 @@ use ratatui::{ Frame, }; -use crate::model::Model; +use crate::{infrastructure::terminal::Tui, model::Model}; use crate::viewmodels::ViewModelState; @@ -30,7 +30,7 @@ pub enum View { impl View { #[allow(dead_code, unused_variables)] - pub fn draw_screen(&self, state: &ViewModelState) { + pub fn draw_screen(&self, terminal: &mut Tui, state: &ViewModelState) { match self { View::MailingLists => todo!(), View::BookmarkedPatchsets => todo!(), @@ -40,6 +40,7 @@ impl View { } } } + pub fn draw_ui(f: &mut Frame, model: &Model) { // Clear the whole screen for sanitizing reasons f.render_widget(Clear, f.area()); From a2ca127f6554bfe1612c8f8eb20eed58285ec1be Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Sun, 10 Aug 2025 23:44:47 +0530 Subject: [PATCH 14/14] feat(View): implement View for MailingLists Port the previous MVC architecture's View of MailingList to the new MVVM architecture. Keep previous implementations to ensure `patch-hub` does not break. This implementation is not currently used, it is being introduced ahead of main loop integration to avoid breaking compilation during the transition. Signed-off-by: Ivin Joel Abraham --- src/app.rs | 2 +- src/viewmodels/mailing_list_viewmodel.rs | 8 ++ src/viewmodels/mod.rs | 14 ++- src/views/mail_list.rs | 93 +++++++++++++++++- src/views/mod.rs | 120 +++++++++++++++++------ src/views/navigation_bar.rs | 2 +- src/views/widgets.rs | 10 ++ 7 files changed, 217 insertions(+), 32 deletions(-) create mode 100644 src/viewmodels/mailing_list_viewmodel.rs create mode 100644 src/views/widgets.rs diff --git a/src/app.rs b/src/app.rs index aa2ddfe3..5f67e08b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -58,7 +58,7 @@ impl App { loop { let state = &self.get_current_viewmodel().state(); let current_view = self.get_current_view(); - current_view.draw_screen(&mut self.terminal, state); + current_view.render_screen(&mut self.terminal, state)?; if let Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Release { diff --git a/src/viewmodels/mailing_list_viewmodel.rs b/src/viewmodels/mailing_list_viewmodel.rs new file mode 100644 index 00000000..84ba55c7 --- /dev/null +++ b/src/viewmodels/mailing_list_viewmodel.rs @@ -0,0 +1,8 @@ +use crate::lore::mailing_list::MailingList; + +pub struct MailingListsState { + pub highlighted_list_index: usize, + pub filtered_mailing_lists: Vec, + pub mailing_lists: Vec, + pub search_string: String, +} diff --git a/src/viewmodels/mod.rs b/src/viewmodels/mod.rs index 598f0a00..523dd0f2 100644 --- a/src/viewmodels/mod.rs +++ b/src/viewmodels/mod.rs @@ -1,4 +1,9 @@ +pub mod mailing_list_viewmodel; + use ratatui::crossterm::event::KeyEvent; + +use crate::viewmodels::mailing_list_viewmodel::MailingListsState; + #[allow(dead_code)] pub trait ViewModel { fn handle_key(&self, event: KeyEvent); @@ -6,4 +11,11 @@ pub trait ViewModel { fn state(&self) -> ViewModelState; } -pub enum ViewModelState {} +#[allow(dead_code)] +pub enum ViewModelState { + MailingLists(MailingListsState), + BookmarkedPatchsets, + LatestPatchsets, + PatchsetDetails, + EditConfig, +} diff --git a/src/views/mail_list.rs b/src/views/mail_list.rs index 91f4c5af..f7dde805 100644 --- a/src/views/mail_list.rs +++ b/src/views/mail_list.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; + use ratatui::{ layout::Rect, style::{Color, Modifier, Style}, @@ -6,8 +8,96 @@ use ratatui::{ Frame, }; -use crate::model::Model; +use crate::{ + model::Model, viewmodels::mailing_list_viewmodel::MailingListsState, + views::widgets::centered_text_widget, +}; + +pub fn render_navigation_bar(frame: &mut Frame, state: &MailingListsState, chunks: Rc<[Rect]>) { + let target_list_text = target_list_text(state); + let target_list_widget = centered_text_widget(target_list_text); + + let keys_hint_text = keys_hint_text(); + let keys_hint_widget = centered_text_widget(vec![keys_hint_text]); + + frame.render_widget(target_list_widget, chunks[0]); + frame.render_widget(keys_hint_widget, chunks[1]); +} + +pub fn render_main_content(f: &mut Frame, state: &MailingListsState, chunk: Rect) { + let highlighted_list_index = state.highlighted_list_index; + let mut list_items = Vec::::new(); + + for mailing_list in &state.filtered_mailing_lists { + list_items.push(ListItem::new( + Line::from(vec![ + Span::styled( + mailing_list.name().to_string(), + Style::default().fg(Color::Magenta), + ), + Span::styled( + format!(" - {}", mailing_list.description()), + Style::default().fg(Color::White), + ), + ]) + .centered(), + )) + } + + let list_block = Block::default() + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Double) + .style(Style::default()); + let list = List::new(list_items) + .block(list_block) + .highlight_style( + Style::default() + .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::REVERSED) + .fg(Color::Cyan), + ) + .highlight_symbol(">") + .highlight_spacing(HighlightSpacing::Always); + + let mut list_state = ListState::default(); + list_state.select(Some(highlighted_list_index)); + + f.render_stateful_widget(list, chunk, &mut list_state); +} + +fn target_list_text(state: &MailingListsState) -> Vec { + let mut text_area = Span::default(); + + if state.search_string.is_empty() { + text_area = Span::styled("type the target list", Style::default().fg(Color::DarkGray)) + } else { + for mailing_list in &state.mailing_lists { + if mailing_list.name().eq(&state.search_string) { + text_area = Span::styled(&state.search_string, Style::default().fg(Color::Green)); + break; + } else if mailing_list.name().starts_with(&state.search_string) { + text_area = + Span::styled(&state.search_string, Style::default().fg(Color::LightCyan)); + } + } + if text_area.content.is_empty() { + text_area = Span::styled(&state.search_string, Style::default().fg(Color::Red)); + } + } + + vec![ + Span::styled("Target List: ", Style::default().fg(Color::Green)), + text_area, + ] +} + +pub fn keys_hint_text() -> Span<'static> { + Span::styled( + "(ESC) to quit | (ENTER) to confirm | (?) help", + Style::default().fg(Color::Red), + ) +} pub fn render_main(f: &mut Frame, model: &Model, chunk: Rect) { let highlighted_list_index = model.mailing_list_selection.highlighted_list_index; let mut list_items = Vec::::new(); @@ -90,6 +180,7 @@ pub fn mode_footer_text(model: &Model) -> Vec { ] } +#[allow(dead_code)] pub fn keys_hint() -> Span<'static> { Span::styled( "(ESC) to quit | (ENTER) to confirm | (?) help", diff --git a/src/views/mod.rs b/src/views/mod.rs index a41aa68d..6a0ba1b5 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -6,6 +6,9 @@ pub mod loading_screen; mod mail_list; mod navigation_bar; pub mod popup; +mod widgets; + +use std::rc::Rc; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, @@ -30,14 +33,94 @@ pub enum View { impl View { #[allow(dead_code, unused_variables)] - pub fn draw_screen(&self, terminal: &mut Tui, state: &ViewModelState) { - match self { - View::MailingLists => todo!(), - View::BookmarkedPatchsets => todo!(), - View::LatestPatchsets => todo!(), - View::PatchsetDetails => todo!(), - View::EditConfig => todo!(), - } + pub fn render_screen( + &self, + terminal: &mut Tui, + state: &ViewModelState, + ) -> color_eyre::Result<()> { + terminal.draw(|frame| { + View::clear_frame(frame); + + let main_layout = View::main_layout(); + let main_layout_chunks = View::chunk_layout(&main_layout, frame); + + View::render_title_on_chunk(frame, main_layout_chunks[0]); + self.render_main_content(frame, state, main_layout_chunks[1]); + + let footer_layout = View::footer_layout(); + let footer_layout_chunks = View::chunk_layout(&footer_layout, frame); + self.render_navigation_bar(frame, state, footer_layout_chunks); + })?; + + Ok(()) + } + + fn render_navigation_bar(&self, frame: &mut Frame, state: &ViewModelState, chunk: Rc<[Rect]>) { + match (self, state) { + (View::MailingLists, ViewModelState::MailingLists(s)) => { + mail_list::render_navigation_bar(frame, s, chunk) + } + (View::BookmarkedPatchsets, _) => todo!(), + (View::LatestPatchsets, _) => todo!(), + (View::PatchsetDetails, _) => todo!(), + (View::EditConfig, _) => todo!(), + _ => todo!(), + }; + } + + fn render_main_content(&self, frame: &mut Frame, state: &ViewModelState, chunk: Rect) { + match (self, state) { + (View::MailingLists, ViewModelState::MailingLists(s)) => { + mail_list::render_main_content(frame, s, chunk) + } + (View::BookmarkedPatchsets, _) => todo!(), + (View::LatestPatchsets, _) => todo!(), + (View::PatchsetDetails, _) => todo!(), + (View::EditConfig, _) => todo!(), + _ => todo!(), + }; + } + + fn render_title_on_chunk(frame: &mut Frame, chunk: Rect) { + let title_block = Block::default() + .borders(Borders::ALL) + .style(Style::default()) + .title_alignment(Alignment::Center); + + let title_content: String = "patch-hub".to_string(); + + let title = Paragraph::new(Text::styled( + title_content, + Style::default().fg(Color::Green).bold(), + )) + .centered() + .block(title_block); + + frame.render_widget(title, chunk); + } + + fn clear_frame(frame: &mut Frame) { + frame.render_widget(Clear, frame.area()); + } + + fn main_layout() -> Layout { + Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Min(1), + Constraint::Length(3), + ]) + } + + fn footer_layout() -> Layout { + Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(30), Constraint::Percentage(80)]) + } + + fn chunk_layout(layout: &Layout, frame: &mut Frame) -> Rc<[Rect]> { + layout.split(frame.area()) } } @@ -54,8 +137,7 @@ pub fn draw_ui(f: &mut Frame, model: &Model) { ]) .split(f.area()); - render_title(f, chunks[0]); - + View::render_title_on_chunk(f, chunks[0]); match model.current_screen { View::MailingLists => mail_list::render_main(f, model, chunks[1]), View::BookmarkedPatchsets => { @@ -75,24 +157,6 @@ pub fn draw_ui(f: &mut Frame, model: &Model) { }); } -fn render_title(f: &mut Frame, chunk: Rect) { - let title_block = Block::default() - .borders(Borders::ALL) - .style(Style::default()) - .title_alignment(Alignment::Center); - - let title_content: String = "patch-hub".to_string(); - - let title = Paragraph::new(Text::styled( - title_content, - Style::default().fg(Color::Green).bold(), - )) - .centered() - .block(title_block); - - f.render_widget(title, chunk); -} - /// helper function to create a centered rect using up certain percentage of the available rect `r` fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { // Cut the given rectangle into three vertical pieces diff --git a/src/views/navigation_bar.rs b/src/views/navigation_bar.rs index 0fb99906..e94c9db6 100644 --- a/src/views/navigation_bar.rs +++ b/src/views/navigation_bar.rs @@ -23,7 +23,7 @@ pub fn render(f: &mut Frame, model: &Model, chunk: Rect) { let current_keys_hint = { match model.current_screen { - View::MailingLists => mail_list::keys_hint(), + View::MailingLists => mail_list::keys_hint_text(), View::BookmarkedPatchsets => bookmarked::keys_hint(), View::LatestPatchsets => latest::keys_hint(), View::PatchsetDetails => details_actions::keys_hint(), diff --git a/src/views/widgets.rs b/src/views/widgets.rs new file mode 100644 index 00000000..deb124d1 --- /dev/null +++ b/src/views/widgets.rs @@ -0,0 +1,10 @@ +use ratatui::{ + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, +}; + +pub fn centered_text_widget(text: Vec) -> Paragraph<'_> { + Paragraph::new(Line::from(text)) + .block(Block::default().borders(Borders::ALL)) + .centered() +}