parsql_cli/ui/
app.rs

1//! Main application state and UI rendering
2
3use anyhow::Result;
4use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5use ratatui::{
6    layout::{Constraint, Direction, Layout, Rect},
7    style::{Color, Style},
8    text::{Line, Span},
9    widgets::{Block, Borders, Paragraph, Wrap},
10    Frame,
11};
12
13use crate::config::Config;
14use super::command_input::CommandInput;
15use super::components::{render_header, render_status_bar};
16use super::migration_list::MigrationListView;
17use super::migration_detail::MigrationDetailView;
18use super::help::HelpView;
19use super::output_stream::OutputStreamWidget;
20use super::theme::ModernTheme;
21use super::database::DatabaseInfo;
22use super::migration_creator::MigrationCreator;
23use super::migration_loader::MigrationLoader;
24use super::migration_executor::MigrationExecutor;
25use super::migration_viewer::{MigrationViewer, MigrationFileType};
26use super::migration_content_view::MigrationContentView;
27
28#[derive(Debug, Clone, PartialEq)]
29pub enum AppMode {
30    Normal,
31    CommandInput,
32    Help,
33}
34
35#[derive(Debug, Clone, PartialEq)]
36pub enum View {
37    MigrationList,
38    MigrationDetail { version: i64 },
39    DatabaseConfig,
40    Logs,
41}
42
43pub struct App {
44    pub mode: AppMode,
45    pub view: View,
46    pub command_input: CommandInput,
47    pub migration_list: MigrationListView,
48    pub migration_detail: MigrationDetailView,
49    pub help_view: HelpView,
50    pub output_stream: OutputStreamWidget,
51    pub migration_content_view: MigrationContentView,
52    pub database_url: Option<String>,
53    pub config: Config,
54    pub verbose: bool,
55    pub messages: Vec<(String, MessageType)>,
56    pub should_quit: bool,
57}
58
59#[derive(Debug, Clone)]
60pub enum MessageType {
61    Info,
62    Success,
63    Warning,
64    Error,
65}
66
67impl App {
68    pub fn new(database_url: Option<String>, config: Config, verbose: bool) -> Self {
69        let mut app = Self {
70            mode: AppMode::Normal,
71            view: View::MigrationList,
72            command_input: CommandInput::new(),
73            migration_list: MigrationListView::new(),
74            migration_detail: MigrationDetailView::new(),
75            help_view: HelpView::new(),
76            output_stream: OutputStreamWidget::new(1000),
77            migration_content_view: MigrationContentView::new(),
78            database_url,
79            config,
80            verbose,
81            messages: Vec::new(),
82            should_quit: false,
83        };
84        
85        // Load initial data
86        app.refresh_data();
87        
88        app
89    }
90    
91    pub fn refresh_data(&mut self) {
92        // Load migrations based on database connection
93        if let Some(ref db_url) = self.database_url {
94            self.output_stream.add_info("Refreshing migration data...".to_string());
95            
96            // Load migrations from directory and database
97            let migrations_dir = std::path::PathBuf::from(&self.config.migrations.directory);
98            let loader = MigrationLoader::new(migrations_dir, self.config.migrations.to_parsql_migrations_config());
99            
100            // Load migration files
101            match loader.load_sql_migrations() {
102                Ok(sql_migrations) => {
103                    self.output_stream.add_info(format!("Found {} migration files", sql_migrations.len()));
104                    
105                    // Get migration status (blocking for now)
106                    match loader.get_migration_status_blocking(db_url) {
107                        Ok(statuses) => {
108                            // Update migration list view
109                            self.migration_list.set_migrations(statuses);
110                            
111                            let applied_count = self.migration_list.migrations.iter()
112                                .filter(|m| m.applied)
113                                .count();
114                            let pending_count = self.migration_list.migrations.len() - applied_count;
115                            
116                            self.output_stream.add_success(format!(
117                                "Loaded {} migrations ({} applied, {} pending)",
118                                self.migration_list.migrations.len(),
119                                applied_count,
120                                pending_count
121                            ));
122                        }
123                        Err(e) => {
124                            self.output_stream.add_error(format!("Failed to get migration status: {}", e));
125                        }
126                    }
127                }
128                Err(e) => {
129                    self.output_stream.add_error(format!("Failed to load migrations: {}", e));
130                }
131            }
132        } else {
133            self.output_stream.add_warning("No database connection. Use /connect to set database URL".to_string());
134            self.add_message("No database connection. Use /connect to set database URL".to_string(), MessageType::Warning);
135        }
136    }
137    
138    pub fn add_message(&mut self, message: String, msg_type: MessageType) {
139        self.messages.push((message, msg_type));
140        // Keep only last 10 messages
141        if self.messages.len() > 10 {
142            self.messages.remove(0);
143        }
144    }
145    
146    pub fn handle_key_event(&mut self, key: KeyEvent) -> Result<bool> {
147        match self.mode {
148            AppMode::Normal => self.handle_normal_mode_key(key),
149            AppMode::CommandInput => self.handle_command_input_key(key),
150            AppMode::Help => self.handle_help_mode_key(key),
151        }
152    }
153    
154    fn handle_normal_mode_key(&mut self, key: KeyEvent) -> Result<bool> {
155        // Check if migration content view is visible
156        if self.migration_content_view.is_visible() {
157            match key.code {
158                KeyCode::Esc | KeyCode::Char('q') => {
159                    self.migration_content_view.hide();
160                    Ok(false)
161                }
162                KeyCode::Up | KeyCode::Char('k') => {
163                    self.migration_content_view.scroll_up();
164                    Ok(false)
165                }
166                KeyCode::Down | KeyCode::Char('j') => {
167                    self.migration_content_view.scroll_down(20); // Approximate viewport height
168                    Ok(false)
169                }
170                KeyCode::PageUp => {
171                    self.migration_content_view.scroll_page_up(20);
172                    Ok(false)
173                }
174                KeyCode::PageDown => {
175                    self.migration_content_view.scroll_page_down(20);
176                    Ok(false)
177                }
178                _ => Ok(false)
179            }
180        } else {
181            match key.code {
182                KeyCode::Char('q') if key.modifiers.contains(KeyModifiers::CONTROL) => {
183                    self.should_quit = true;
184                    Ok(true)
185                }
186                KeyCode::Char('/') => {
187                    self.mode = AppMode::CommandInput;
188                    self.command_input.clear();
189                    // Initialize with '/' character
190                    self.command_input.handle_key(KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE));
191                    Ok(false)
192                }
193                KeyCode::Char('?') => {
194                    self.mode = AppMode::Help;
195                    Ok(false)
196                }
197                KeyCode::Tab => {
198                    // Switch between views
199                    self.view = match self.view {
200                        View::MigrationList => View::Logs,
201                        View::MigrationDetail { .. } => View::MigrationList,
202                        View::DatabaseConfig => View::MigrationList,
203                        View::Logs => View::MigrationList,
204                    };
205                    Ok(false)
206                }
207                _ => {
208                    // Pass key to current view
209                    self.handle_view_key(key)
210                }
211            }
212        }
213    }
214    
215    fn handle_command_input_key(&mut self, key: KeyEvent) -> Result<bool> {
216        match key.code {
217            KeyCode::Esc => {
218                self.mode = AppMode::Normal;
219                self.command_input.clear();
220                Ok(false)
221            }
222            KeyCode::Enter => {
223                let command = self.command_input.get_command();
224                self.mode = AppMode::Normal;
225                self.execute_command(&command)?;
226                self.command_input.clear();
227                Ok(false)
228            }
229            KeyCode::Tab => {
230                self.command_input.complete_suggestion();
231                Ok(false)
232            }
233            _ => {
234                self.command_input.handle_key(key);
235                Ok(false)
236            }
237        }
238    }
239    
240    fn handle_help_mode_key(&mut self, key: KeyEvent) -> Result<bool> {
241        match key.code {
242            KeyCode::Esc | KeyCode::Char('q') => {
243                self.mode = AppMode::Normal;
244                Ok(false)
245            }
246            _ => {
247                self.help_view.handle_key(key);
248                Ok(false)
249            }
250        }
251    }
252    
253    fn execute_command(&mut self, command: &str) -> Result<()> {
254        // Log the command being executed
255        self.output_stream.add_command(command.to_string());
256        
257        let parts: Vec<&str> = command.split_whitespace().collect();
258        if parts.is_empty() {
259            return Ok(());
260        }
261        
262        match parts[0] {
263            "/help" | "/h" => {
264                self.mode = AppMode::Help;
265            }
266            "/quit" | "/q" => {
267                self.should_quit = true;
268            }
269            "/connect" => {
270                if parts.len() > 1 {
271                    let db_url = parts[1..].join(" ");
272                    self.output_stream.add_info(format!("Connecting to database: {}", db_url));
273                    
274                    match DatabaseInfo::parse(&db_url) {
275                        Ok(db_info) => {
276                            self.output_stream.add_info(format!("Parsed connection: {}", db_info.display_path));
277                            
278                            // Test the connection
279                            match db_info.test_connection() {
280                                Ok(_) => {
281                                    self.database_url = Some(db_info.connection_string.clone());
282                                    self.output_stream.add_success(format!("Successfully connected to: {}", db_info.display_path));
283                                    self.add_message(format!("Connected to: {}", db_info.display_path), MessageType::Success);
284                                    
285                                    // For SQLite, show the actual file path
286                                    if let super::database::DatabaseType::SQLite = db_info.db_type {
287                                        if let Some(path) = db_info.connection_string.strip_prefix("sqlite:") {
288                                            if path != ":memory:" {
289                                                self.output_stream.add_info(format!("Database file: {}", path));
290                                            }
291                                        }
292                                    }
293                                    
294                                    self.refresh_data();
295                                }
296                                Err(e) => {
297                                    self.output_stream.add_error(format!("Connection failed: {}", e));
298                                    self.add_message(format!("Connection failed: {}", e), MessageType::Error);
299                                }
300                            }
301                        }
302                        Err(e) => {
303                            self.output_stream.add_error(format!("Invalid database URL: {}", e));
304                            self.add_message(format!("Invalid database URL: {}", e), MessageType::Error);
305                        }
306                    }
307                } else {
308                    self.output_stream.add_error("Usage: /connect <database_url>".to_string());
309                    self.add_message("Usage: /connect <database_url>".to_string(), MessageType::Error);
310                }
311            }
312            "/create" => {
313                if parts.len() > 1 {
314                    let name = parts[1..].join("_");
315                    let migration_type = "sql"; // Default to SQL migrations
316                    
317                    self.output_stream.add_info(format!("Creating {} migration: {}", migration_type.to_uppercase(), name));
318                    self.output_stream.add_progress("Generating migration files...".to_string());
319                    
320                    // Get migrations directory from config or use default
321                    let migrations_dir = std::path::PathBuf::from(&self.config.migrations.directory);
322                    let creator = MigrationCreator::new(migrations_dir.clone());
323                    
324                    match creator.create_migration(&name, migration_type) {
325                        Ok(files) => {
326                            self.output_stream.add_success(format!("Created migration: {} (v{})", files.name, files.version));
327                            self.output_stream.add_info(format!("  Up file: {}", files.up_file));
328                            if let Some(down_file) = files.down_file {
329                                self.output_stream.add_info(format!("  Down file: {}", down_file));
330                            }
331                            self.output_stream.add_info(format!("Edit the migration files in: {}", migrations_dir.display()));
332                            self.add_message(format!("Created migration: {}", name), MessageType::Success);
333                            
334                            // Refresh migration list
335                            self.refresh_data();
336                        }
337                        Err(e) => {
338                            self.output_stream.add_error(format!("Failed to create migration: {}", e));
339                            self.add_message(format!("Failed to create migration: {}", e), MessageType::Error);
340                        }
341                    }
342                } else {
343                    self.output_stream.add_error("Usage: /create <migration_name>".to_string());
344                    self.add_message("Usage: /create <migration_name>".to_string(), MessageType::Error);
345                }
346            }
347            "/run" => {
348                if self.database_url.is_none() {
349                    self.output_stream.add_error("No database connection. Use /connect first".to_string());
350                    self.add_message("No database connection".to_string(), MessageType::Error);
351                    return Ok(());
352                }
353                
354                let db_url = self.database_url.as_ref().unwrap();
355                self.output_stream.add_info("Checking for pending migrations...".to_string());
356                
357                // Load migrations
358                let migrations_dir = std::path::PathBuf::from(&self.config.migrations.directory);
359                let loader = MigrationLoader::new(migrations_dir.clone(), self.config.migrations.to_parsql_migrations_config());
360                
361                match loader.load_sql_migrations() {
362                    Ok(sql_migrations) => {
363                        // Filter pending migrations
364                        let pending_count = self.migration_list.get_pending_count();
365                        if pending_count == 0 {
366                            self.output_stream.add_info("No pending migrations to run".to_string());
367                            return Ok(());
368                        }
369                        
370                        self.output_stream.add_progress(format!("Running {} pending migrations...", pending_count));
371                        
372                        // Execute migrations
373                        let executor = MigrationExecutor::new(self.config.migrations.to_parsql_migrations_config());
374                        
375                        if db_url.starts_with("sqlite:") {
376                            let db_path = db_url.strip_prefix("sqlite:").unwrap();
377                            match executor.run_sqlite_migrations(db_path, sql_migrations, &mut self.output_stream) {
378                                Ok(count) => {
379                                    self.output_stream.add_success(format!("Successfully ran {} migrations", count));
380                                    self.add_message(format!("Ran {} migrations", count), MessageType::Success);
381                                    self.refresh_data(); // Refresh to show updated status
382                                }
383                                Err(e) => {
384                                    self.output_stream.add_error(format!("Migration failed: {}", e));
385                                    self.add_message(format!("Migration failed: {}", e), MessageType::Error);
386                                }
387                            }
388                        } else if db_url.starts_with("postgresql://") || db_url.starts_with("postgres://") {
389                            match executor.run_postgres_migrations(db_url, sql_migrations, &mut self.output_stream) {
390                                Ok(count) => {
391                                    self.output_stream.add_success(format!("Successfully ran {} migrations", count));
392                                    self.add_message(format!("Ran {} migrations", count), MessageType::Success);
393                                    self.refresh_data(); // Refresh to show updated status
394                                }
395                                Err(e) => {
396                                    self.output_stream.add_error(format!("Migration failed: {}", e));
397                                    self.add_message(format!("Migration failed: {}", e), MessageType::Error);
398                                }
399                            }
400                        } else {
401                            self.output_stream.add_error("Unsupported database URL format".to_string());
402                        }
403                    }
404                    Err(e) => {
405                        self.output_stream.add_error(format!("Failed to load migrations: {}", e));
406                    }
407                }
408            }
409            "/rollback" => {
410                if self.database_url.is_none() {
411                    self.output_stream.add_error("No database connection. Use /connect first".to_string());
412                    self.add_message("No database connection".to_string(), MessageType::Error);
413                    return Ok(());
414                }
415                
416                if parts.len() > 1 {
417                    if let Ok(target_version) = parts[1].parse::<i64>() {
418                        let db_url = self.database_url.as_ref().unwrap();
419                        self.output_stream.add_info(format!("Rolling back to version: {}", target_version));
420                        
421                        // Load migrations
422                        let migrations_dir = std::path::PathBuf::from(&self.config.migrations.directory);
423                        let loader = MigrationLoader::new(migrations_dir.clone(), self.config.migrations.to_parsql_migrations_config());
424                        
425                        match loader.load_sql_migrations() {
426                            Ok(sql_migrations) => {
427                                let executor = MigrationExecutor::new(self.config.migrations.to_parsql_migrations_config());
428                                
429                                if db_url.starts_with("sqlite:") {
430                                    let db_path = db_url.strip_prefix("sqlite:").unwrap();
431                                    match executor.rollback_sqlite(db_path, target_version, sql_migrations, &mut self.output_stream) {
432                                        Ok(count) => {
433                                            self.output_stream.add_success(format!("Successfully rolled back {} migrations", count));
434                                            self.add_message(format!("Rolled back {} migrations", count), MessageType::Success);
435                                            self.refresh_data(); // Refresh to show updated status
436                                        }
437                                        Err(e) => {
438                                            self.output_stream.add_error(format!("Rollback failed: {}", e));
439                                            self.add_message(format!("Rollback failed: {}", e), MessageType::Error);
440                                        }
441                                    }
442                                } else if db_url.starts_with("postgresql://") || db_url.starts_with("postgres://") {
443                                    match executor.rollback_postgres(db_url, target_version, sql_migrations, &mut self.output_stream) {
444                                        Ok(count) => {
445                                            self.output_stream.add_success(format!("Successfully rolled back {} migrations", count));
446                                            self.add_message(format!("Rolled back {} migrations", count), MessageType::Success);
447                                            self.refresh_data(); // Refresh to show updated status
448                                        }
449                                        Err(e) => {
450                                            self.output_stream.add_error(format!("Rollback failed: {}", e));
451                                            self.add_message(format!("Rollback failed: {}", e), MessageType::Error);
452                                        }
453                                    }
454                                } else {
455                                    self.output_stream.add_error("Unsupported database URL format".to_string());
456                                }
457                            }
458                            Err(e) => {
459                                self.output_stream.add_error(format!("Failed to load migrations: {}", e));
460                            }
461                        }
462                    } else {
463                        self.output_stream.add_error("Invalid version number".to_string());
464                        self.add_message("Invalid version number".to_string(), MessageType::Error);
465                    }
466                } else {
467                    self.output_stream.add_error("Usage: /rollback <version>".to_string());
468                    self.add_message("Usage: /rollback <version>".to_string(), MessageType::Error);
469                }
470            }
471            "/status" => {
472                self.view = View::MigrationList;
473                self.refresh_data();
474            }
475            "/logs" => {
476                self.view = View::Logs;
477            }
478            "/view" => {
479                if parts.len() > 1 {
480                    if let Ok(version) = parts[1].parse::<i64>() {
481                        let file_type = if parts.len() > 2 && parts[2] == "down" {
482                            MigrationFileType::Down
483                        } else {
484                            MigrationFileType::Up
485                        };
486                        
487                        let migrations_dir = std::path::PathBuf::from(&self.config.migrations.directory);
488                        let viewer = MigrationViewer::new(migrations_dir);
489                        
490                        match viewer.view_migration(version, file_type, &mut self.output_stream) {
491                            Ok(content) => {
492                                let title = format!("Migration {} ({})", version, if matches!(file_type, MigrationFileType::Up) { "up" } else { "down" });
493                                self.migration_content_view.show_content(title, content);
494                            }
495                            Err(e) => {
496                                self.output_stream.add_error(format!("Failed to view migration: {}", e));
497                                self.add_message(format!("Failed to view migration: {}", e), MessageType::Error);
498                            }
499                        }
500                    } else {
501                        self.output_stream.add_error("Invalid version number".to_string());
502                    }
503                } else {
504                    self.output_stream.add_error("Usage: /view <version> [up|down]".to_string());
505                    self.add_message("Usage: /view <version> [up|down]".to_string(), MessageType::Error);
506                }
507            }
508            "/edit" => {
509                if parts.len() > 1 {
510                    if let Ok(version) = parts[1].parse::<i64>() {
511                        let file_type = if parts.len() > 2 && parts[2] == "down" {
512                            MigrationFileType::Down
513                        } else {
514                            MigrationFileType::Up
515                        };
516                        
517                        let migrations_dir = std::path::PathBuf::from(&self.config.migrations.directory);
518                        let viewer = MigrationViewer::new(migrations_dir);
519                        
520                        self.output_stream.add_info("Launching editor...".to_string());
521                        
522                        // Note: This will block the TUI until editor closes
523                        // In a real implementation, you might want to save state and restore after
524                        match viewer.edit_migration(version, file_type, &mut self.output_stream) {
525                            Ok(_) => {
526                                self.output_stream.add_success("Editor closed".to_string());
527                                self.add_message("Migration edited successfully".to_string(), MessageType::Success);
528                            }
529                            Err(e) => {
530                                self.output_stream.add_error(format!("Failed to edit migration: {}", e));
531                                self.add_message(format!("Failed to edit migration: {}", e), MessageType::Error);
532                            }
533                        }
534                    } else {
535                        self.output_stream.add_error("Invalid version number".to_string());
536                    }
537                } else {
538                    self.output_stream.add_error("Usage: /edit <version> [up|down]".to_string());
539                    self.add_message("Usage: /edit <version> [up|down]".to_string(), MessageType::Error);
540                }
541            }
542            _ => {
543                self.output_stream.add_error(format!("Unknown command: {}", parts[0]));
544                self.add_message(format!("Unknown command: {}", parts[0]), MessageType::Error);
545            }
546        }
547        
548        Ok(())
549    }
550    
551    fn handle_view_key(&mut self, key: KeyEvent) -> Result<bool> {
552        match &self.view {
553            View::MigrationList => {
554                // Handle migration list keys
555                match key.code {
556                    KeyCode::Up | KeyCode::Char('k') => {
557                        self.migration_list.previous();
558                    }
559                    KeyCode::Down | KeyCode::Char('j') => {
560                        self.migration_list.next();
561                    }
562                    KeyCode::Enter => {
563                        if let Some(version) = self.migration_list.get_selected_version() {
564                            self.view = View::MigrationDetail { version };
565                        }
566                    }
567                    KeyCode::Char('r') => {
568                        self.add_message("Refreshing migration list...".to_string(), MessageType::Info);
569                        self.refresh_data();
570                    }
571                    KeyCode::Char('a') => {
572                        let pending_count = self.migration_list.get_pending_count();
573                        if pending_count > 0 {
574                            self.add_message(
575                                format!("Running {} pending migrations...", pending_count),
576                                MessageType::Info,
577                            );
578                            // TODO: Actually run migrations
579                        } else {
580                            self.add_message(
581                                "No pending migrations to run".to_string(),
582                                MessageType::Warning,
583                            );
584                        }
585                    }
586                    _ => {}
587                }
588            }
589            View::MigrationDetail { .. } => {
590                // Handle migration detail keys
591                match key.code {
592                    KeyCode::Esc | KeyCode::Char('q') => {
593                        self.view = View::MigrationList;
594                    }
595                    KeyCode::Char('r') => {
596                        self.add_message("Running this migration...".to_string(), MessageType::Info);
597                        // TODO: Actually run the specific migration
598                    }
599                    KeyCode::Char('b') => {
600                        self.add_message("Rolling back to before this migration...".to_string(), MessageType::Info);
601                        // TODO: Actually rollback
602                    }
603                    _ => {}
604                }
605            }
606            _ => {}
607        }
608        Ok(false)
609    }
610    
611    pub fn tick(&mut self) {
612        // Update any time-based state
613    }
614    
615    pub fn draw(&mut self, f: &mut Frame) {
616        // Set background color
617        f.render_widget(
618            Block::default().style(Style::default().bg(ModernTheme::BG_PRIMARY)),
619            f.area(),
620        );
621        
622        let chunks = Layout::default()
623            .direction(Direction::Vertical)
624            .constraints([
625                Constraint::Length(3),  // Header
626                Constraint::Min(10),    // Main content area
627                Constraint::Length(3),  // Status bar / Command input
628            ])
629            .split(f.area());
630        
631        // Render header
632        render_header(f, chunks[0], &self.database_url);
633        
634        // Split main content area into two columns
635        let main_chunks = Layout::default()
636            .direction(Direction::Horizontal)
637            .constraints([
638                Constraint::Percentage(50),  // Left panel (migrations/config)
639                Constraint::Percentage(50),  // Right panel (output stream)
640            ])
641            .split(chunks[1]);
642        
643        // Render left panel
644        match &self.view {
645            View::MigrationList => self.migration_list.render(f, main_chunks[0]),
646            View::MigrationDetail { version } => self.migration_detail.render(f, main_chunks[0], *version),
647            View::DatabaseConfig => self.render_database_config(f, main_chunks[0]),
648            View::Logs => {
649                // In logs view, use full width for output stream
650                self.output_stream.render(f, chunks[1], "Output Stream");
651            }
652        }
653        
654        // Render output stream in right panel (except in logs view)
655        if !matches!(self.view, View::Logs) {
656            self.output_stream.render(f, main_chunks[1], "Output");
657        }
658        
659        // Render status bar or command input
660        match self.mode {
661            AppMode::CommandInput => {
662                self.command_input.render(f, chunks[2]);
663            }
664            _ => {
665                render_status_bar(f, chunks[2], &self.view, &self.mode);
666            }
667        }
668        
669        // Render help overlay if in help mode
670        if self.mode == AppMode::Help {
671            self.help_view.render(f, f.area());
672        }
673        
674        // Render migration content view if visible
675        if self.migration_content_view.is_visible() {
676            let area = centered_rect(80, 80, f.area());
677            self.migration_content_view.render(f, area);
678        }
679    }
680    
681    fn render_messages(&self, _f: &mut Frame, _area: Rect) {
682        // Removed - using output stream instead
683    }
684    
685    fn render_database_config(&self, f: &mut Frame, area: Rect) {
686        let config_text = vec![
687            Line::from(vec![
688                Span::raw("Database URL: "),
689                Span::styled(
690                    self.database_url.as_deref().unwrap_or("Not configured"),
691                    Style::default().fg(Color::Yellow)
692                ),
693            ]),
694            Line::from(""),
695            Line::from("Migration Settings:"),
696            Line::from(format!("  Directory: {}", self.config.migrations.directory)),
697            Line::from(format!("  Table Name: {}", self.config.migrations.table_name)),
698            Line::from(format!("  Transaction per migration: {}", self.config.migrations.transaction_per_migration)),
699            Line::from(format!("  Verify checksums: {}", self.config.migrations.verify_checksums)),
700        ];
701        
702        let paragraph = Paragraph::new(config_text)
703            .block(Block::default().borders(Borders::ALL).title("Database Configuration"))
704            .wrap(Wrap { trim: true });
705        
706        f.render_widget(paragraph, area);
707    }
708    
709    fn render_logs(&self, f: &mut Frame, area: Rect) {
710        let logs_text = self.messages
711            .iter()
712            .map(|(msg, msg_type)| {
713                let prefix = match msg_type {
714                    MessageType::Info => "[INFO] ",
715                    MessageType::Success => "[SUCCESS] ",
716                    MessageType::Warning => "[WARN] ",
717                    MessageType::Error => "[ERROR] ",
718                };
719                Line::from(format!("{}{}", prefix, msg))
720            })
721            .collect::<Vec<_>>();
722        
723        let paragraph = Paragraph::new(logs_text)
724            .block(Block::default().borders(Borders::ALL).title("Logs"))
725            .wrap(Wrap { trim: true });
726        
727        f.render_widget(paragraph, area);
728    }
729}
730
731/// Helper function to create a centered rect
732fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
733    let popup_layout = Layout::default()
734        .direction(Direction::Vertical)
735        .constraints([
736            Constraint::Percentage((100 - percent_y) / 2),
737            Constraint::Percentage(percent_y),
738            Constraint::Percentage((100 - percent_y) / 2),
739        ])
740        .split(r);
741
742    Layout::default()
743        .direction(Direction::Horizontal)
744        .constraints([
745            Constraint::Percentage((100 - percent_x) / 2),
746            Constraint::Percentage(percent_x),
747            Constraint::Percentage((100 - percent_x) / 2),
748        ])
749        .split(popup_layout[1])[1]
750}