From db0b1085b5a1c5b138fb1b309bd9fc187fe6b541 Mon Sep 17 00:00:00 2001 From: ValDesign Date: Tue, 20 Jan 2026 20:03:33 +0100 Subject: [PATCH 1/7] refactor(tui): renamed databases screen to backups --- src/ui/app.rs | 10 +++++----- src/ui/screens/{databases.rs => backups.rs} | 6 +++--- src/ui/screens/home.rs | 6 +++--- src/ui/screens/mod.rs | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) rename src/ui/screens/{databases.rs => backups.rs} (63%) diff --git a/src/ui/app.rs b/src/ui/app.rs index 2ba9357..a629f6a 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -13,12 +13,12 @@ use ratatui::{ use crate::{ db::DatabaseConnection, - ui::screens::{DatabasesScreen, HomeItem, HomeScreen, SettingsScreen}, + ui::screens::{BackupsScreen, HomeItem, HomeScreen, SettingsScreen}, }; pub enum CurrentScreen { Main, - Databases, + Backups, Settings, } @@ -56,8 +56,8 @@ impl App { eprintln!("Draw error: {}", e); } } - CurrentScreen::Databases => { - if let Err(e) = DatabasesScreen::draw(self, frame) { + CurrentScreen::Backups => { + if let Err(e) = BackupsScreen::draw(self, frame) { eprintln!("Draw error: {}", e); } } @@ -101,7 +101,7 @@ impl App { let items = HomeScreen::list_items(); if let Some(idx) = self.list_state.selected() { match items[idx] { - HomeItem::Databases => self.set_screen(CurrentScreen::Databases), + HomeItem::Backups => self.set_screen(CurrentScreen::Backups), HomeItem::Settings => self.set_screen(CurrentScreen::Settings), HomeItem::Exit => self.should_quit = true, } diff --git a/src/ui/screens/databases.rs b/src/ui/screens/backups.rs similarity index 63% rename from src/ui/screens/databases.rs rename to src/ui/screens/backups.rs index 7668f35..8a2172a 100644 --- a/src/ui/screens/databases.rs +++ b/src/ui/screens/backups.rs @@ -4,11 +4,11 @@ use ratatui::Frame; use crate::ui::{app::App, screens::ScreenLayout}; -pub struct DatabasesScreen; +pub struct BackupsScreen; -impl DatabasesScreen { +impl BackupsScreen { pub fn draw(app: &mut App, frame: &mut Frame) -> Result<()> { - ScreenLayout::draw(frame, Some("Databases")); + ScreenLayout::draw(frame, Some("Backups")); Ok(()) } diff --git a/src/ui/screens/home.rs b/src/ui/screens/home.rs index f19e5e6..415ab1a 100644 --- a/src/ui/screens/home.rs +++ b/src/ui/screens/home.rs @@ -14,7 +14,7 @@ use crate::ui::{ }; pub enum HomeItem { - Databases, + Backups, Settings, Exit, } @@ -22,7 +22,7 @@ pub enum HomeItem { impl From<&HomeItem> for ListItem<'_> { fn from(value: &HomeItem) -> Self { let line = Line::from(match value { - HomeItem::Databases => "Databases", + HomeItem::Backups => "Backups", HomeItem::Settings => "Settings", HomeItem::Exit => "Exit", }) @@ -56,7 +56,7 @@ impl HomeScreen { } pub fn list_items() -> &'static [HomeItem] { - static ITEMS: [HomeItem; 3] = [HomeItem::Databases, HomeItem::Settings, HomeItem::Exit]; + static ITEMS: [HomeItem; 3] = [HomeItem::Backups, HomeItem::Settings, HomeItem::Exit]; &ITEMS } } diff --git a/src/ui/screens/mod.rs b/src/ui/screens/mod.rs index c4e15fe..6b5070c 100644 --- a/src/ui/screens/mod.rs +++ b/src/ui/screens/mod.rs @@ -1,5 +1,5 @@ -mod databases; -pub use databases::DatabasesScreen; +mod backups; +pub use backups::BackupsScreen; mod home; pub use home::{HomeItem, HomeScreen}; mod layout; From f1d6539d9d6483d933e8cd810152e63973400114 Mon Sep 17 00:00:00 2001 From: ValDesign Date: Tue, 20 Jan 2026 20:29:01 +0100 Subject: [PATCH 2/7] feat(tui): started working on the backups TUI layout --- src/main.rs | 2 +- src/ui/app.rs | 5 ++++- src/ui/screens/backups.rs | 28 +++++++++++++++++++++++++++- src/utils/config.rs | 30 +++++++++++++++--------------- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/main.rs b/src/main.rs index 93c4a65..941c617 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,7 +22,7 @@ async fn main() -> Result<(), Box> { let config = Config::new(); match cli.command { - None | Some(Commands::Tui) => App::new().run().await?, + None | Some(Commands::Tui) => App::new(config).run().await?, }; Ok(()) diff --git a/src/ui/app.rs b/src/ui/app.rs index a629f6a..61c88e9 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -14,6 +14,7 @@ use ratatui::{ use crate::{ db::DatabaseConnection, ui::screens::{BackupsScreen, HomeItem, HomeScreen, SettingsScreen}, + utils::config::Config, }; pub enum CurrentScreen { @@ -26,17 +27,19 @@ pub struct App { should_quit: bool, current_screen: CurrentScreen, pub list_state: ListState, + pub config: Config, pub database_connection: DatabaseConnection, } impl App { - pub fn new() -> Self { + pub fn new(config: Config) -> Self { let mut list_state = ListState::default(); list_state.select_first(); Self { should_quit: false, current_screen: CurrentScreen::Main, list_state, + config, database_connection: DatabaseConnection::new(), } } diff --git a/src/ui/screens/backups.rs b/src/ui/screens/backups.rs index 8a2172a..377624a 100644 --- a/src/ui/screens/backups.rs +++ b/src/ui/screens/backups.rs @@ -1,6 +1,10 @@ use std::io::Result; -use ratatui::Frame; +use ratatui::{ + Frame, + layout::{Constraint, Direction, Layout}, + widgets::{Block, Borders, Paragraph}, +}; use crate::ui::{app::App, screens::ScreenLayout}; @@ -10,6 +14,28 @@ impl BackupsScreen { pub fn draw(app: &mut App, frame: &mut Frame) -> Result<()> { ScreenLayout::draw(frame, Some("Backups")); + let backups = &app.config.backups; + let backup_names = backups + .iter() + .map(|(_, v)| &v.display_name) + .collect::>() + .as_slice(); + + let area = frame.area(); + + let layout = Layout::default() + .direction(Direction::Horizontal) + .constraints(vec![Constraint::Percentage(25), Constraint::Percentage(75)]) + .split(area.centered( + Constraint::Length(area.width - 2), + Constraint::Length(area.height - 2), + )); + + frame.render_widget( + Paragraph::new("outer 0").block(Block::new().borders(Borders::ALL)), + layout[0], + ); + Ok(()) } } diff --git a/src/utils/config.rs b/src/utils/config.rs index ce15791..4d97cd0 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -1,31 +1,31 @@ use std::{collections::HashMap, env, fs::File, io::Read, path::Path}; #[derive(Debug)] -enum BackupDatastoreType { +pub enum BackupDatastoreType { FileSystem, S3, } #[derive(Debug)] -struct BackupDatastore { - storage_type: BackupDatastoreType, - path: String, +pub struct BackupDatastore { + pub storage_type: BackupDatastoreType, + pub path: String, } #[derive(Debug)] -struct BackupSchedule { - enabled: bool, - cron: String, +pub struct BackupSchedule { + pub enabled: bool, + pub cron: String, } #[derive(Debug)] -struct Backup { - display_name: String, - connection_string: String, - ignore_collections: Vec, - datastore: BackupDatastore, - schedule: BackupSchedule, - encryption_key: Option, +pub struct Backup { + pub display_name: String, + pub connection_string: String, + pub ignore_collections: Vec, + pub datastore: BackupDatastore, + pub schedule: BackupSchedule, + pub encryption_key: Option, } #[derive(Debug)] @@ -142,7 +142,7 @@ enum Frame { #[derive(Debug)] pub struct Config { - backups: HashMap, + pub backups: HashMap, } impl Config { From ea991172116197d39c4c6b338b88cc47f036f907 Mon Sep 17 00:00:00 2001 From: ValDesign Date: Wed, 21 Jan 2026 14:22:22 +0100 Subject: [PATCH 3/7] feat(tui): started to implement backups managaement --- src/ui/app.rs | 14 +++++++++++--- src/ui/screens/backups.rs | 23 ++++++++++++++--------- src/ui/screens/home.rs | 2 +- src/ui/screens/layout.rs | 15 ++++++++++++--- src/ui/screens/settings.rs | 2 +- 5 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index 61c88e9..8518e8a 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -17,6 +17,7 @@ use crate::{ utils::config::Config, }; +#[derive(PartialEq)] pub enum CurrentScreen { Main, Backups, @@ -25,7 +26,7 @@ pub enum CurrentScreen { pub struct App { should_quit: bool, - current_screen: CurrentScreen, + pub current_screen: CurrentScreen, pub list_state: ListState, pub config: Config, pub database_connection: DatabaseConnection, @@ -98,8 +99,12 @@ impl App { } match (&self.current_screen, key.code) { - (CurrentScreen::Main, KeyCode::Down) => self.list_state.select_next(), - (CurrentScreen::Main, KeyCode::Up) => self.list_state.select_previous(), + (CurrentScreen::Main | CurrentScreen::Backups, KeyCode::Down) => { + self.list_state.select_next() + } + (CurrentScreen::Main | CurrentScreen::Backups, KeyCode::Up) => { + self.list_state.select_previous() + } (CurrentScreen::Main, KeyCode::Enter) => { let items = HomeScreen::list_items(); if let Some(idx) = self.list_state.selected() { @@ -110,6 +115,9 @@ impl App { } } } + (CurrentScreen::Backups | CurrentScreen::Settings, KeyCode::Backspace) => { + self.set_screen(CurrentScreen::Main) + } (_, KeyCode::Char('q') | KeyCode::Esc) => self.should_quit = true, _ => {} } diff --git a/src/ui/screens/backups.rs b/src/ui/screens/backups.rs index 377624a..ab3f946 100644 --- a/src/ui/screens/backups.rs +++ b/src/ui/screens/backups.rs @@ -3,7 +3,8 @@ use std::io::Result; use ratatui::{ Frame, layout::{Constraint, Direction, Layout}, - widgets::{Block, Borders, Paragraph}, + style::Style, + widgets::{Block, BorderType, List, ListDirection}, }; use crate::ui::{app::App, screens::ScreenLayout}; @@ -12,14 +13,13 @@ pub struct BackupsScreen; impl BackupsScreen { pub fn draw(app: &mut App, frame: &mut Frame) -> Result<()> { - ScreenLayout::draw(frame, Some("Backups")); + ScreenLayout::draw(app, frame, Some("Backups")); let backups = &app.config.backups; let backup_names = backups .iter() - .map(|(_, v)| &v.display_name) - .collect::>() - .as_slice(); + .map(|(_, v)| v.display_name.clone()) + .collect::>(); let area = frame.area(); @@ -31,10 +31,15 @@ impl BackupsScreen { Constraint::Length(area.height - 2), )); - frame.render_widget( - Paragraph::new("outer 0").block(Block::new().borders(Borders::ALL)), - layout[0], - ); + let list_block = Block::bordered().border_type(BorderType::Rounded); + let list = List::new(backup_names) + .block(list_block) + .highlight_style(Style::new().reversed()) + .highlight_symbol("▶") + .repeat_highlight_symbol(true) + .direction(ListDirection::TopToBottom); + + frame.render_stateful_widget(list, layout[0], &mut app.list_state); Ok(()) } diff --git a/src/ui/screens/home.rs b/src/ui/screens/home.rs index 415ab1a..a93add4 100644 --- a/src/ui/screens/home.rs +++ b/src/ui/screens/home.rs @@ -35,7 +35,7 @@ pub struct HomeScreen; impl HomeScreen { pub fn draw(app: &mut App, frame: &mut Frame) -> Result<()> { - ScreenLayout::draw(frame, None); + ScreenLayout::draw(app, frame, None); let area = frame.area(); diff --git a/src/ui/screens/layout.rs b/src/ui/screens/layout.rs index 912ee8a..ee5da17 100644 --- a/src/ui/screens/layout.rs +++ b/src/ui/screens/layout.rs @@ -6,11 +6,14 @@ use ratatui::{ widgets::{Block, BorderType}, }; +use crate::ui::app::{App, CurrentScreen}; + pub struct ScreenLayout; impl ScreenLayout { - pub fn draw(frame: &mut Frame, title: Option<&str>) { + pub fn draw(app: &mut App, frame: &mut Frame, title: Option<&str>) { let area = frame.area(); + let display_title = format!( "MongoDB Backup Manager{}", title.map_or("".to_string(), |t| format!(" - {}", t)) @@ -19,9 +22,15 @@ impl ScreenLayout { .border_type(BorderType::Rounded) .title(display_title.bold()) .title_alignment(HorizontalAlignment::Left); - let quit_action = Block::new().title_bottom(Line::from("Esc or q to exit").centered()); + let hint_text = format!( + "Esc or q to exit{}", + (app.current_screen != CurrentScreen::Main) + .then(|| ", Backspace to go back") + .unwrap_or("") + ); + let action_hint = Block::new().title_bottom(Line::from(hint_text).centered()); frame.render_widget(app_title, area); - frame.render_widget(quit_action, area); + frame.render_widget(action_hint, area); } } diff --git a/src/ui/screens/settings.rs b/src/ui/screens/settings.rs index e238ff2..1d25972 100644 --- a/src/ui/screens/settings.rs +++ b/src/ui/screens/settings.rs @@ -8,7 +8,7 @@ pub struct SettingsScreen; impl SettingsScreen { pub fn draw(app: &mut App, frame: &mut Frame) -> Result<()> { - ScreenLayout::draw(frame, Some("Settings")); + ScreenLayout::draw(app, frame, Some("Settings")); Ok(()) } From a468bef2563f5c25487228724f887b87f5001961 Mon Sep 17 00:00:00 2001 From: Nolhan Date: Thu, 26 Feb 2026 11:19:56 +0100 Subject: [PATCH 4/7] feat(datastores): added `.get_base_path()` to `Datastore` --- src/cli/commands/backup/inspect.rs | 10 ++-------- src/datastores/mod.rs | 7 +++++++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/cli/commands/backup/inspect.rs b/src/cli/commands/backup/inspect.rs index 5b0035e..2594169 100644 --- a/src/cli/commands/backup/inspect.rs +++ b/src/cli/commands/backup/inspect.rs @@ -51,16 +51,10 @@ pub fn inspect(name: String) { .collect::>(), }; - let datastore_type = backup_job.datastore.as_str(); - let datastore_base_path = match &backup_job.datastore { - Datastore::FileSystem(store) => store.base_path.display().to_string(), - Datastore::S3(store) => store.base_path.display().to_string(), - }; - println!("-- {} --", backup_job.display_name); println!("Datastore:"); - println!("\tType: {}", datastore_type); - println!("\tPath: {}", datastore_base_path); + println!("\tType: {}", backup_job.datastore.as_str()); + println!("\tPath: {}", backup_job.datastore.get_base_path()); println!( "Schedule: {:?}", if let Some(schedule) = backup_job.clone().raw_schedule { diff --git a/src/datastores/mod.rs b/src/datastores/mod.rs index 0fe045c..1a4d811 100644 --- a/src/datastores/mod.rs +++ b/src/datastores/mod.rs @@ -58,6 +58,13 @@ impl Datastore { } } + pub fn get_base_path(&self) -> String { + match self { + Datastore::FileSystem(store) => store.base_path.display().to_string(), + Datastore::S3(store) => store.base_path.display().to_string(), + } + } + pub fn check_backup_integrity(&self) -> Result { delegate_to_datastore!(self, check_backup_integrity()) } From 2cef4d25580b9d8b5e5bdbcd80654222b33eb100 Mon Sep 17 00:00:00 2001 From: Nolhan Date: Thu, 26 Feb 2026 12:16:09 +0100 Subject: [PATCH 5/7] feat(tui): added BackupInspect screen --- src/ui/app.rs | 25 +++++++++- src/ui/screens/backup_inspect.rs | 79 ++++++++++++++++++++++++++++++++ src/ui/screens/backups.rs | 12 ++++- src/ui/screens/layout.rs | 8 ++-- src/ui/screens/mod.rs | 3 ++ 5 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 src/ui/screens/backup_inspect.rs diff --git a/src/ui/app.rs b/src/ui/app.rs index 4b4482f..79f379e 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -11,6 +11,9 @@ use ratatui::{ widgets::ListState, }; +use crate::ui::app::CurrentScreen::BackupInspect; +use crate::ui::screens::BackupInspectScreen; +use crate::utils::backup_manager::BackupJob; use crate::{ db::DatabaseConnection, ui::screens::{BackupsScreen, HomeItem, HomeScreen, SettingsScreen}, @@ -21,6 +24,7 @@ use crate::{ pub enum CurrentScreen { Main, Backups, + BackupInspect { backup: BackupJob }, Settings, } @@ -34,7 +38,8 @@ pub struct App { } impl App { - pub fn new(config: Config) -> Self { + pub fn new() -> Self { + let config = Config::new(); let mut list_state = ListState::default(); list_state.select_first(); Self { @@ -66,6 +71,11 @@ impl App { eprintln!("Draw error: {}", e); } } + CurrentScreen::BackupInspect { ref backup } => { + if let Err(e) = BackupInspectScreen::draw(self, backup.clone(), frame) { + eprintln!("Draw error: {}", e) + } + } CurrentScreen::Settings => { if let Err(e) = SettingsScreen::draw(self, frame) { eprintln!("Draw error: {}", e); @@ -116,6 +126,19 @@ impl App { } } } + (CurrentScreen::Backups, KeyCode::Enter) => { + let items = BackupsScreen::list_items(self); + if let Some(idx) = self.list_state.selected() { + let backup = self + .config + .backups + .get(&format!("backup.{}", items[idx])) + .expect("Backup not found"); + self.set_screen(CurrentScreen::BackupInspect { + backup: backup.clone(), + }) + } + } (CurrentScreen::Backups | CurrentScreen::Settings, KeyCode::Backspace) => { self.set_screen(CurrentScreen::Main) } diff --git a/src/ui/screens/backup_inspect.rs b/src/ui/screens/backup_inspect.rs new file mode 100644 index 0000000..fd0f28f --- /dev/null +++ b/src/ui/screens/backup_inspect.rs @@ -0,0 +1,79 @@ +use std::io::Result; + +use crate::datastores::Datastore; +use crate::ui::{app::App, screens::ScreenLayout}; +use crate::utils::backup_manager::BackupJob; +use ratatui::prelude::{Style, Stylize}; +use ratatui::text::Line; +use ratatui::widgets::{Borders, List, ListDirection, Paragraph, Wrap}; +use ratatui::{ + Frame, + widgets::{Block, BorderType}, +}; + +pub struct BackupInspectScreen; + +impl BackupInspectScreen { + pub fn draw(app: &mut App, backup: BackupJob, frame: &mut Frame) -> Result<()> { + ScreenLayout::draw(app, frame, Some(&backup.display_name)); + + let area = frame.area(); + + let text = format!( + "Datastore:\n\tType: {:?}\n\tPath: {}\n\tSchedule: {:?}\n{:?}Encryption: {}", + backup.datastore.as_str(), + backup.datastore.get_base_path(), + if let Some(schedule) = backup.clone().raw_schedule { + schedule + } else { + "disabled".to_string() + }, + if let Some(next) = backup.get_next_run() { + format!("Next run: {:?}", next) + } else { + String::new() + }, + if backup.is_encryption_enabled() { + "enabled" + } else { + "disabled" + } + ); + let mut lines = vec![ + Line::raw("Datastore:"), + Line::raw(format!(" Type: {}", backup.datastore.as_str())), + Line::raw(format!(" Path: {}", backup.datastore.get_base_path())), + Line::raw(format!( + "Schedule: {}", + if let Some(schedule) = backup.clone().raw_schedule { + schedule + } else { + "disabled".to_string() + } + )), + Line::raw(format!( + "Encryption: {}", + if backup.is_encryption_enabled() { + "enabled" + } else { + "disabled" + } + )), + ]; + + if let Some(next) = backup.clone().get_next_run() { + lines.insert(4, Line::raw(format!("Next run: {:?}", next))) + } + + let paragraph = Paragraph::new(lines).block( + Block::new() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title(backup.display_name), + ); + + frame.render_widget(paragraph, area); + + Ok(()) + } +} diff --git a/src/ui/screens/backups.rs b/src/ui/screens/backups.rs index ab3f946..6bc1360 100644 --- a/src/ui/screens/backups.rs +++ b/src/ui/screens/backups.rs @@ -17,8 +17,8 @@ impl BackupsScreen { let backups = &app.config.backups; let backup_names = backups - .iter() - .map(|(_, v)| v.display_name.clone()) + .values() + .map(|v| v.display_name.clone()) .collect::>(); let area = frame.area(); @@ -43,4 +43,12 @@ impl BackupsScreen { Ok(()) } + + pub fn list_items(app: &mut App) -> Vec { + let backups = &app.config.backups; + backups + .values() + .map(|v| v.identifier.clone()) + .collect::>() + } } diff --git a/src/ui/screens/layout.rs b/src/ui/screens/layout.rs index ee5da17..a4cb185 100644 --- a/src/ui/screens/layout.rs +++ b/src/ui/screens/layout.rs @@ -24,9 +24,11 @@ impl ScreenLayout { .title_alignment(HorizontalAlignment::Left); let hint_text = format!( "Esc or q to exit{}", - (app.current_screen != CurrentScreen::Main) - .then(|| ", Backspace to go back") - .unwrap_or("") + if app.current_screen != CurrentScreen::Main { + ", Backspace to go back" + } else { + "" + } ); let action_hint = Block::new().title_bottom(Line::from(hint_text).centered()); diff --git a/src/ui/screens/mod.rs b/src/ui/screens/mod.rs index 6b5070c..726ed57 100644 --- a/src/ui/screens/mod.rs +++ b/src/ui/screens/mod.rs @@ -4,7 +4,10 @@ mod home; pub use home::{HomeItem, HomeScreen}; mod layout; pub use layout::ScreenLayout; +mod backup_inspect; +pub use backup_inspect::BackupInspectScreen; mod settings; + pub use settings::SettingsScreen; use ratatui::{ From 220edabdcdbcb46fe9029806195e2dd2f8fc758d Mon Sep 17 00:00:00 2001 From: Nolhan Date: Thu, 5 Mar 2026 11:36:50 +0100 Subject: [PATCH 6/7] feat(tui): added BackupInspect screen --- src/cli/commands/backup/inspect.rs | 32 +------------ src/datastores/mod.rs | 36 ++++++++++++++ src/ui/app.rs | 19 ++++---- src/ui/screens/backup_inspect.rs | 76 ++++++++++++++++++++---------- 4 files changed, 99 insertions(+), 64 deletions(-) diff --git a/src/cli/commands/backup/inspect.rs b/src/cli/commands/backup/inspect.rs index 2594169..ceb68bf 100644 --- a/src/cli/commands/backup/inspect.rs +++ b/src/cli/commands/backup/inspect.rs @@ -19,37 +19,7 @@ pub fn inspect(name: String) { }; let backup_dir_prefix = format!("backup_{name}_"); - let backups = match &backup_job.datastore { - Datastore::FileSystem(store) => store - .list_backups() - .unwrap_or_default() - .into_iter() - .filter_map(|store| { - let dir_name = store.base_path.file_name()?.to_str()?.to_string(); - - if dir_name.starts_with(&backup_dir_prefix) { - Some((dir_name, Datastore::FileSystem(store))) - } else { - None - } - }) - .collect::>(), - - Datastore::S3(store) => store - .list_backups() - .unwrap_or_default() - .into_iter() - .filter_map(|store| { - let dir_name = store.base_path.file_name()?.to_str()?.to_string(); - - if dir_name.starts_with(&backup_dir_prefix) { - Some((dir_name, Datastore::S3(store))) - } else { - None - } - }) - .collect::>(), - }; + let backups = backup_job.datastore.get_backups(&name); println!("-- {} --", backup_job.display_name); println!("Datastore:"); diff --git a/src/datastores/mod.rs b/src/datastores/mod.rs index 1a4d811..eeb9ac7 100644 --- a/src/datastores/mod.rs +++ b/src/datastores/mod.rs @@ -108,4 +108,40 @@ impl Datastore { Datastore::S3(store) => store.open_read_stream(object_name).await, } } + + pub fn get_backups(&self, backup_identifier: &str) -> Vec<(String, Datastore)> { + let backup_dir_prefix = format!("backup_{backup_identifier}_"); + + match self { + Datastore::FileSystem(store) => store + .list_backups() + .unwrap_or_default() + .into_iter() + .filter_map(|store| { + let dir_name = store.base_path.file_name()?.to_str()?.to_string(); + + if dir_name.starts_with(&backup_dir_prefix) { + Some((dir_name, Datastore::FileSystem(store))) + } else { + None + } + }) + .collect::>(), + + Datastore::S3(store) => store + .list_backups() + .unwrap_or_default() + .into_iter() + .filter_map(|store| { + let dir_name = store.base_path.file_name()?.to_str()?.to_string(); + + if dir_name.starts_with(&backup_dir_prefix) { + Some((dir_name, Datastore::S3(store))) + } else { + None + } + }) + .collect::>(), + } + } } diff --git a/src/ui/app.rs b/src/ui/app.rs index 79f379e..4a9c22e 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1,5 +1,4 @@ -use std::io; - +use chrono::DateTime; use ratatui::{ Terminal, crossterm::{ @@ -10,7 +9,9 @@ use ratatui::{ prelude::CrosstermBackend, widgets::ListState, }; +use std::io; +use crate::datastores::Datastore; use crate::ui::app::CurrentScreen::BackupInspect; use crate::ui::screens::BackupInspectScreen; use crate::utils::backup_manager::BackupJob; @@ -110,12 +111,14 @@ impl App { } match (&self.current_screen, key.code) { - (CurrentScreen::Main | CurrentScreen::Backups, KeyCode::Down) => { - self.list_state.select_next() - } - (CurrentScreen::Main | CurrentScreen::Backups, KeyCode::Up) => { - self.list_state.select_previous() - } + ( + CurrentScreen::Main | CurrentScreen::Backups | CurrentScreen::BackupInspect { backup: _ }, + KeyCode::Down, + ) => self.list_state.select_next(), + ( + CurrentScreen::Main | CurrentScreen::Backups | CurrentScreen::BackupInspect { backup: _ }, + KeyCode::Up, + ) => self.list_state.select_previous(), (CurrentScreen::Main, KeyCode::Enter) => { let items = HomeScreen::list_items(); if let Some(idx) = self.list_state.selected() { diff --git a/src/ui/screens/backup_inspect.rs b/src/ui/screens/backup_inspect.rs index fd0f28f..c223696 100644 --- a/src/ui/screens/backup_inspect.rs +++ b/src/ui/screens/backup_inspect.rs @@ -1,8 +1,8 @@ -use std::io::Result; - use crate::datastores::Datastore; use crate::ui::{app::App, screens::ScreenLayout}; use crate::utils::backup_manager::BackupJob; +use chrono::DateTime; +use ratatui::layout::{Constraint, Direction, Layout}; use ratatui::prelude::{Style, Stylize}; use ratatui::text::Line; use ratatui::widgets::{Borders, List, ListDirection, Paragraph, Wrap}; @@ -10,6 +10,7 @@ use ratatui::{ Frame, widgets::{Block, BorderType}, }; +use std::io::Result; pub struct BackupInspectScreen; @@ -18,27 +19,18 @@ impl BackupInspectScreen { ScreenLayout::draw(app, frame, Some(&backup.display_name)); let area = frame.area(); + let layout = Layout::default() + .direction(Direction::Horizontal) + .constraints(vec![ + Constraint::Min(25), + Constraint::Length(1), + Constraint::Fill(1), + ]) + .split(area.centered( + Constraint::Length(area.width - 2), + Constraint::Length(area.height - 2), + )); - let text = format!( - "Datastore:\n\tType: {:?}\n\tPath: {}\n\tSchedule: {:?}\n{:?}Encryption: {}", - backup.datastore.as_str(), - backup.datastore.get_base_path(), - if let Some(schedule) = backup.clone().raw_schedule { - schedule - } else { - "disabled".to_string() - }, - if let Some(next) = backup.get_next_run() { - format!("Next run: {:?}", next) - } else { - String::new() - }, - if backup.is_encryption_enabled() { - "enabled" - } else { - "disabled" - } - ); let mut lines = vec![ Line::raw("Datastore:"), Line::raw(format!(" Type: {}", backup.datastore.as_str())), @@ -68,11 +60,45 @@ impl BackupInspectScreen { let paragraph = Paragraph::new(lines).block( Block::new() .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .title(backup.display_name), + .border_type(BorderType::Rounded), ); - frame.render_widget(paragraph, area); + frame.render_widget(paragraph, layout[0]); + + let mut backups_labels = backup + .datastore + .get_backups(&backup.identifier) + .iter() + .map(|(dir_name, datastore)| { + let health_state = datastore.check_backup_integrity().is_ok_and(|res| res); + let timestamp = dir_name + .rsplit("_") + .next() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + let date = match DateTime::from_timestamp_secs(timestamp) { + Some(value) => value.to_rfc3339(), + None => "Unknown date".to_string(), + }; + format!( + "\t{} - {} - {}", + dir_name, + date, + if health_state { "Healthy" } else { "Unhealthy" } + ) + }) + .collect::>(); + backups_labels.push("Trigger manual backup".to_string()); + + let list_block = Block::bordered().border_type(BorderType::Rounded); + let list = List::new(backups_labels) + .block(list_block) + .highlight_style(Style::new().reversed()) + .highlight_symbol("▶") + .repeat_highlight_symbol(true) + .direction(ListDirection::TopToBottom); + + frame.render_stateful_widget(list, layout[2], &mut app.list_state); Ok(()) } From d6b7454e10df4305426955fd595c26bfc610eaf0 Mon Sep 17 00:00:00 2001 From: Nolhan Date: Fri, 6 Mar 2026 10:26:28 +0100 Subject: [PATCH 7/7] refactor(tui): disabled mouse capture to enhance performances --- src/ui/app.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index 4a9c22e..b864707 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -55,7 +55,7 @@ impl App { pub async fn run(&mut self) -> io::Result<()> { enable_raw_mode()?; let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + execute!(stdout, EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; @@ -90,11 +90,7 @@ impl App { } disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; terminal.show_cursor()?; Ok(())