quetty 0.1.9

Terminal-based Azure Service Bus queue manager with intuitive TUI interface
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
//! Application lifecycle management
//!
//! This module handles the initialization, main loop, and shutdown of the application.
//! It extracts complex functionality from main.rs to improve maintainability.

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};

/// Result of theme initialization attempt
#[derive(Debug)]
pub enum ThemeInitializationResult {
    /// Theme loaded successfully with no errors
    Success,
    /// User theme failed, but fallback to default succeeded. Contains error message to show user.
    FallbackSuccess { error_message: String },
    /// Both user theme and default theme failed. Application should exit.
    CriticalFailure { error_message: String },
}

/// Manages the display of configuration errors with user interaction
pub struct ConfigErrorDisplay {
    model: Model<CrosstermTerminalAdapter>,
}

impl ConfigErrorDisplay {
    /// Initialize the error display with the given validation errors
    pub async fn new(
        validation_errors: Vec<ConfigValidationError>,
    ) -> Result<Self, Box<dyn StdError>> {
        // Initialize a minimal model for error display
        let mut model = Model::new()
            .await
            .map_err(|e| format!("Failed to initialize model for error display: {e}"))?;

        // Show the first error in a popup (most critical one)
        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}");
                // Fallback to logging all errors
                for validation_error in &validation_errors {
                    error!(
                        "Config validation error: {}",
                        validation_error.user_message()
                    );
                }
            }
        }

        // Also log all validation errors for debugging
        for (i, validation_error) in validation_errors.iter().enumerate() {
            error!("Config validation error {}: {:?}", i + 1, validation_error);
        }

        Ok(Self { model })
    }

    /// Show the error popup and wait for user acknowledgment
    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."
        );

        // Draw the error popup
        if let Err(e) = self.model.view() {
            error!("Error during error popup rendering: {e}");
        }

        // Main loop to handle the error popup until user closes it
        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() {
                            // Handle the message
                            msg = self.model.update(msg);
                        }
                    }

                    // Check if error popup was closed - if so, quit the app
                    if !self.model.app.mounted(&ComponentId::ErrorPopup) {
                        info!("Configuration error popup closed by user, terminating application");
                        self.model.set_quit(true);
                        break;
                    }

                    // Check if popup was closed (which should set quit to true)
                    if let Err(e) = self.model.view() {
                        error!("Error during view rendering: {e}");
                        break;
                    }
                }
                _ => {}
            }
        }

        Ok(())
    }

    /// Properly shutdown the error display
    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();
    }
}

/// Application initialization and lifecycle management
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())
            }
        }
    }

    /// Initialize the global theme manager
    fn initialize_theme(
        theme_config: &ThemeConfig,
    ) -> Result<ThemeInitializationResult, Box<dyn StdError>> {
        let result = Self::try_initialize_theme_manager(theme_config);

        // Handle critical theme failures immediately
        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)
    }

    /// Attempt to initialize theme manager with fallback
    fn try_initialize_theme_manager(theme_config: &ThemeConfig) -> ThemeInitializationResult {
        // Try to initialize with user's theme config first
        if let Err(e) = ThemeManager::init_global(theme_config) {
            error!("Failed to initialize theme manager with user config: {e}");

            // Try to fallback to default theme
            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
                    ),
                };
            }
        }

        // User theme loaded successfully
        ThemeInitializationResult::Success
    }

    /// Validate configuration after theme manager is initialized
    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()
            );

            // Check if the validation errors are only related to missing authentication fields
            // If so, allow the application to start and show the config screen instead of exiting
            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(()); // Allow startup to continue
            }

            // For other validation errors, still show error and exit
            Self::show_config_error_and_exit(validation_errors).await?;
            return Err("Configuration validation failed".into());
        }
        Ok(())
    }

    /// Create and initialize the application model
    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())
            }
        }
    }

    /// Handle theme fallback by showing error popup if needed
    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(())
    }

    /// Setup terminal for application use
    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(())
    }

    /// Run the main application loop
    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(())
    }

    /// Process a single iteration of the main loop
    fn process_single_iteration(
        model: &mut Model<CrosstermTerminalAdapter>,
    ) -> Result<(), Box<dyn StdError>> {
        model.update_outside_msg();

        // Tick and handle messages
        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);
            }
            _ => {}
        }

        // Handle redraw if needed
        Self::handle_redraw(model)?;

        Ok(())
    }

    /// Handle tick errors by showing error popup
    fn handle_tick_error(
        model: &mut Model<CrosstermTerminalAdapter>,
        err: tuirealm::ApplicationError,
    ) -> Result<(), Box<dyn StdError>> {
        error!("Application tick error: {err:?}");

        // Show error in popup
        if let Err(e) =
            model.mount_error_popup(&AppError::Component(format!("Application error: {err:?}")))
        {
            error!("Failed to mount error popup: {e}");
            // Fallback to simpler error handling
            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(())
    }

    /// Process all received messages
    fn process_messages(model: &mut Model<CrosstermTerminalAdapter>, messages: Vec<Msg>) {
        // Process all received messages and trigger redraw if any were handled
        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);
            }
        }
    }

    /// Handle view redraw if needed
    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}");
                // Show error in popup
                if let Err(popup_err) = model.mount_error_popup(&e) {
                    model
                        .error_reporter
                        .report_mount_error("ErrorPopup", "mount", popup_err);
                    // Since we can't show the error popup, report the original error through ErrorReporter
                    model
                        .error_reporter
                        .report_simple(e, "ViewRendering", "main_loop");
                }
            }
            model.state_manager.redraw_complete();
        }
        Ok(())
    }

    /// Properly shutdown the application
    pub fn shutdown_application(
        mut model: Model<CrosstermTerminalAdapter>,
    ) -> Result<(), Box<dyn StdError>> {
        info!("Application shutdown initiated");
        model.shutdown();

        // Terminate terminal
        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(())
    }

    /// Show configuration error and exit
    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(())
    }

    /// Report critical error and prepare for application exit
    /// Uses ErrorReporter system for consistency with application error handling
    fn report_critical_error(
        error: AppError,
        component: &str,
        operation: &str,
        user_message: &str,
    ) {
        // Create a temporary ErrorReporter for critical initialization errors
        // This ensures consistency with the application's error handling patterns
        let (tx, _rx) = std::sync::mpsc::channel();
        let error_reporter = ErrorReporter::new(tx);

        // Use the specialized critical error reporting
        error_reporter.report_critical_and_exit(error, component, operation, user_message);

        // Also ensure the error is visible in case ErrorReporter fails
        eprintln!("Critical Error: {user_message}");
    }
}