use crate::config::Config;
use crate::core::{Mirror, MirrorTester, TestResult};
use crate::storage::Database;
use anyhow::Result;
use ratatui::widgets::ListState;
use std::sync::Arc;
use tracing::{debug, info};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum View {
Dashboard,
Testing,
History,
Config,
StaticMirrors,
Help,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TestingState {
Idle,
InProgress {
completed: usize,
total: usize,
current_mirror: String,
},
Completed,
}
pub struct App {
pub current_view: View,
pub should_quit: bool,
pub mirrors: Vec<Mirror>,
pub test_results: Vec<TestResult>,
pub selected_mirror: Option<usize>,
pub list_state: ListState,
pub testing_state: TestingState,
pub config: Config,
pub database: Database,
pub service_running: bool,
pub last_update: Option<chrono::DateTime<chrono::Utc>>,
pub history: Vec<crate::storage::UpdateRecord>,
pub static_mirrors: Vec<Mirror>,
pub selected_history: Option<usize>,
pub history_list_state: ListState,
pub config_edit_mode: bool,
pub selected_config_item: usize,
pub status_message: Option<String>,
pub error_message: Option<String>,
pub input_buffer: String,
pub input_mode: bool,
pub scroll_offset: u16,
}
impl App {
pub async fn new(config: Config, database: Database) -> Result<Self> {
debug!("Initializing TUI application");
let mirrors = database.get_mirrors(&Default::default()).unwrap_or_default();
let test_results = database.get_test_results(&Default::default()).unwrap_or_default();
let history = database.get_history(50).unwrap_or_default();
let last_update = database.get_latest_update()
.ok()
.flatten()
.map(|r| r.updated_at);
let static_mirrors = database.get_mirrors(&crate::storage::MirrorFilter {
static_only: true,
..Default::default()
}).unwrap_or_default();
let service_running = Self::check_service_status();
let mut list_state = ListState::default();
if !mirrors.is_empty() {
list_state.select(Some(0));
}
info!("TUI application initialized with {} mirrors", mirrors.len());
Ok(Self {
current_view: View::Dashboard,
should_quit: false,
mirrors,
test_results,
selected_mirror: Some(0),
list_state,
testing_state: TestingState::Idle,
config,
database,
service_running,
last_update,
history,
static_mirrors,
selected_history: None,
history_list_state: ListState::default(),
config_edit_mode: false,
selected_config_item: 0,
status_message: None,
error_message: None,
input_buffer: String::new(),
input_mode: false,
scroll_offset: 0,
})
}
fn check_service_status() -> bool {
std::process::Command::new("systemctl")
.args(&["is-active", "--quiet", "smirrors.service"])
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub fn switch_view(&mut self, view: View) {
debug!("Switching to view: {:?}", self.current_view);
self.current_view = view;
self.clear_messages();
}
pub fn next_mirror(&mut self) {
let list_len = match self.current_view {
View::Dashboard | View::Testing => self.mirrors.len(),
View::History => self.history.len(),
View::StaticMirrors => self.static_mirrors.len(),
_ => return,
};
if list_len == 0 {
return;
}
let state = match self.current_view {
View::History => &mut self.history_list_state,
_ => &mut self.list_state,
};
let i = match state.selected() {
Some(i) => {
if i >= list_len - 1 {
0
} else {
i + 1
}
}
None => 0,
};
state.select(Some(i));
match self.current_view {
View::History => self.selected_history = Some(i),
_ => self.selected_mirror = Some(i),
}
}
pub fn previous_mirror(&mut self) {
let list_len = match self.current_view {
View::Dashboard | View::Testing => self.mirrors.len(),
View::History => self.history.len(),
View::StaticMirrors => self.static_mirrors.len(),
_ => return,
};
if list_len == 0 {
return;
}
let state = match self.current_view {
View::History => &mut self.history_list_state,
_ => &mut self.list_state,
};
let i = match state.selected() {
Some(i) => {
if i == 0 {
list_len - 1
} else {
i - 1
}
}
None => 0,
};
state.select(Some(i));
match self.current_view {
View::History => self.selected_history = Some(i),
_ => self.selected_mirror = Some(i),
}
}
pub fn select_mirror(&mut self, index: usize) {
if index < self.mirrors.len() {
self.list_state.select(Some(index));
self.selected_mirror = Some(index);
}
}
pub fn get_selected_mirror(&self) -> Option<&Mirror> {
self.selected_mirror.and_then(|i| self.mirrors.get(i))
}
pub async fn run_test(&mut self) -> Result<()> {
info!("Starting mirror tests");
self.testing_state = TestingState::InProgress {
completed: 0,
total: self.mirrors.len(),
current_mirror: String::new(),
};
let tester = MirrorTester::from_config(&self.config)?;
let mirrors = self.mirrors.clone();
let progress_callback = {
let total = mirrors.len();
Arc::new(move |completed: usize, _: usize, mirror: &str| {
debug!("Test progress: {}/{} - {}", completed, total, mirror);
})
};
let results = tester.test_all(mirrors, Some(progress_callback)).await;
for result in &results {
if let Err(e) = self.database.save_test_result(result) {
debug!("Failed to save test result: {}", e);
}
}
self.test_results = results;
self.testing_state = TestingState::Completed;
self.refresh_mirrors()?;
self.status_message = Some(format!(
"Testing completed: {} mirrors tested",
self.test_results.len()
));
info!("Mirror tests completed");
Ok(())
}
pub async fn run_update(&mut self, dry_run: bool) -> Result<()> {
info!("Starting mirror update (dry_run: {})", dry_run);
if dry_run {
self.status_message = Some("Dry run: Would update mirrors".to_string());
return Ok(());
}
self.status_message = Some("Update triggered".to_string());
Ok(())
}
pub async fn add_static_mirror(&mut self, url: String) -> Result<()> {
info!("Adding static mirror: {}", url);
let parsed_url = url::Url::parse(&url)
.map_err(|e| anyhow::anyhow!("Invalid URL: {}", e))?;
let mirror = Mirror::new_static(parsed_url);
self.database.save_mirror(&mirror)?;
self.static_mirrors = self.database.get_mirrors(&crate::storage::MirrorFilter {
static_only: true,
..Default::default()
})?;
self.status_message = Some(format!("Added static mirror: {}", url));
info!("Static mirror added successfully");
Ok(())
}
pub async fn remove_static_mirror(&mut self) -> Result<()> {
if let Some(idx) = self.selected_mirror {
if let Some(mirror) = self.static_mirrors.get(idx) {
info!("Removing static mirror: {}", mirror.url);
self.status_message = Some(format!("Removed static mirror: {}", mirror.url));
}
}
Ok(())
}
pub fn refresh_mirrors(&mut self) -> Result<()> {
debug!("Refreshing mirrors from database");
self.mirrors = self.database.get_mirrors(&Default::default())?;
self.test_results = self.database.get_test_results(&Default::default())?;
self.history = self.database.get_history(50)?;
Ok(())
}
pub fn reload_config(&mut self) -> Result<()> {
debug!("Reloading configuration");
self.config = Config::load()?;
self.status_message = Some("Configuration reloaded".to_string());
Ok(())
}
pub fn save_config(&mut self) -> Result<()> {
debug!("Saving configuration");
self.config.save()?;
self.status_message = Some("Configuration saved".to_string());
Ok(())
}
pub fn handle_input_char(&mut self, c: char) {
if self.input_mode {
self.input_buffer.push(c);
}
}
pub fn handle_input_backspace(&mut self) {
if self.input_mode {
self.input_buffer.pop();
}
}
pub fn submit_input(&mut self) {
if !self.input_buffer.is_empty() {
match self.current_view {
View::StaticMirrors => {
self.input_mode = false;
}
_ => {}
}
}
}
pub fn cancel_input(&mut self) {
self.input_mode = false;
self.input_buffer.clear();
}
pub fn enter_input_mode(&mut self) {
self.input_mode = true;
self.input_buffer.clear();
}
pub fn clear_messages(&mut self) {
self.status_message = None;
self.error_message = None;
}
pub fn set_error(&mut self, msg: String) {
self.error_message = Some(msg);
}
pub fn set_status(&mut self, msg: String) {
self.status_message = Some(msg);
}
pub fn next_config_item(&mut self) {
self.selected_config_item = (self.selected_config_item + 1) % 15;
}
pub fn previous_config_item(&mut self) {
if self.selected_config_item == 0 {
self.selected_config_item = 14;
} else {
self.selected_config_item -= 1;
}
}
pub fn quit(&mut self) {
info!("Quitting TUI application");
self.should_quit = true;
}
}