use crate::app::model::Model;
use crate::components::common::{ComponentId, Msg};
use crate::config::{self, AppConfig, ConfigValidationError};
use crate::error::{AppError, ErrorReporter};
use crate::theme::{ThemeConfig, ThemeManager};
use log::{debug, error, info, warn};
use std::error::Error as StdError;
use tuirealm::application::PollStrategy;
use tuirealm::terminal::CrosstermTerminalAdapter;
use tuirealm::{AttrValue, Attribute, Update};
#[derive(Debug)]
pub enum ThemeInitializationResult {
Success,
FallbackSuccess { error_message: String },
CriticalFailure { error_message: String },
}
pub struct ConfigErrorDisplay {
model: Model<CrosstermTerminalAdapter>,
}
impl ConfigErrorDisplay {
pub async fn new(
validation_errors: Vec<ConfigValidationError>,
) -> Result<Self, Box<dyn StdError>> {
let mut model = Model::new()
.await
.map_err(|e| format!("Failed to initialize model for error display: {e}"))?;
if let Some(first_error) = validation_errors.first() {
let error_message = first_error.user_message();
error!("Configuration error: {error_message}");
if let Err(e) = model.mount_error_popup(&AppError::Config(error_message)) {
error!("Failed to mount configuration error popup: {e}");
for validation_error in &validation_errors {
error!(
"Config validation error: {}",
validation_error.user_message()
);
}
}
}
for (i, validation_error) in validation_errors.iter().enumerate() {
error!("Config validation error {}: {:?}", i + 1, validation_error);
}
Ok(Self { model })
}
pub async fn show_and_wait_for_acknowledgment(&mut self) -> Result<(), Box<dyn StdError>> {
info!(
"Configuration validation failed. Application will exit after user acknowledges the error."
);
if let Err(e) = self.model.view() {
error!("Error during error popup rendering: {e}");
}
while !self.model.state_manager.should_quit() {
self.model.update_outside_msg();
match self.model.app.tick(PollStrategy::Once) {
Err(err) => {
error!("Application tick error during error display: {err}");
break;
}
Ok(messages) if !messages.is_empty() => {
for msg in messages.into_iter() {
let mut msg = Some(msg);
while msg.is_some() {
msg = self.model.update(msg);
}
}
if !self.model.app.mounted(&ComponentId::ErrorPopup) {
info!("Configuration error popup closed by user, terminating application");
self.model.set_quit(true);
break;
}
if let Err(e) = self.model.view() {
error!("Error during view rendering: {e}");
break;
}
}
_ => {}
}
}
Ok(())
}
pub fn shutdown(mut self) {
info!("Terminating application due to configuration errors");
self.model.shutdown();
let _ = self.model.terminal.leave_alternate_screen();
let _ = self.model.terminal.disable_raw_mode();
let _ = self.model.terminal.clear_screen();
}
}
pub struct ApplicationLifecycle;
impl ApplicationLifecycle {
pub async fn initialize_with_config_and_profile(
custom_config_path: Option<&str>,
profile_name: &str,
) -> Result<Model<CrosstermTerminalAdapter>, Box<dyn StdError>> {
info!("Starting Quetty application");
let config =
Self::load_configuration_with_path_and_profile(custom_config_path, profile_name)?;
let theme_init_result = Self::initialize_theme(&config.theme())?;
Self::validate_configuration(config).await?;
info!("Configuration loaded and validated successfully");
let mut model = Self::create_model().await?;
Self::handle_theme_fallback(&mut model, theme_init_result)?;
Ok(model)
}
fn load_configuration_with_path_and_profile(
custom_config_path: Option<&str>,
profile_name: &str,
) -> Result<&'static AppConfig, Box<dyn StdError>> {
let config_result = match custom_config_path {
Some(path) => {
info!("Loading configuration from custom path: {path}");
config::init_config_from_path(path)
}
None => {
info!("Loading configuration for profile: {profile_name}");
config::get_config_for_profile(profile_name)
}
};
match config_result {
config::ConfigLoadResult::Success(config) => Ok(config.as_ref()),
config::ConfigLoadResult::LoadError(error) => {
Self::report_critical_error(
AppError::Config(error.to_string()),
"ConfigurationLoader",
"load_config",
"Configuration loading failed. The application cannot start without a valid configuration.",
);
Err(error.to_string().into())
}
config::ConfigLoadResult::DeserializeError(error) => {
Self::report_critical_error(
AppError::Config(error.to_string()),
"ConfigurationParser",
"parse_config",
"Configuration parsing failed. Please fix your configuration syntax and try again.",
);
Err(error.to_string().into())
}
}
}
fn initialize_theme(
theme_config: &ThemeConfig,
) -> Result<ThemeInitializationResult, Box<dyn StdError>> {
let result = Self::try_initialize_theme_manager(theme_config);
if let ThemeInitializationResult::CriticalFailure { error_message } = &result {
Self::report_critical_error(
AppError::Config(error_message.clone()),
"ThemeManager",
"initialize",
"Application cannot start due to theme initialization failure. Please check your theme configuration.",
);
return Err(error_message.clone().into());
}
Ok(result)
}
fn try_initialize_theme_manager(theme_config: &ThemeConfig) -> ThemeInitializationResult {
if let Err(e) = ThemeManager::init_global(theme_config) {
error!("Failed to initialize theme manager with user config: {e}");
let default_config = ThemeConfig::default();
if let Err(default_e) = ThemeManager::init_global(&default_config) {
error!("Failed to initialize theme manager with default theme: {default_e}");
return ThemeInitializationResult::CriticalFailure {
error_message: format!(
"Critical theme error: Unable to load any theme.\n\nUser theme error: {e}\nDefault theme error: {default_e}\n\nPlease check your theme files."
),
};
} else {
info!("Successfully fell back to default theme");
return ThemeInitializationResult::FallbackSuccess {
error_message: format!(
"Unable to load theme '{}' with flavor '{}': {}\n\nFalling back to default theme (quetty/dark).",
theme_config.theme_name, theme_config.flavor_name, e
),
};
}
}
ThemeInitializationResult::Success
}
async fn validate_configuration(config: &AppConfig) -> Result<(), Box<dyn StdError>> {
if let Err(validation_errors) = config.validate() {
error!(
"Configuration validation failed with {} errors",
validation_errors.len()
);
let only_auth_errors =
validation_errors.iter().all(|error| {
matches!(error,
crate::config::validation::ConfigValidationError::MissingAzureAdField { .. } |
crate::config::validation::ConfigValidationError::ConflictingAuthConfig { .. }
)
});
if only_auth_errors {
warn!("Configuration has missing authentication fields - will show config screen");
return Ok(()); }
Self::show_config_error_and_exit(validation_errors).await?;
return Err("Configuration validation failed".into());
}
Ok(())
}
async fn create_model() -> Result<Model<CrosstermTerminalAdapter>, Box<dyn StdError>> {
match Model::new().await {
Ok(model) => {
info!("Model initialized successfully");
Ok(model)
}
Err(e) => {
Self::report_critical_error(
AppError::Component(e.to_string()),
"ApplicationModel",
"initialize",
"Failed to initialize application model. The application cannot start. Please check your configuration and try again.",
);
Err(e.into())
}
}
}
fn handle_theme_fallback(
model: &mut Model<CrosstermTerminalAdapter>,
theme_init_result: ThemeInitializationResult,
) -> Result<(), Box<dyn StdError>> {
if let ThemeInitializationResult::FallbackSuccess { error_message } = theme_init_result {
if let Err(e) = model.mount_error_popup(&AppError::Config(error_message)) {
model.error_reporter.report_config_error("theme", &e);
}
}
Ok(())
}
pub fn setup_terminal(
model: &mut Model<CrosstermTerminalAdapter>,
) -> Result<(), Box<dyn StdError>> {
debug!("Entering alternate screen");
model
.terminal
.enter_alternate_screen()
.map_err(|e| format!("Failed to enter alternate screen: {e}"))?;
model
.terminal
.enable_raw_mode()
.map_err(|e| format!("Failed to enable raw mode: {e}"))?;
Ok(())
}
pub fn run_application_loop(
model: &mut Model<CrosstermTerminalAdapter>,
) -> Result<(), Box<dyn StdError>> {
info!("Entering main application loop");
while !model.state_manager.should_quit() {
Self::process_single_iteration(model)?;
}
info!("Application loop exited");
Ok(())
}
fn process_single_iteration(
model: &mut Model<CrosstermTerminalAdapter>,
) -> Result<(), Box<dyn StdError>> {
model.update_outside_msg();
match model.app.tick(PollStrategy::Once) {
Err(err) => {
Self::handle_tick_error(model, err)?;
}
Ok(messages) if !messages.is_empty() => {
Self::process_messages(model, messages);
}
_ => {}
}
Self::handle_redraw(model)?;
Ok(())
}
fn handle_tick_error(
model: &mut Model<CrosstermTerminalAdapter>,
err: tuirealm::ApplicationError,
) -> Result<(), Box<dyn StdError>> {
error!("Application tick error: {err:?}");
if let Err(e) =
model.mount_error_popup(&AppError::Component(format!("Application error: {err:?}")))
{
error!("Failed to mount error popup: {e}");
if model
.app
.attr(
&ComponentId::TextLabel,
Attribute::Text,
AttrValue::String(format!("Application error: {err:?}")),
)
.is_err()
{
return Err(format!("Failed to display error: {err:?}").into());
}
}
model.state_manager.set_redraw(true);
Ok(())
}
fn process_messages(model: &mut Model<CrosstermTerminalAdapter>, messages: Vec<Msg>) {
model.state_manager.set_redraw(true);
for msg in messages.into_iter() {
let mut msg = Some(msg);
while msg.is_some() {
msg = model.update(msg);
}
}
}
fn handle_redraw(model: &mut Model<CrosstermTerminalAdapter>) -> Result<(), Box<dyn StdError>> {
if model.state_manager.needs_redraw() {
if let Err(e) = model.view() {
error!("Error during view rendering: {e}");
if let Err(popup_err) = model.mount_error_popup(&e) {
model
.error_reporter
.report_mount_error("ErrorPopup", "mount", popup_err);
model
.error_reporter
.report_simple(e, "ViewRendering", "main_loop");
}
}
model.state_manager.redraw_complete();
}
Ok(())
}
pub fn shutdown_application(
mut model: Model<CrosstermTerminalAdapter>,
) -> Result<(), Box<dyn StdError>> {
info!("Application shutdown initiated");
model.shutdown();
debug!("Leaving alternate screen");
let _ = model.terminal.leave_alternate_screen();
let _ = model.terminal.disable_raw_mode();
let _ = model.terminal.clear_screen();
info!("Application terminated successfully");
Ok(())
}
async fn show_config_error_and_exit(
validation_errors: Vec<ConfigValidationError>,
) -> Result<(), Box<dyn StdError>> {
let mut error_display = ConfigErrorDisplay::new(validation_errors).await?;
error_display.show_and_wait_for_acknowledgment().await?;
error_display.shutdown();
Ok(())
}
fn report_critical_error(
error: AppError,
component: &str,
operation: &str,
user_message: &str,
) {
let (tx, _rx) = std::sync::mpsc::channel();
let error_reporter = ErrorReporter::new(tx);
error_reporter.report_critical_and_exit(error, component, operation, user_message);
eprintln!("Critical Error: {user_message}");
}
}