arct_tui/
app.rs

1//! Main application state and logic
2
3use crate::events::{Action, Event, EventHandler, key_to_action};
4use crate::icons;
5use crate::panels::PanelId;
6use crate::shell::ShellExecutor;
7use crate::theme::Theme;
8use crate::ui;
9use anyhow::Result;
10use arct_core::{CommandAnalyzer, Context, ContextDetector, Educator, Session};
11use crossterm::{
12    event::{KeyCode, KeyEvent, KeyModifiers},
13    execute,
14    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
15};
16use ratatui::{
17    backend::{Backend, CrosstermBackend},
18    Terminal,
19};
20use std::collections::HashMap;
21use std::io;
22
23/// Main application state
24pub struct App {
25    /// Whether the application should quit
26    pub should_quit: bool,
27
28    /// Currently active panel
29    pub active_panel: PanelId,
30
31    /// User session
32    pub session: Session,
33
34    /// Current context (project detection, etc.)
35    pub context: Context,
36
37    /// Command analyzer
38    pub analyzer: CommandAnalyzer,
39
40    /// Educator for explanations
41    pub educator: Educator,
42
43    /// Current theme
44    pub theme: Theme,
45
46    /// Event handler
47    event_handler: EventHandler,
48
49    /// Show help overlay
50    pub show_help: bool,
51
52    /// Command buffer for shell input
53    pub command_buffer: String,
54
55    /// Last command explanation
56    pub last_explanation: Option<arct_core::Explanation>,
57
58    /// Shell executor
59    shell_executor: ShellExecutor,
60
61    /// Last command output
62    pub last_output: String,
63
64    /// Output panel scroll offset
65    pub output_scroll: usize,
66
67    /// Command history
68    command_history: Vec<String>,
69
70    /// Current position in history (0 = most recent, None = not browsing)
71    history_position: Option<usize>,
72
73    /// Environment variables set by export command
74    pub environment_vars: HashMap<String, String>,
75
76    /// Command aliases (name -> command)
77    pub aliases: HashMap<String, String>,
78
79    /// Application configuration
80    pub config: arct_config::Config,
81
82    /// Autocompleter
83    autocompleter: crate::autocomplete::Autocompleter,
84
85    /// Current completion suggestions (shown below shell input)
86    pub completion_suggestions: Vec<String>,
87
88    /// AI assistant provider (if enabled)
89    ai_provider: Option<Box<dyn arct_ai::AIProvider>>,
90
91    /// AI conversation history
92    pub ai_conversation: Vec<arct_ai::Message>,
93
94    /// AI input buffer (when in AI mode)
95    pub ai_input_buffer: String,
96
97    /// Last AI response
98    pub ai_response: Option<String>,
99
100    /// AI loading state
101    pub ai_loading: bool,
102
103    /// AI mode enabled (toggle between shell and AI)
104    pub ai_mode: bool,
105
106    /// Onboarding wizard (shown on first run)
107    pub onboarding: Option<crate::panels::onboarding::OnboardingWizard>,
108
109    /// Settings panel (interactive)
110    pub settings_panel: Option<crate::panels::settings::SettingsPanel>,
111
112    /// Analytics tracker
113    pub analytics: Option<crate::analytics::Analytics>,
114
115    /// Current session ID
116    session_id: String,
117
118    /// Lesson panel for interactive lessons
119    pub lesson_panel: Option<crate::panels::lesson::LessonPanel>,
120
121    /// Lesson mode enabled (toggle between explanation and lesson)
122    pub lesson_mode: bool,
123
124    /// Virtual filesystem for lesson sandboxing
125    pub virtual_fs: Option<arct_core::VirtualFileSystem>,
126
127    /// Lesson menu for selecting lessons
128    pub lesson_menu: Option<crate::panels::lesson_menu::LessonMenuPanel>,
129
130    /// Completed lesson IDs
131    pub completed_lessons: std::collections::HashSet<String>,
132
133    /// User statistics and progress tracking
134    pub user_stats: arct_core::UserStats,
135
136    /// Challenge manager for daily/weekly challenges
137    pub challenge_manager: arct_core::ChallengeManager,
138
139    /// Recommendation engine for suggesting lessons
140    pub recommendation_engine: arct_core::RecommendationEngine,
141
142    /// Achievements panel
143    pub achievements_panel: Option<crate::panels::achievements::AchievementsPanel>,
144
145    /// Progress panel
146    pub progress_panel: Option<crate::panels::progress::ProgressPanel>,
147
148    /// Challenges panel
149    pub challenges_panel: Option<crate::panels::challenges::ChallengesPanel>,
150
151    /// Pending achievements to show notifications for
152    pub pending_achievements: Vec<arct_core::Achievement>,
153
154    /// Currently showing achievement notification
155    pub showing_notification: Option<crate::panels::notification::NotificationPanel>,
156}
157
158impl App {
159    /// Create a new application
160    pub fn new() -> Result<Self> {
161        let session = Session::new();
162        let working_dir = session.state.working_directory.clone();
163        let context = ContextDetector::detect(&working_dir)?;
164
165        // Load configuration
166        let config = arct_config::Config::load().unwrap_or_else(|e| {
167            tracing::warn!("Failed to load config, using defaults: {}", e);
168            arct_config::Config::default()
169        });
170
171        // Load session data (history, stats, progress) from disk
172        let session_data = match crate::persistence::load_session() {
173            Ok(data) => {
174                tracing::info!("Loaded session with {} commands, {} completed lessons",
175                    data.command_history.len(),
176                    data.completed_lessons.len()
177                );
178                data
179            }
180            Err(e) => {
181                tracing::warn!("Failed to load session: {}", e);
182                crate::persistence::SessionData::new()
183            }
184        };
185
186        let command_history = session_data.command_history;
187
188        // Load aliases and environment variables from config
189        let aliases = config.shell.aliases.clone();
190        let environment_vars = config.shell.environment.clone();
191
192        // Select theme based on config
193        let theme = Theme::from_name(&config.theme.default_theme);
194
195        // Initialize AI provider if enabled
196        let ai_provider = if config.ai.enabled {
197            match Self::create_ai_provider(&config.ai) {
198                Ok(provider) => {
199                    tracing::info!("AI provider initialized: {}", provider.name());
200                    Some(provider)
201                }
202                Err(e) => {
203                    tracing::warn!("Failed to initialize AI provider: {}", e);
204                    None
205                }
206            }
207        } else {
208            None
209        };
210
211        // Check if first run (show onboarding)
212        let onboarding = if !config.general.setup_complete {
213            Some(crate::panels::onboarding::OnboardingWizard::new())
214        } else {
215            None
216        };
217
218        // Create welcome message for returning users
219        let welcome_message = if config.general.setup_complete {
220            let name = config.general.user_name.as_deref().unwrap_or("there");
221            let mut msg = format!("{}Welcome back, {}!\n\n", icons::welcome().content, name);
222
223            // Add quick tips
224            msg.push_str("Quick reminders:\n");
225            if config.ai.enabled {
226                msg.push_str("  • Press Ctrl+A to ask the AI for help\n");
227            }
228            msg.push_str("  • Press ? for help\n");
229            msg.push_str("  • Press Ctrl+S for settings\n");
230            msg.push_str("  • Tab to autocomplete commands\n\n");
231            msg.push_str("Start typing a command to begin!\n");
232            msg
233        } else {
234            String::new()
235        };
236
237        // Load user stats from session and update streak
238        let mut user_stats = session_data.user_stats;
239        user_stats.update_streak(); // Update streak on app start
240
241        // Load challenge manager from session
242        let mut challenge_manager = session_data.challenge_manager;
243        // Generate today's challenges (will use cached if same day/week)
244        challenge_manager.get_daily_challenge();
245        challenge_manager.get_weekly_challenge();
246
247        // Load completed lessons from session
248        let completed_lessons = session_data.completed_lessons;
249
250        Ok(Self {
251            should_quit: false,
252            active_panel: PanelId::Shell,
253            session,
254            context,
255            analyzer: CommandAnalyzer::new(),
256            educator: Educator::new(),
257            theme,
258            event_handler: EventHandler::new(),
259            show_help: false,
260            command_buffer: String::new(),
261            last_explanation: None,
262            shell_executor: ShellExecutor::new()?,
263            last_output: welcome_message,
264            output_scroll: 0,
265            command_history,
266            history_position: None,
267            environment_vars,
268            aliases,
269            config,
270            autocompleter: crate::autocomplete::Autocompleter::new(),
271            completion_suggestions: Vec::new(),
272            ai_provider,
273            ai_conversation: Vec::new(),
274            ai_input_buffer: String::new(),
275            ai_response: None,
276            ai_loading: false,
277            ai_mode: false,
278            onboarding,
279            settings_panel: None,
280            analytics: crate::analytics::Analytics::new().ok(),
281            session_id: uuid::Uuid::new_v4().to_string(),
282            lesson_panel: Self::initialize_lesson_panel(),
283            lesson_mode: false,
284            virtual_fs: None,
285            lesson_menu: None,
286            completed_lessons,
287            user_stats,
288            challenge_manager,
289            recommendation_engine: arct_core::RecommendationEngine::new(),
290            achievements_panel: None,
291            progress_panel: None,
292            challenges_panel: None,
293            pending_achievements: Vec::new(),
294            showing_notification: None,
295        })
296    }
297
298    /// Initialize empty lesson panel (user will select lesson from menu)
299    fn initialize_lesson_panel() -> Option<crate::panels::lesson::LessonPanel> {
300        Some(crate::panels::lesson::LessonPanel::new())
301    }
302
303    /// Create AI provider from configuration
304    fn create_ai_provider(config: &arct_config::AIConfig) -> Result<Box<dyn arct_ai::AIProvider>> {
305        let ai_config = match config.provider.as_str() {
306            "anthropic" => {
307                let api_key = config.api_key.clone()
308                    .ok_or_else(|| anyhow::anyhow!("Anthropic API key not set"))?;
309                let model = config.model.clone()
310                    .unwrap_or_else(|| "claude-3-5-sonnet-20241022".to_string());
311                arct_ai::AIConfig::Anthropic { api_key, model }
312            }
313            "openai" => {
314                let api_key = config.api_key.clone()
315                    .ok_or_else(|| anyhow::anyhow!("OpenAI API key not set"))?;
316                let model = config.model.clone()
317                    .unwrap_or_else(|| "gpt-4-turbo-preview".to_string());
318                arct_ai::AIConfig::OpenAI { api_key, model }
319            }
320            "local" => {
321                let endpoint = config.endpoint.clone()
322                    .unwrap_or_else(|| "http://localhost:11434".to_string());
323                let model = config.model.clone();
324                arct_ai::AIConfig::Local { endpoint, model }
325            }
326            "managed" => {
327                let auth_token = config.api_key.clone()
328                    .ok_or_else(|| anyhow::anyhow!("Managed API token not set"))?;
329                arct_ai::AIConfig::Managed { auth_token }
330            }
331            "claude-cli" => {
332                // Claude Code CLI - no API key needed
333                let model = config.model.clone();
334                arct_ai::AIConfig::ClaudeCLI { model }
335            }
336            _ => arct_ai::AIConfig::Disabled,
337        };
338
339        arct_ai::AIFactory::create(&ai_config)
340            .map_err(|e| anyhow::anyhow!("Failed to create AI provider: {}", e))
341    }
342
343    /// Show ASCII art splash screen
344    fn show_splash_screen() -> Result<()> {
345        use crossterm::{
346            cursor,
347            style::{Color, Print, SetForegroundColor, ResetColor},
348            terminal::{Clear, ClearType},
349        };
350        use std::io::Write;
351
352        let mut stdout = io::stdout();
353
354        // Clear screen
355        execute!(stdout, Clear(ClearType::All), cursor::MoveTo(0, 0))?;
356
357        // ASCII art logo
358        let logo = r#"
359 ▄▄▄       ██▀███   ▄████▄      ▄▄▄       ▄████▄   ▄▄▄      ▓█████▄ ▓█████  ███▄ ▄███▓▓██   ██▓
360▒████▄    ▓██ ▒ ██▒▒██▀ ▀█     ▒████▄    ▒██▀ ▀█  ▒████▄    ▒██▀ ██▌▓█   ▀ ▓██▒▀█▀ ██▒ ▒██  ██▒
361▒██  ▀█▄  ▓██ ░▄█ ▒▒▓█    ▄    ▒██  ▀█▄  ▒▓█    ▄ ▒██  ▀█▄  ░██   █▌▒███   ▓██    ▓██░  ▒██ ██░
362░██▄▄▄▄██ ▒██▀▀█▄  ▒▓▓▄ ▄██▒   ░██▄▄▄▄██ ▒▓▓▄ ▄██▒░██▄▄▄▄██ ░▓█▄   ▌▒▓█  ▄ ▒██    ▒██   ░ ▐██▓░
363 ▓█   ▓██▒░██▓ ▒██▒▒ ▓███▀ ░    ▓█   ▓██▒▒ ▓███▀ ░ ▓█   ▓██▒░▒████▓ ░▒████▒▒██▒   ░██▒  ░ ██▒▓░
364 ▒▒   ▓▒█░░ ▒▓ ░▒▓░░ ░▒ ▒  ░    ▒▒   ▓▒█░░ ░▒ ▒  ░ ▒▒   ▓▒█░ ▒▒▓  ▒ ░░ ▒░ ░░ ▒░   ░  ░   ██▒▒▒
365  ▒   ▒▒ ░  ░▒ ░ ▒░  ░  ▒        ▒   ▒▒ ░  ░  ▒     ▒   ▒▒ ░ ░ ▒  ▒  ░ ░  ░░  ░      ░ ▓██ ░▒░
366  ░   ▒     ░░   ░ ░             ░   ▒   ░          ░   ▒    ░ ░  ░    ░   ░      ░    ▒ ▒ ░░
367      ░  ░   ░     ░ ░               ░  ░░ ░            ░  ░   ░       ░  ░       ░    ░ ░
368                   ░                     ░                   ░                         ░ ░
369"#;
370
371        let tagline = "Λ° Learn Shell Commands Interactively with AI";
372        let version = format!("v{}", env!("CARGO_PKG_VERSION"));
373
374        // Get terminal size for centering
375        let (width, height) = crossterm::terminal::size()?;
376        let start_row = (height / 2).saturating_sub(7); // Center vertically
377
378        // Calculate logo width (longest line is ~92 chars)
379        let logo_width = 92;
380        let logo_col = (width / 2).saturating_sub(logo_width / 2);
381
382        // Print logo with orange color, centered
383        for (i, line) in logo.lines().enumerate() {
384            let row = start_row + i as u16;
385            execute!(
386                stdout,
387                cursor::MoveTo(logo_col, row),
388                SetForegroundColor(Color::Rgb { r: 255, g: 140, b: 0 }), // Arc Academy Orange
389                Print(line),
390                ResetColor
391            )?;
392        }
393
394        // Print tagline centered below logo
395        let tagline_row = start_row + 11;
396        let tagline_col = (width / 2).saturating_sub((tagline.len() / 2) as u16);
397        execute!(
398            stdout,
399            cursor::MoveTo(tagline_col, tagline_row),
400            SetForegroundColor(Color::White),
401            Print(tagline),
402            ResetColor
403        )?;
404
405        // Print version
406        let version_row = tagline_row + 1;
407        let version_col = (width / 2).saturating_sub((version.len() / 2) as u16);
408        execute!(
409            stdout,
410            cursor::MoveTo(version_col, version_row),
411            SetForegroundColor(Color::DarkGrey),
412            Print(version),
413            ResetColor
414        )?;
415
416        stdout.flush()?;
417
418        // Pause for 1.5 seconds
419        std::thread::sleep(std::time::Duration::from_millis(1500));
420
421        // Clear screen before entering TUI
422        execute!(stdout, Clear(ClearType::All), cursor::MoveTo(0, 0))?;
423
424        Ok(())
425    }
426
427    /// Run the application
428    pub async fn run(&mut self) -> Result<()> {
429        // Show splash screen
430        Self::show_splash_screen()?;
431
432        // Setup terminal
433        enable_raw_mode()?;
434        let mut stdout = io::stdout();
435        execute!(stdout, EnterAlternateScreen)?;
436        let backend = CrosstermBackend::new(stdout);
437        let mut terminal = Terminal::new(backend)?;
438
439        // Start event handler
440        self.event_handler.start().await;
441
442        // Main loop
443        let result = self.main_loop(&mut terminal).await;
444
445        // Save all progress before exiting
446        self.save_session();
447
448        // Restore terminal
449        disable_raw_mode()?;
450        execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
451        terminal.show_cursor()?;
452
453        result
454    }
455
456    /// Main application loop
457    async fn main_loop<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> Result<()> {
458        loop {
459            // Draw UI
460            terminal.draw(|f| ui::draw(f, self))?;
461
462            // Handle events
463            if let Some(event) = self.event_handler.next().await {
464                self.handle_event(event).await?;
465            }
466
467            // Check for quit
468            if self.should_quit {
469                break;
470            }
471        }
472
473        Ok(())
474    }
475
476    /// Handle an event
477    async fn handle_event(&mut self, event: Event) -> Result<()> {
478        match event {
479            Event::Key(key) => {
480                // If onboarding is active, handle onboarding events
481                if self.onboarding.is_some() {
482                    return self.handle_onboarding_event(key).await;
483                }
484
485                // If settings panel is open, handle settings events
486                if self.settings_panel.is_some() {
487                    return self.handle_settings_event(key).await;
488                }
489
490                // If lesson menu is open, handle menu events
491                if let Some(ref mut menu) = self.lesson_menu {
492                    match key.code {
493                        KeyCode::Up | KeyCode::Char('k') => {
494                            menu.select_previous();
495                            return Ok(());
496                        }
497                        KeyCode::Down | KeyCode::Char('j') => {
498                            menu.select_next();
499                            return Ok(());
500                        }
501                        KeyCode::Char(c) if c.is_ascii_digit() => {
502                            if let Some(digit) = c.to_digit(10) {
503                                // Map 1-9 to lessons 1-9, and 0 to lesson 10
504                                let lesson_num = if digit == 0 { 10 } else { digit as usize };
505                                menu.select_by_number(lesson_num);
506                            }
507                            return Ok(());
508                        }
509                        KeyCode::Enter => {
510                            // Load selected lesson
511                            if let Some(lesson) = menu.get_selected_lesson() {
512                                if let Some(ref mut panel) = self.lesson_panel {
513                                    panel.load_lesson(lesson);
514                                    self.lesson_menu = None;
515                                    self.last_output = format!("{}Lesson loaded! Follow the instructions in the lesson panel.\n", icons::lesson().content);
516                                }
517                            }
518                            return Ok(());
519                        }
520                        KeyCode::Esc | KeyCode::Char('q') => {
521                            self.lesson_menu = None;
522                            return Ok(());
523                        }
524                        _ => {}
525                    }
526                }
527
528                // If in AI mode and in Shell panel, handle AI input
529                if self.ai_mode && self.active_panel == PanelId::Shell && !self.show_help {
530                    match key.code {
531                        KeyCode::Char(c) if key.modifiers == KeyModifiers::NONE || key.modifiers == KeyModifiers::SHIFT => {
532                            self.ai_input_buffer.push(c);
533                            return Ok(());
534                        }
535                        KeyCode::Backspace => {
536                            self.ai_input_buffer.pop();
537                            return Ok(());
538                        }
539                        KeyCode::Enter => {
540                            if !self.ai_input_buffer.is_empty() {
541                                let question = self.ai_input_buffer.clone();
542                                self.ai_input_buffer.clear();
543                                self.ask_ai(question).await?;
544                            }
545                            return Ok(());
546                        }
547                        _ => {}
548                    }
549                }
550
551                // If in shell panel and not a special action, handle as text input or history
552                if self.active_panel == PanelId::Shell && !self.show_help && !self.ai_mode {
553                    match key.code {
554                        KeyCode::Char(c) if key.modifiers == KeyModifiers::NONE || key.modifiers == KeyModifiers::SHIFT => {
555                            self.command_buffer.push(c);
556                            // Reset history position when typing
557                            self.history_position = None;
558                            // Clear completion suggestions when typing
559                            self.completion_suggestions.clear();
560                            return Ok(());
561                        }
562                        KeyCode::Backspace => {
563                            self.command_buffer.pop();
564                            // Reset history position when editing
565                            self.history_position = None;
566                            // Clear completion suggestions when editing
567                            self.completion_suggestions.clear();
568                            return Ok(());
569                        }
570                        KeyCode::Tab if key.modifiers == KeyModifiers::NONE => {
571                            // Tab in shell panel triggers autocomplete only if there's input
572                            // Otherwise, fall through to panel cycling
573                            if !self.command_buffer.is_empty() {
574                                self.handle_autocomplete()?;
575                                return Ok(());
576                            }
577                            // Empty buffer - let Tab fall through to cycle panels
578                        }
579                        KeyCode::Up => {
580                            // Navigate backward in history (older commands)
581                            self.history_previous();
582                            return Ok(());
583                        }
584                        KeyCode::Down => {
585                            // Navigate forward in history (newer commands)
586                            self.history_next();
587                            return Ok(());
588                        }
589                        _ => {}
590                    }
591                }
592
593                // Handle as action
594                let action = key_to_action(key);
595                self.handle_action(action).await?;
596            }
597            Event::Resize(_, _) => {
598                // Terminal will automatically redraw on next iteration
599            }
600            Event::Tick => {
601                // Update any time-based state here
602            }
603            Event::Quit => {
604                self.should_quit = true;
605            }
606        }
607
608        Ok(())
609    }
610
611    /// Handle an action
612    async fn handle_action(&mut self, action: Action) -> Result<()> {
613        match action {
614            Action::Quit => {
615                if self.show_help {
616                    self.show_help = false;
617                } else {
618                    self.should_quit = true;
619                }
620            }
621            Action::NextPanel => {
622                self.active_panel = self.active_panel.next();
623                // Don't reset scroll - users may want to keep reading output
624            }
625            Action::PreviousPanel => {
626                self.active_panel = self.active_panel.previous();
627                // Don't reset scroll - users may want to keep reading output
628            }
629            Action::ScrollUp => {
630                // Only scroll when Output panel is focused
631                if self.active_panel == PanelId::Output {
632                    self.output_scroll = self.output_scroll.saturating_sub(1);
633                }
634            }
635            Action::ScrollDown => {
636                // Only scroll when Output panel is focused
637                if self.active_panel == PanelId::Output {
638                    let total_lines = self.last_output.lines().count();
639                    if self.output_scroll < total_lines.saturating_sub(1) {
640                        self.output_scroll += 1;
641                    }
642                }
643            }
644            Action::PageUp => {
645                // Only scroll when Output panel is focused
646                if self.active_panel == PanelId::Output {
647                    self.output_scroll = self.output_scroll.saturating_sub(10);
648                }
649            }
650            Action::PageDown => {
651                // Only scroll when Output panel is focused
652                if self.active_panel == PanelId::Output {
653                    let total_lines = self.last_output.lines().count();
654                    self.output_scroll = (self.output_scroll + 10).min(total_lines.saturating_sub(1));
655                }
656            }
657            Action::ScrollOutputUp => {
658                // Always scroll output regardless of which panel is focused
659                self.output_scroll = self.output_scroll.saturating_sub(1);
660            }
661            Action::ScrollOutputDown => {
662                // Always scroll output regardless of which panel is focused
663                let total_lines = self.last_output.lines().count();
664                if self.output_scroll < total_lines.saturating_sub(1) {
665                    self.output_scroll += 1;
666                }
667            }
668            Action::Help => {
669                self.show_help = !self.show_help;
670            }
671            Action::ToggleTheme => {
672                self.theme = self.theme.cycle_next();
673            }
674            Action::ToggleAI => {
675                self.toggle_ai_mode();
676            }
677            Action::ToggleSettings => {
678                if self.settings_panel.is_some() {
679                    self.settings_panel = None;
680                } else {
681                    self.settings_panel = Some(crate::panels::settings::SettingsPanel::new());
682                }
683            }
684            Action::ToggleLesson => {
685                self.toggle_lesson_mode();
686            }
687            Action::ShowLessonMenu => {
688                if self.lesson_mode {
689                    // Toggle menu on/off
690                    if self.lesson_menu.is_some() {
691                        self.lesson_menu = None;
692                    } else {
693                        self.lesson_menu = Some(crate::panels::lesson_menu::LessonMenuPanel::new());
694                    }
695                }
696            }
697            Action::Escape => {
698                if self.showing_notification.is_some() {
699                    self.dismiss_notification();
700                } else if self.show_help {
701                    self.show_help = false;
702                } else if self.achievements_panel.is_some() {
703                    self.achievements_panel = None;
704                } else if self.progress_panel.is_some() {
705                    self.progress_panel = None;
706                } else if self.challenges_panel.is_some() {
707                    self.challenges_panel = None;
708                } else if self.settings_panel.is_some() {
709                    self.settings_panel = None;
710                } else if self.lesson_menu.is_some() {
711                    self.lesson_menu = None;
712                } else if self.ai_mode {
713                    self.ai_mode = false;
714                }
715            }
716            Action::Enter => {
717                if self.showing_notification.is_some() {
718                    self.dismiss_notification();
719                } else if !self.ai_mode {
720                    self.execute_command().await?;
721                }
722            }
723            Action::ShowAchievements => {
724                self.toggle_achievements_panel();
725            }
726            Action::ShowProgress => {
727                self.toggle_progress_panel();
728            }
729            Action::ShowChallenges => {
730                self.toggle_challenges_panel();
731            }
732            Action::DismissNotification => {
733                self.dismiss_notification();
734            }
735            _ => {}
736        }
737
738        Ok(())
739    }
740
741    /// Helper: Check if a flag exists in the command
742    fn has_flag(cmd: &arct_core::Command, flag: &str) -> bool {
743        cmd.flags.iter().any(|f| {
744            f.raw == flag ||
745            f.short == Some(flag.chars().nth(1).unwrap_or(' ')) ||
746            f.long.as_ref().map(|l| l == flag.trim_start_matches("--")).unwrap_or(false)
747        })
748    }
749
750    /// Execute command against virtual filesystem and return output
751    fn execute_virtual_fs_command(&mut self, cmd: &arct_core::Command) -> Option<String> {
752        if !self.lesson_mode {
753            return None;
754        }
755
756        let vfs = self.virtual_fs.as_mut()?;
757        let program = cmd.program.as_str();
758
759        match program {
760            "pwd" => {
761                let current = vfs.get_current_dir().display().to_string();
762                Some(format!("{}\n", current))
763            }
764
765            "ls" => {
766                // Check for -a flag (show hidden files)
767                let show_hidden = Self::has_flag(cmd, "-a") || Self::has_flag(cmd, "--all");
768
769                match vfs.list_directory(None) {
770                    Ok(mut entries) => {
771                        // Filter out hidden files if -a not specified
772                        if !show_hidden {
773                            entries.retain(|e| !e.name.starts_with('.'));
774                        }
775
776                        let mut output = String::new();
777                        for entry in entries {
778                            if entry.is_dir {
779                                output.push_str(&format!("{}{}/\n", icons::folder().content, entry.name));
780                            } else {
781                                output.push_str(&format!("{}{}\n", icons::file().content, entry.name));
782                            }
783                        }
784
785                        if output.is_empty() {
786                            output = "Empty directory\n".to_string();
787                        }
788
789                        Some(output)
790                    }
791                    Err(e) => Some(format!("{}ls: {}\n", icons::error().content, e)),
792                }
793            }
794
795            "cd" => {
796                let target = cmd.args.first().map(|s| s.as_str()).unwrap_or("~");
797                match vfs.change_directory(target) {
798                    Ok(new_path) => {
799                        Some(format!("{}Changed to: {}\n", icons::folder().content, new_path))
800                    }
801                    Err(e) => Some(format!("{}cd: {}\n", icons::error().content, e)),
802                }
803            }
804
805            "cat" => {
806                if cmd.args.is_empty() {
807                    return Some(format!("{}cat: missing file argument\n", icons::error().content));
808                }
809
810                let mut output = String::new();
811                for file in &cmd.args {
812                    match vfs.read_file(file) {
813                        Ok(content) => output.push_str(&content),
814                        Err(e) => output.push_str(&format!("{}cat: {}\n", icons::error().content, e)),
815                    }
816                }
817                Some(output)
818            }
819
820            "mkdir" => {
821                if cmd.args.is_empty() {
822                    return Some(format!("{}mkdir: missing directory name\n", icons::error().content));
823                }
824
825                let parents = Self::has_flag(cmd, "-p") || Self::has_flag(cmd, "--parents");
826                let mut output = String::new();
827
828                for dir in &cmd.args {
829                    match vfs.create_directory(dir, parents) {
830                        Ok(_) => output.push_str(&format!("{}Created directory: {}\n", icons::folder().content, dir)),
831                        Err(e) => output.push_str(&format!("{}mkdir: {}\n", icons::error().content, e)),
832                    }
833                }
834                Some(output)
835            }
836
837            "touch" => {
838                if cmd.args.is_empty() {
839                    return Some(format!("{}touch: missing file argument\n", icons::error().content));
840                }
841
842                let mut output = String::new();
843                for file in &cmd.args {
844                    match vfs.touch_file(file) {
845                        Ok(_) => output.push_str(&format!("{}Created/updated file: {}\n", icons::file().content, file)),
846                        Err(e) => output.push_str(&format!("{}touch: {}\n", icons::error().content, e)),
847                    }
848                }
849                Some(output)
850            }
851
852            "rm" => {
853                if cmd.args.is_empty() {
854                    return Some(format!("{}rm: missing file argument\n", icons::error().content));
855                }
856
857                let recursive = Self::has_flag(cmd, "-r") || Self::has_flag(cmd, "-R") || Self::has_flag(cmd, "--recursive");
858                let force = Self::has_flag(cmd, "-f") || Self::has_flag(cmd, "--force");
859                let mut output = String::new();
860
861                for item in &cmd.args {
862                    match vfs.remove(item, recursive, force) {
863                        Ok(_) => output.push_str(&format!("{}Removed: {}\n", icons::success().content, item)),
864                        Err(e) => output.push_str(&format!("{}rm: {}\n", icons::error().content, e)),
865                    }
866                }
867                Some(output)
868            }
869
870            "mv" => {
871                if cmd.args.len() < 2 {
872                    return Some(format!("{}mv: missing source or destination\n", icons::error().content));
873                }
874
875                let source = &cmd.args[0];
876                let dest = &cmd.args[1];
877
878                match vfs.move_item(source, dest) {
879                    Ok(_) => Some(format!("{}Moved {} to {}\n", icons::success().content, source, dest)),
880                    Err(e) => Some(format!("{}mv: {}\n", icons::error().content, e)),
881                }
882            }
883
884            "cp" => {
885                if cmd.args.len() < 2 {
886                    return Some(format!("{}cp: missing source or destination\n", icons::error().content));
887                }
888
889                let recursive = Self::has_flag(cmd, "-r") || Self::has_flag(cmd, "-R") || Self::has_flag(cmd, "--recursive");
890                let source = &cmd.args[0];
891                let dest = &cmd.args[1];
892
893                match vfs.copy(source, dest, recursive) {
894                    Ok(_) => Some(format!("{}Copied {} to {}\n", icons::success().content, source, dest)),
895                    Err(e) => Some(format!("{}cp: {}\n", icons::error().content, e)),
896                }
897            }
898
899            // Commands that don't manipulate the filesystem
900            _ => None,
901        }
902    }
903
904    /// Execute the current command
905    async fn execute_command(&mut self) -> Result<()> {
906        // In lesson mode, allow empty commands (for Information steps that just need Enter)
907        if self.command_buffer.is_empty() && !self.lesson_mode {
908            return Ok(());
909        }
910
911        let mut command_str = self.command_buffer.clone();
912
913        // If in lesson mode with empty command, skip parsing and go to validation
914        if self.lesson_mode && command_str.is_empty() {
915            // Extract lesson completion info outside the borrow scope
916            let mut lesson_completed_info: Option<(String, arct_core::Difficulty)> = None;
917
918            if let Some(ref mut lesson_panel) = self.lesson_panel {
919                let validation = lesson_panel.validate_current_step(&command_str);
920
921                if validation.is_success() {
922                    // Success! Move to next step
923                    self.last_output = format!("{}{}\n\nMoving to next step...\n",
924                        icons::success().content,
925                        match &validation {
926                            arct_core::ValidationResult::Success { message } => message,
927                            _ => "Success!",
928                        }
929                    );
930
931                    if !lesson_panel.next_step() {
932                        // Lesson complete! Extract info for later processing
933                        if let Some(lesson) = lesson_panel.current_lesson.as_ref() {
934                            lesson_completed_info = Some((lesson.id.clone(), lesson.difficulty));
935                        }
936                        self.last_output.push_str(&format!("\n{}Congratulations! You've completed this lesson!\n\nPress Ctrl+L to exit lesson mode or 'm' to select another lesson.\n", icons::celebration().content));
937                    }
938                } else {
939                    // Information steps should always succeed with Enter
940                    self.last_output = "Press Enter to continue...\n".to_string();
941                }
942
943                self.command_buffer.clear();
944                self.add_to_history(command_str.clone());
945            }
946
947            // Process lesson completion outside the borrow scope
948            if let Some((lesson_id, difficulty)) = lesson_completed_info {
949                self.record_lesson_completion(lesson_id, difficulty);
950            }
951
952            return Ok(());
953        }
954
955        // Parse command
956        let cmd = self.analyzer.parse(&command_str)?;
957
958        // If in lesson mode, execute against virtual FS and validate
959        if self.lesson_mode {
960            // Execute command against virtual filesystem and get output
961            let vfs_output = self.execute_virtual_fs_command(&cmd);
962
963            // Extract lesson completion info outside the borrow scope
964            let mut lesson_completed_info: Option<(String, arct_core::Difficulty)> = None;
965
966            if let Some(ref mut lesson_panel) = self.lesson_panel {
967                let validation = lesson_panel.validate_current_step(&command_str);
968
969                // Build output: virtual FS output FIRST, then validation feedback
970                let mut output = String::new();
971
972                // Show the command output from virtual filesystem
973                if let Some(vfs_out) = vfs_output {
974                    output.push_str(&vfs_out);
975                    output.push('\n');
976                }
977
978                // Then show validation feedback
979                if validation.is_success() {
980                    // Success! Move to next step
981                    output.push_str(&format!("{}{}\n\n",
982                        icons::success().content,
983                        match &validation {
984                            arct_core::ValidationResult::Success { message } => message,
985                            _ => "Correct!",
986                        }
987                    ));
988
989                    if !lesson_panel.next_step() {
990                        // Lesson complete! Extract info for later processing
991                        if let Some(lesson) = lesson_panel.current_lesson.as_ref() {
992                            lesson_completed_info = Some((lesson.id.clone(), lesson.difficulty));
993                        }
994                        output.push_str(&format!("{}Congratulations! You've completed this lesson!\n\nPress Ctrl+L to exit lesson mode or 'm' to select another lesson.\n", icons::celebration().content));
995                    } else {
996                        output.push_str("Moving to next step...\n");
997                    }
998                } else {
999                    // Show validation failure
1000                    output.push_str(&match validation {
1001                        arct_core::ValidationResult::Failure { message, hint } => {
1002                            let mut fail_output = format!("{}{}\n", icons::error().content, message);
1003                            if let Some(h) = hint {
1004                                fail_output.push_str(&format!("\n{}Hint: {}\n", icons::hint().content, h));
1005                            }
1006                            fail_output.push_str("\nTry again!\n");
1007                            fail_output
1008                        }
1009                        arct_core::ValidationResult::Partial { message, progress } => {
1010                            format!("{}{} ({:.0}% correct)\n\nKeep trying!\n", icons::warning().content, message, progress)
1011                        }
1012                        _ => "Try again!\n".to_string(),
1013                    });
1014                }
1015
1016                self.last_output = output;
1017                self.command_buffer.clear();
1018                self.add_to_history(command_str.clone());
1019            }
1020
1021            // Process lesson completion outside the borrow scope
1022            if let Some((lesson_id, difficulty)) = lesson_completed_info {
1023                self.record_lesson_completion(lesson_id, difficulty);
1024            }
1025
1026            return Ok(());
1027        }
1028
1029        // Generate explanation
1030        let explanation = self.educator.explain(&cmd)?;
1031        self.last_explanation = Some(explanation);
1032
1033        // Check if this is a shell builtin command
1034        match cmd.program.as_str() {
1035            "cd" => {
1036                // Add to history before clearing buffer
1037                self.add_to_history(command_str.clone());
1038                self.handle_cd_command(&cmd)?;
1039                self.command_buffer.clear();
1040                return Ok(());
1041            }
1042            "history" => {
1043                // Add to history before clearing buffer
1044                self.add_to_history(command_str.clone());
1045                self.handle_history_command(&cmd)?;
1046                self.command_buffer.clear();
1047                return Ok(());
1048            }
1049            "export" => {
1050                // Add to history before clearing buffer
1051                self.add_to_history(command_str.clone());
1052                self.handle_export_command(&cmd)?;
1053                self.command_buffer.clear();
1054                return Ok(());
1055            }
1056            "alias" => {
1057                // Add to history before clearing buffer
1058                self.add_to_history(command_str.clone());
1059                self.handle_alias_command(&cmd)?;
1060                self.command_buffer.clear();
1061                return Ok(());
1062            }
1063            _ => {
1064                // Check if the command is an alias and expand it
1065                if let Some(aliased_command) = self.aliases.get(cmd.program.as_str()) {
1066                    // Replace the alias with the full command
1067                    let args_str = if !cmd.args.is_empty() {
1068                        format!(" {}", cmd.args.join(" "))
1069                    } else {
1070                        String::new()
1071                    };
1072                    command_str = format!("{}{}", aliased_command, args_str);
1073                }
1074            }
1075        }
1076
1077        // Show executing status
1078        self.last_output = format!("{}Executing: {}\n", icons::loading().content, command_str);
1079
1080        // Execute the command for real with timeout
1081        let start_time = std::time::Instant::now();
1082
1083        // Use tokio timeout to prevent hanging
1084        let timeout_duration = std::time::Duration::from_secs(5);
1085        let env_vars = self.environment_vars.clone();
1086        let output_result = tokio::time::timeout(
1087            timeout_duration,
1088            self.shell_executor.execute(command_str.clone(), env_vars)
1089        ).await;
1090
1091        let output = match output_result {
1092            Ok(Ok(output)) => output,
1093            Ok(Err(e)) => format!("{}Error: {}", icons::error().content, e),
1094            Err(_) => format!("Command timed out after {} seconds", timeout_duration.as_secs()),
1095        };
1096
1097        let duration = start_time.elapsed();
1098
1099        // Store output
1100        self.last_output = output.clone();
1101
1102        // Determine if command was successful
1103        let success = !output.starts_with(icons::error().content.as_ref()) && !output.contains("timed out");
1104
1105        // Reset scroll to top for new output
1106        self.output_scroll = 0;
1107
1108        // Record in session
1109        self.session.record_command(
1110            command_str.clone(),
1111            Some(0),
1112            Some(duration.as_millis() as u64),
1113        );
1114
1115        // Track in analytics database
1116        if let Some(ref analytics) = self.analytics {
1117            let working_dir = self.session.state.working_directory.to_string_lossy().to_string();
1118            let _ = analytics.record_command(
1119                &command_str,
1120                success,
1121                &working_dir,
1122                &self.session_id,
1123            );
1124        }
1125
1126        // Add to history
1127        self.add_to_history(command_str.clone());
1128
1129        // Track command use for stats and achievements
1130        self.record_command_for_stats(&command_str);
1131        self.check_and_unlock_achievements();
1132
1133        // Clear buffer
1134        self.command_buffer.clear();
1135
1136        Ok(())
1137    }
1138
1139    /// Handle cd command specially (it's a shell builtin)
1140    fn handle_cd_command(&mut self, cmd: &arct_core::Command) -> Result<()> {
1141        use std::path::PathBuf;
1142
1143        // If in lesson mode, use virtual filesystem
1144        if self.lesson_mode {
1145            if let Some(ref mut vfs) = self.virtual_fs {
1146                let target_str = if cmd.args.is_empty() {
1147                    "~"
1148                } else {
1149                    &cmd.args[0]
1150                };
1151
1152                match vfs.change_directory(target_str) {
1153                    Ok(new_path) => {
1154                        self.last_output = format!(
1155                            "{}Changed directory to:\n  {}\n\n{}You're in the virtual lesson filesystem\n",
1156                            icons::success().content, new_path, icons::hint().content
1157                        );
1158                        self.output_scroll = 0;
1159                        return Ok(());
1160                    }
1161                    Err(e) => {
1162                        self.last_output = format!("{}cd: {}\n", icons::error().content, e);
1163                        self.output_scroll = 0;
1164                        return Ok(());
1165                    }
1166                }
1167            }
1168        }
1169
1170        // Normal mode - use real filesystem
1171        // Determine target directory
1172        let target = if cmd.args.is_empty() {
1173            // cd with no args goes to home directory
1174            dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?
1175        } else {
1176            let target_str = &cmd.args[0];
1177
1178            // Expand ~ to home directory
1179            if target_str == "~" {
1180                dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?
1181            } else if target_str.starts_with("~/") {
1182                let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?;
1183                home.join(&target_str[2..])
1184            } else {
1185                PathBuf::from(target_str)
1186            }
1187        };
1188
1189        // Try to change directory
1190        match std::env::set_current_dir(&target) {
1191            Ok(_) => {
1192                // Update session working directory
1193                self.session.state.working_directory = std::env::current_dir()?;
1194
1195                // Update context
1196                self.update_context()?;
1197
1198                // Show success message
1199                let new_dir = std::env::current_dir()?;
1200                self.last_output = format!(
1201                    "{}Changed directory to:\n  {}\n",
1202                    icons::success().content, new_dir.display()
1203                );
1204
1205                // Reset scroll
1206                self.output_scroll = 0;
1207
1208                // Record in session
1209                self.session.record_command(
1210                    format!("cd {}", cmd.args.join(" ")),
1211                    Some(0),
1212                    Some(0),
1213                );
1214
1215                Ok(())
1216            }
1217            Err(e) => {
1218                // Show error message
1219                self.last_output = format!(
1220                    "{}cd: {}\n  Cannot change to: {}\n",
1221                    icons::error().content, e, target.display()
1222                );
1223                self.output_scroll = 0;
1224
1225                // Record in session as failed
1226                self.session.record_command(
1227                    format!("cd {}", cmd.args.join(" ")),
1228                    Some(1),
1229                    Some(0),
1230                );
1231
1232                Ok(())
1233            }
1234        }
1235    }
1236
1237    /// Update context (e.g., when directory changes)
1238    pub fn update_context(&mut self) -> Result<()> {
1239        let working_dir = &self.session.state.working_directory;
1240        self.context = ContextDetector::detect(working_dir)?;
1241        Ok(())
1242    }
1243
1244    /// Navigate to previous command in history (Up arrow)
1245    fn history_previous(&mut self) {
1246        if self.command_history.is_empty() {
1247            return;
1248        }
1249
1250        match self.history_position {
1251            None => {
1252                // Start browsing history from most recent
1253                self.history_position = Some(0);
1254                self.command_buffer = self.command_history[0].clone();
1255            }
1256            Some(pos) => {
1257                // Move to older command if possible
1258                if pos < self.command_history.len() - 1 {
1259                    let new_pos = pos + 1;
1260                    self.history_position = Some(new_pos);
1261                    self.command_buffer = self.command_history[new_pos].clone();
1262                }
1263            }
1264        }
1265    }
1266
1267    /// Navigate to next command in history (Down arrow)
1268    fn history_next(&mut self) {
1269        match self.history_position {
1270            None => {
1271                // Not browsing history, do nothing
1272            }
1273            Some(0) => {
1274                // At most recent, clear buffer
1275                self.history_position = None;
1276                self.command_buffer.clear();
1277            }
1278            Some(pos) => {
1279                // Move to newer command
1280                let new_pos = pos - 1;
1281                self.history_position = Some(new_pos);
1282                self.command_buffer = self.command_history[new_pos].clone();
1283            }
1284        }
1285    }
1286
1287    /// Add a command to history
1288    fn add_to_history(&mut self, command: String) {
1289        if command.trim().is_empty() {
1290            return;
1291        }
1292
1293        // Don't add duplicate of most recent command
1294        if let Some(last) = self.command_history.first() {
1295            if last == &command {
1296                return;
1297            }
1298        }
1299
1300        // Add to beginning (most recent first)
1301        self.command_history.insert(0, command);
1302
1303        // Limit history size to 1000 commands
1304        if self.command_history.len() > 1000 {
1305            self.command_history.truncate(1000);
1306        }
1307
1308        // Save all progress to disk
1309        self.save_session();
1310    }
1311
1312    /// Save all session data (history, stats, progress) to disk
1313    fn save_session(&self) {
1314        let session_data = crate::persistence::SessionData {
1315            command_history: self.command_history.clone(),
1316            last_updated: chrono::Local::now().to_rfc3339(),
1317            user_stats: self.user_stats.clone(),
1318            completed_lessons: self.completed_lessons.clone(),
1319            challenge_manager: self.challenge_manager.clone(),
1320        };
1321
1322        if let Err(e) = crate::persistence::save_session(&session_data) {
1323            tracing::warn!("Failed to save session: {}", e);
1324        }
1325    }
1326
1327    /// Handle history command (show command history)
1328    fn handle_history_command(&mut self, cmd: &arct_core::Command) -> Result<()> {
1329        // Parse optional argument for number of commands to show
1330        let limit = if cmd.args.is_empty() {
1331            50 // Default: show last 50 commands
1332        } else {
1333            cmd.args[0].parse::<usize>().unwrap_or(50)
1334        };
1335
1336        if self.command_history.is_empty() {
1337            self.last_output = "No commands in history yet.\n".to_string();
1338        } else {
1339            let mut output = String::new();
1340            let total = self.command_history.len();
1341
1342            // Show commands in reverse chronological order (oldest to newest on screen)
1343            // but numbered from oldest to newest (like bash)
1344            for (i, cmd) in self.command_history.iter().rev().enumerate().take(limit) {
1345                let index = total - self.command_history.len() + i + 1;
1346                output.push_str(&format!("{:5}  {}\n", index, cmd));
1347            }
1348
1349            self.last_output = output;
1350        }
1351
1352        // Reset scroll
1353        self.output_scroll = 0;
1354
1355        // Record in session
1356        self.session.record_command(
1357            format!("history {}", if cmd.args.is_empty() { String::new() } else { cmd.args.join(" ") }),
1358            Some(0),
1359            Some(0),
1360        );
1361
1362        Ok(())
1363    }
1364
1365    /// Handle export command (set environment variables)
1366    fn handle_export_command(&mut self, cmd: &arct_core::Command) -> Result<()> {
1367        if cmd.args.is_empty() {
1368            // No arguments - show all exported variables
1369            if self.environment_vars.is_empty() {
1370                self.last_output = "No environment variables set.\n".to_string();
1371            } else {
1372                let mut output = String::new();
1373                output.push_str("Exported environment variables:\n\n");
1374                let mut vars: Vec<_> = self.environment_vars.iter().collect();
1375                vars.sort_by_key(|(k, _)| *k);
1376                for (key, value) in vars {
1377                    output.push_str(&format!("  {}={}\n", key, value));
1378                }
1379                self.last_output = output;
1380            }
1381        } else {
1382            // Parse VAR=value format
1383            for arg in &cmd.args {
1384                if let Some((key, value)) = arg.split_once('=') {
1385                    let key = key.trim().to_string();
1386                    let value = value.trim().to_string();
1387
1388                    // Remove quotes if present
1389                    let value = if (value.starts_with('"') && value.ends_with('"')) ||
1390                                   (value.starts_with('\'') && value.ends_with('\'')) {
1391                        value[1..value.len()-1].to_string()
1392                    } else {
1393                        value
1394                    };
1395
1396                    self.environment_vars.insert(key.clone(), value.clone());
1397                    self.last_output = format!("{}Exported: {}={}\n", icons::success().content, key, value);
1398
1399                    // Save to config
1400                    self.config.shell.environment = self.environment_vars.clone();
1401                    if let Err(e) = self.config.save() {
1402                        tracing::warn!("Failed to save config: {}", e);
1403                    }
1404                } else {
1405                    self.last_output = format!("{}Invalid export syntax: {}\n  Usage: export VAR=value\n", icons::error().content, arg);
1406                    break;
1407                }
1408            }
1409        }
1410
1411        // Reset scroll
1412        self.output_scroll = 0;
1413
1414        // Record in session
1415        self.session.record_command(
1416            format!("export {}", cmd.args.join(" ")),
1417            Some(0),
1418            Some(0),
1419        );
1420
1421        Ok(())
1422    }
1423
1424    /// Handle alias command (create command shortcuts)
1425    fn handle_alias_command(&mut self, cmd: &arct_core::Command) -> Result<()> {
1426        if cmd.args.is_empty() {
1427            // No arguments - show all aliases
1428            if self.aliases.is_empty() {
1429                self.last_output = "No aliases defined.\n".to_string();
1430            } else {
1431                let mut output = String::new();
1432                output.push_str("Defined aliases:\n\n");
1433                let mut aliases: Vec<_> = self.aliases.iter().collect();
1434                aliases.sort_by_key(|(k, _)| *k);
1435                for (name, command) in aliases {
1436                    output.push_str(&format!("  {}='{}'\n", name, command));
1437                }
1438                self.last_output = output;
1439            }
1440        } else {
1441            // Parse name=command format
1442            let arg = cmd.args.join(" ");
1443            if let Some((name, command)) = arg.split_once('=') {
1444                let name = name.trim().to_string();
1445                let command = command.trim().to_string();
1446
1447                // Remove quotes if present
1448                let command = if (command.starts_with('"') && command.ends_with('"')) ||
1449                                 (command.starts_with('\'') && command.ends_with('\'')) {
1450                    command[1..command.len()-1].to_string()
1451                } else {
1452                    command
1453                };
1454
1455                self.aliases.insert(name.clone(), command.clone());
1456                self.last_output = format!("{}Alias created: {}='{}'\n", icons::success().content, name, command);
1457
1458                // Save to config
1459                self.config.shell.aliases = self.aliases.clone();
1460                if let Err(e) = self.config.save() {
1461                    tracing::warn!("Failed to save config: {}", e);
1462                }
1463            } else {
1464                self.last_output = format!("{}Invalid alias syntax: {}\n  Usage: alias name='command'\n", icons::error().content, arg);
1465            }
1466        }
1467
1468        // Reset scroll
1469        self.output_scroll = 0;
1470
1471        // Record in session
1472        self.session.record_command(
1473            format!("alias {}", cmd.args.join(" ")),
1474            Some(0),
1475            Some(0),
1476        );
1477
1478        Ok(())
1479    }
1480
1481    /// Handle Tab key autocompletion
1482    fn handle_autocomplete(&mut self) -> Result<()> {
1483        if self.command_buffer.is_empty() {
1484            return Ok(());
1485        }
1486
1487        // Get completion results
1488        let working_dir = &self.session.state.working_directory;
1489        let result = self.autocompleter.complete(&self.command_buffer, working_dir)?;
1490
1491        // If there's a unique completion or common prefix, apply it
1492        if !result.common_prefix.is_empty() && result.common_prefix != self.command_buffer {
1493            // Update the buffer with the common prefix
1494            // We need to replace the last token with the completion
1495            let tokens: Vec<&str> = self.command_buffer.split_whitespace().collect();
1496
1497            if tokens.is_empty() {
1498                self.command_buffer = result.common_prefix.clone();
1499            } else if tokens.len() == 1 && !self.command_buffer.ends_with(' ') {
1500                // Completing first token (command)
1501                self.command_buffer = result.common_prefix.clone();
1502            } else {
1503                // Completing a path - replace last token
1504                let last_token = tokens.last().unwrap_or(&"");
1505                if let Some(idx) = self.command_buffer.rfind(last_token) {
1506                    self.command_buffer.truncate(idx);
1507                    self.command_buffer.push_str(&result.common_prefix);
1508                }
1509            }
1510        }
1511
1512        // Store suggestions for display (limit to 10)
1513        self.completion_suggestions = result.completions.into_iter().take(10).collect();
1514
1515        Ok(())
1516    }
1517
1518    /// Toggle AI assistant mode
1519    pub fn toggle_ai_mode(&mut self) {
1520        if self.ai_provider.is_some() {
1521            self.ai_mode = !self.ai_mode;
1522            if self.ai_mode {
1523                // Clear AI input when entering AI mode
1524                self.ai_input_buffer.clear();
1525                self.ai_loading = false;
1526            }
1527        } else {
1528            self.last_output = format!("{}AI is not enabled. Configure it in ~/.config/arct/config.toml\n", icons::error().content);
1529        }
1530    }
1531
1532    /// Toggle lesson mode
1533    pub fn toggle_lesson_mode(&mut self) {
1534        self.lesson_mode = !self.lesson_mode;
1535
1536        if self.lesson_mode {
1537            // Initialize virtual filesystem
1538            match arct_core::VirtualFileSystem::new("nav-basics", &self.session_id) {
1539                Ok(vfs) => {
1540                    self.virtual_fs = Some(vfs);
1541                    self.last_output = format!("{}Lesson mode activated! You're now in a safe virtual filesystem.\n\nPress Ctrl+L again to return to normal mode.\n\nNavigate through lessons using the Learning panel on the right.\n", icons::lesson().content);
1542                }
1543                Err(e) => {
1544                    self.last_output = format!("{}Failed to initialize lesson environment: {}\n", icons::error().content, e);
1545                    self.lesson_mode = false;
1546                    return;
1547                }
1548            }
1549
1550            // Initialize lesson panel if not already done
1551            if self.lesson_panel.is_none() {
1552                self.lesson_panel = Self::initialize_lesson_panel();
1553            }
1554
1555            // Show lesson menu to let user choose a lesson
1556            if self.lesson_menu.is_none() {
1557                self.lesson_menu = Some(crate::panels::lesson_menu::LessonMenuPanel::new());
1558            }
1559        } else {
1560            // Clean up virtual filesystem
1561            self.virtual_fs = None;
1562            self.last_output = format!("{}Lesson mode deactivated. Back to normal shell mode and real filesystem.\n", icons::learning().content);
1563        }
1564    }
1565
1566    /// Ask the AI assistant a question
1567    pub async fn ask_ai(&mut self, question: String) -> Result<()> {
1568        if self.ai_provider.is_none() {
1569            return Ok(());
1570        }
1571
1572        if question.trim().is_empty() {
1573            return Ok(());
1574        }
1575
1576        self.ai_loading = true;
1577
1578        // Add user message to conversation
1579        self.ai_conversation.push(arct_ai::Message::user(question.clone()));
1580
1581        // Build conversation with system prompt
1582        let user_name = self.config.general.user_name.as_deref().unwrap_or("there");
1583        let system_prompt = format!(
1584            "You are an AI teaching assistant integrated into Arc Academy Terminal, \
1585             an interactive terminal learning application. Your role is to help users \
1586             learn shell commands and terminal skills.\n\n\
1587             You're helping {}, so address them by name occasionally to make the \
1588             interaction personal and engaging.\n\n\
1589             Guidelines:\n\
1590             - Teach shell commands with clear, executable examples\n\
1591             - Explain concepts in beginner-friendly language\n\
1592             - Provide commands the user can type themselves in the terminal\n\
1593             - Keep responses concise (3-4 sentences or a short example)\n\
1594             - Focus on common Linux/Unix commands (bash, grep, find, etc.)\n\
1595             - Suggest safer alternatives when appropriate\n\
1596             - You are NOT Claude Code - you cannot execute commands or use tools\n\
1597             - You are a teaching assistant helping someone learn the terminal\n\
1598             - Be encouraging and supportive in your teaching approach",
1599            user_name
1600        );
1601
1602        let mut messages = vec![
1603            arct_ai::Message::system(system_prompt),
1604        ];
1605        messages.extend(self.ai_conversation.clone());
1606
1607        // Get response from AI
1608        // SAFETY: ai_provider is guaranteed to be Some when ai_mode is true
1609        // because toggle_ai_mode() checks ai_provider.is_some() before enabling
1610        let provider = self.ai_provider.as_ref()
1611            .expect("BUG: ai_provider must exist when ai_mode is true - this is a logic error");
1612        let response = provider.complete(&messages, None).await;
1613
1614        self.ai_loading = false;
1615
1616        match response {
1617            Ok(ai_response) => {
1618                // Strip markdown formatting for terminal display
1619                let cleaned_content = Self::strip_markdown(&ai_response.content);
1620
1621                // Add assistant response to conversation
1622                self.ai_conversation.push(arct_ai::Message::assistant(ai_response.content.clone()));
1623                self.ai_response = Some(cleaned_content);
1624                Ok(())
1625            }
1626            Err(e) => {
1627                self.ai_response = Some(format!("{}Error: {}", icons::error().content, e));
1628                Err(anyhow::anyhow!("AI request failed: {}", e))
1629            }
1630        }
1631    }
1632
1633    /// Clear AI conversation
1634    pub fn clear_ai_conversation(&mut self) {
1635        self.ai_conversation.clear();
1636        self.ai_response = None;
1637        self.ai_input_buffer.clear();
1638    }
1639
1640    /// Strip markdown formatting from text for plain terminal display
1641    fn strip_markdown(text: &str) -> String {
1642        let mut result = String::new();
1643        let mut in_code_block = false;
1644        let mut skip_line = false;
1645
1646        for line in text.lines() {
1647            // Toggle code block state
1648            if line.trim().starts_with("```") {
1649                in_code_block = !in_code_block;
1650                skip_line = true;
1651            }
1652
1653            if skip_line {
1654                skip_line = false;
1655                continue;
1656            }
1657
1658            // Clean the line
1659            let mut cleaned = line.to_string();
1660
1661            // Remove headers (## Header -> Header)
1662            if cleaned.trim_start().starts_with('#') {
1663                cleaned = cleaned.trim_start().trim_start_matches('#').trim().to_string();
1664            }
1665
1666            // Remove bold/italic markers
1667            cleaned = cleaned.replace("**", "").replace("*", "");
1668
1669            // Remove inline code backticks (but not the content)
1670            cleaned = cleaned.replace('`', "");
1671
1672            // Remove list markers (- item -> item, but keep indentation)
1673            if let Some(stripped) = cleaned.trim_start().strip_prefix("- ") {
1674                let indent = cleaned.len() - cleaned.trim_start().len();
1675                cleaned = format!("{}{}", " ".repeat(indent), stripped);
1676            }
1677
1678            result.push_str(&cleaned);
1679            result.push('\n');
1680        }
1681
1682        result.trim_end().to_string()
1683    }
1684
1685    /// Handle onboarding wizard events
1686    async fn handle_onboarding_event(&mut self, key: KeyEvent) -> Result<()> {
1687        if let Some(wizard) = self.onboarding.as_mut() {
1688            match key.code {
1689                KeyCode::Char(c) if key.modifiers == KeyModifiers::NONE || key.modifiers == KeyModifiers::SHIFT => {
1690                    wizard.handle_char(c);
1691                }
1692                KeyCode::Backspace => {
1693                    wizard.handle_backspace();
1694                }
1695                KeyCode::Up => {
1696                    wizard.handle_up();
1697                }
1698                KeyCode::Down => {
1699                    let max_options = match wizard.step {
1700                        crate::panels::onboarding::OnboardingStep::AskAI => 3,
1701                        crate::panels::onboarding::OnboardingStep::AskAIProvider => 3,
1702                        _ => 1,
1703                    };
1704                    wizard.handle_down(max_options);
1705                }
1706                KeyCode::Enter => {
1707                    wizard.handle_enter();
1708
1709                    // Check if onboarding is complete
1710                    if wizard.step == crate::panels::onboarding::OnboardingStep::Complete {
1711                        // Save settings to config
1712                        if let Some(wizard) = self.onboarding.take() {
1713                            self.complete_onboarding(wizard).await?;
1714                        }
1715                    }
1716                }
1717                _ => {}
1718            }
1719        }
1720        Ok(())
1721    }
1722
1723    /// Complete onboarding and save settings
1724    async fn complete_onboarding(&mut self, wizard: crate::panels::onboarding::OnboardingWizard) -> Result<()> {
1725        // Update config with onboarding results
1726        if !wizard.user_name.is_empty() {
1727            self.config.general.user_name = Some(wizard.user_name.clone());
1728        }
1729
1730        if let Some(ai_enabled) = wizard.ai_enabled {
1731            self.config.ai.enabled = ai_enabled;
1732
1733            if ai_enabled {
1734                // Configure AI provider based on user selection
1735                if wizard.ai_provider.as_deref() == Some("claude-code") {
1736                    // Claude Code CLI for Max subscribers
1737                    self.config.ai.provider = "claude-cli".to_string();
1738                    self.config.ai.model = Some("claude-sonnet-4".to_string());
1739                    // No API key needed - uses Claude Code authentication
1740                } else if wizard.ai_provider.as_deref() == Some("own") {
1741                    // User has their own API key - default to local LLM
1742                    self.config.ai.provider = "local".to_string();
1743                    self.config.ai.endpoint = Some("http://localhost:11434".to_string());
1744                    self.config.ai.model = Some("llama3.2".to_string());
1745                } else if wizard.ai_provider.as_deref() == Some("managed") {
1746                    // Arc Academy managed service
1747                    self.config.ai.provider = "managed".to_string();
1748                }
1749            }
1750        }
1751
1752        // Mark setup as complete
1753        self.config.general.setup_complete = true;
1754
1755        // Save config
1756        self.config.save()?;
1757
1758        // Reinitialize AI provider with new settings
1759        if self.config.ai.enabled {
1760            match Self::create_ai_provider(&self.config.ai) {
1761                Ok(provider) => {
1762                    self.ai_provider = Some(provider);
1763                }
1764                Err(e) => {
1765                    // Log error but don't fail onboarding
1766                    self.last_output = format!("{}AI provider initialization failed: {}\n", icons::warning().content, e);
1767                }
1768            }
1769        }
1770
1771        // Remove onboarding
1772        self.onboarding = None;
1773
1774        // Show greeting in output
1775        let name = self.config.general.user_name.as_deref().unwrap_or("there");
1776        let mut welcome_msg = format!(
1777            "{}Welcome, {}!\n\n\
1778             You're all set to start learning shell commands!\n\n",
1779            icons::celebration().content, name
1780        );
1781
1782        // Add provider-specific setup notes if AI is enabled
1783        if self.config.ai.enabled {
1784            match self.config.ai.provider.as_str() {
1785                "claude-cli" => {
1786                    welcome_msg.push_str(
1787                        &format!("{}Using Claude Code CLI - your Max subscription is ready!\n\
1788                         Press Ctrl+A to ask Claude for help.\n\n", icons::ai().content)
1789                    );
1790                }
1791                "anthropic" | "openai" => {
1792                    welcome_msg.push_str(
1793                        &format!("{}To use AI features, set your API key:\n\
1794                         export ARCT_AI_API_KEY=\"your-api-key-here\"\n\n", icons::note().content)
1795                    );
1796                }
1797                "local" => {
1798                    welcome_msg.push_str(
1799                        &format!("{}Using local LLM - make sure your server is running!\n\n", icons::info().content)
1800                    );
1801                }
1802                _ => {}
1803            }
1804        }
1805
1806        welcome_msg.push_str("Press ? for help, or just start typing commands.\n");
1807        self.last_output = welcome_msg;
1808
1809        Ok(())
1810    }
1811
1812    /// Handle settings panel events
1813    async fn handle_settings_event(&mut self, key: KeyEvent) -> Result<()> {
1814        // Determine what action to take
1815        let (action, selected_field) = {
1816            let panel = match self.settings_panel.as_ref() {
1817                Some(p) => p,
1818                None => return Ok(()),
1819            };
1820
1821            let action = if panel.editing {
1822                // In edit mode
1823                match key.code {
1824                    KeyCode::Char(c) if key.modifiers == KeyModifiers::NONE || key.modifiers == KeyModifiers::SHIFT => {
1825                        SettingsAction::PushChar(c)
1826                    }
1827                    KeyCode::Backspace => SettingsAction::PopChar,
1828                    KeyCode::Enter => SettingsAction::SaveEdit,
1829                    KeyCode::Esc => SettingsAction::CancelEdit,
1830                    _ => SettingsAction::None,
1831                }
1832            } else {
1833                // In navigation mode
1834                match key.code {
1835                    KeyCode::Up | KeyCode::Char('k') if key.modifiers == KeyModifiers::NONE => {
1836                        SettingsAction::PreviousField
1837                    }
1838                    KeyCode::Down | KeyCode::Char('j') if key.modifiers == KeyModifiers::NONE => {
1839                        SettingsAction::NextField
1840                    }
1841                    KeyCode::Enter => SettingsAction::StartEdit,
1842                    KeyCode::Esc | KeyCode::Char('s') if key.modifiers == KeyModifiers::CONTROL => {
1843                        SettingsAction::Close
1844                    }
1845                    _ => SettingsAction::None,
1846                }
1847            };
1848
1849            (action, panel.selected_field)
1850        };
1851
1852        // Execute the action
1853        match action {
1854            SettingsAction::PushChar(c) => {
1855                if let Some(panel) = self.settings_panel.as_mut() {
1856                    panel.push_char(c);
1857                }
1858            }
1859            SettingsAction::PopChar => {
1860                if let Some(panel) = self.settings_panel.as_mut() {
1861                    panel.pop_char();
1862                }
1863            }
1864            SettingsAction::SaveEdit => {
1865                if let Some(panel) = self.settings_panel.as_mut() {
1866                    panel.save_edit(&mut self.config)?;
1867
1868                    // If theme was changed, reload it
1869                    if selected_field == crate::panels::settings::SettingField::Theme {
1870                        self.theme = Theme::from_name(&self.config.theme.default_theme);
1871                    }
1872
1873                    // If AI was toggled, reload provider
1874                    if selected_field == crate::panels::settings::SettingField::AIEnabled {
1875                        if self.config.ai.enabled {
1876                            // Try to initialize AI provider
1877                            match Self::create_ai_provider(&self.config.ai) {
1878                                Ok(provider) => {
1879                                    self.ai_provider = Some(provider);
1880                                }
1881                                Err(e) => {
1882                                    tracing::warn!("Failed to initialize AI provider: {}", e);
1883                                    self.ai_provider = None;
1884                                }
1885                            }
1886                        } else {
1887                            self.ai_provider = None;
1888                            self.ai_mode = false;
1889                        }
1890                    }
1891                }
1892            }
1893            SettingsAction::CancelEdit => {
1894                if let Some(panel) = self.settings_panel.as_mut() {
1895                    panel.cancel_editing();
1896                }
1897            }
1898            SettingsAction::PreviousField => {
1899                if let Some(panel) = self.settings_panel.as_mut() {
1900                    panel.previous_field();
1901                }
1902            }
1903            SettingsAction::NextField => {
1904                if let Some(panel) = self.settings_panel.as_mut() {
1905                    panel.next_field();
1906                }
1907            }
1908            SettingsAction::StartEdit => {
1909                if let Some(panel) = self.settings_panel.as_mut() {
1910                    panel.start_editing(&self.config);
1911                }
1912            }
1913            SettingsAction::Close => {
1914                self.settings_panel = None;
1915            }
1916            SettingsAction::None => {}
1917        }
1918
1919        Ok(())
1920    }
1921
1922    /// Toggle achievements panel
1923    pub fn toggle_achievements_panel(&mut self) {
1924        if self.achievements_panel.is_some() {
1925            self.achievements_panel = None;
1926        } else {
1927            self.achievements_panel = Some(crate::panels::achievements::AchievementsPanel::new());
1928        }
1929    }
1930
1931    /// Toggle progress panel
1932    pub fn toggle_progress_panel(&mut self) {
1933        if self.progress_panel.is_some() {
1934            self.progress_panel = None;
1935        } else {
1936            self.progress_panel = Some(crate::panels::progress::ProgressPanel::new());
1937        }
1938    }
1939
1940    /// Toggle challenges panel
1941    pub fn toggle_challenges_panel(&mut self) {
1942        if self.challenges_panel.is_some() {
1943            self.challenges_panel = None;
1944        } else {
1945            self.challenges_panel = Some(crate::panels::challenges::ChallengesPanel::new());
1946        }
1947    }
1948
1949    /// Check for newly unlocked achievements and queue notifications
1950    pub fn check_and_unlock_achievements(&mut self) {
1951        let newly_unlocked = self.user_stats.check_achievements();
1952
1953        // Add to pending queue
1954        for achievement in newly_unlocked {
1955            self.pending_achievements.push(achievement);
1956        }
1957
1958        // Show first notification if we're not already showing one
1959        if self.showing_notification.is_none() && !self.pending_achievements.is_empty() {
1960            self.show_next_achievement_notification();
1961        }
1962    }
1963
1964    /// Show the next achievement notification from the queue
1965    fn show_next_achievement_notification(&mut self) {
1966        if let Some(achievement) = self.pending_achievements.first() {
1967            self.showing_notification = Some(crate::panels::notification::NotificationPanel::new(
1968                achievement.clone(),
1969            ));
1970        }
1971    }
1972
1973    /// Dismiss the current achievement notification and show next if any
1974    fn dismiss_notification(&mut self) {
1975        self.showing_notification = None;
1976
1977        // Remove the first achievement from queue if it was shown
1978        if !self.pending_achievements.is_empty() {
1979            self.pending_achievements.remove(0);
1980        }
1981
1982        // Show next notification if there are more
1983        if !self.pending_achievements.is_empty() {
1984            self.show_next_achievement_notification();
1985        }
1986    }
1987
1988    /// Record command execution for stats tracking
1989    pub fn record_command_for_stats(&mut self, command: &str) {
1990        // Extract just the command name (first word)
1991        let command_name = command.split_whitespace().next().unwrap_or("");
1992        if !command_name.is_empty() {
1993            self.user_stats.record_command_use(command_name.to_string());
1994        }
1995    }
1996
1997    /// Record lesson completion and check for achievements
1998    pub fn record_lesson_completion(&mut self, lesson_id: String, difficulty: arct_core::Difficulty) {
1999        // Record in stats
2000        self.user_stats.record_lesson_completion(lesson_id.clone(), difficulty);
2001
2002        // Add to completed lessons set
2003        self.completed_lessons.insert(lesson_id);
2004
2005        // Check for newly unlocked achievements
2006        self.check_and_unlock_achievements();
2007    }
2008}
2009
2010/// Actions that can be performed in the settings panel
2011enum SettingsAction {
2012    PushChar(char),
2013    PopChar,
2014    SaveEdit,
2015    CancelEdit,
2016    PreviousField,
2017    NextField,
2018    StartEdit,
2019    Close,
2020    None,
2021}