ricecoder_tui/
app.rs

1//! Application state and main TUI application
2
3use crate::accessibility::{
4    FocusManager, KeyboardNavigationManager, ScreenReaderAnnouncer, StateChangeEvent,
5};
6use crate::config::TuiConfig;
7use crate::event::{Event, EventLoop};
8use crate::image_integration::ImageIntegration;
9use crate::integration::WidgetIntegration;
10use crate::render::Renderer;
11use crate::style::Theme;
12use crate::theme::ThemeManager;
13use anyhow::Result;
14
15/// Application mode
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum AppMode {
18    /// Chat mode for conversational interaction
19    Chat,
20    /// Command mode for executing commands
21    Command,
22    /// Diff mode for reviewing code changes
23    Diff,
24    /// Help mode
25    Help,
26}
27
28impl AppMode {
29    /// Get the display name for the mode
30    pub fn display_name(&self) -> &'static str {
31        match self {
32            AppMode::Chat => "Chat",
33            AppMode::Command => "Command",
34            AppMode::Diff => "Diff",
35            AppMode::Help => "Help",
36        }
37    }
38
39    /// Get the keyboard shortcut for the mode
40    pub fn shortcut(&self) -> &'static str {
41        match self {
42            AppMode::Chat => "Ctrl+1",
43            AppMode::Command => "Ctrl+2",
44            AppMode::Diff => "Ctrl+3",
45            AppMode::Help => "Ctrl+4",
46        }
47    }
48
49    /// Get the next mode in the cycle
50    pub fn next(&self) -> AppMode {
51        match self {
52            AppMode::Chat => AppMode::Command,
53            AppMode::Command => AppMode::Diff,
54            AppMode::Diff => AppMode::Help,
55            AppMode::Help => AppMode::Chat,
56        }
57    }
58
59    /// Get the previous mode in the cycle
60    pub fn previous(&self) -> AppMode {
61        match self {
62            AppMode::Chat => AppMode::Help,
63            AppMode::Command => AppMode::Chat,
64            AppMode::Diff => AppMode::Command,
65            AppMode::Help => AppMode::Diff,
66        }
67    }
68}
69
70/// Chat state
71#[derive(Debug, Clone)]
72pub struct ChatState {
73    /// Messages in the conversation
74    pub messages: Vec<String>,
75    /// Current input
76    pub input: String,
77    /// Whether streaming is active
78    pub streaming: bool,
79    /// Current prompt context with text and images
80    /// Requirements: 1.4 - Add images to prompt context
81    pub prompt_context: crate::prompt_context::PromptContext,
82}
83
84impl Default for ChatState {
85    fn default() -> Self {
86        Self {
87            messages: Vec::new(),
88            input: String::new(),
89            streaming: false,
90            prompt_context: crate::prompt_context::PromptContext::new(),
91        }
92    }
93}
94
95/// Main application state
96pub struct App {
97    /// Current application mode
98    pub mode: AppMode,
99    /// Previous application mode (for quick switching)
100    pub previous_mode: AppMode,
101    /// Chat state
102    pub chat: ChatState,
103    /// Theme manager for runtime theme switching
104    pub theme_manager: ThemeManager,
105    /// Current theme (cached from theme manager)
106    pub theme: Theme,
107    /// Application configuration
108    pub config: TuiConfig,
109    /// Whether the application should exit
110    pub should_exit: bool,
111    /// Event loop
112    pub event_loop: EventLoop,
113    /// Renderer
114    pub renderer: Renderer,
115    /// Mode-specific keybindings enabled
116    pub keybindings_enabled: bool,
117    /// Widget integration manager
118    pub widget_integration: WidgetIntegration,
119    /// Screen reader announcer for accessibility
120    pub screen_reader: ScreenReaderAnnouncer,
121    /// Keyboard navigation manager
122    pub keyboard_nav: KeyboardNavigationManager,
123    /// Focus manager for accessibility
124    pub focus_manager: FocusManager,
125    /// Provider integration for AI responses
126    pub provider_integration: crate::provider_integration::ProviderIntegration,
127    /// Image integration for drag-and-drop and display
128    /// Requirements: 1.1 - Detect drag-and-drop event via crossterm
129    pub image_integration: ImageIntegration,
130    /// Image widget for displaying images in the terminal
131    /// Requirements: 5.1 - Display images in terminal using ricecoder-images ImageDisplay
132    pub image_widget: crate::image_widget::ImageWidget,
133}
134
135impl App {
136    /// Create a new application instance
137    pub fn new() -> Result<Self> {
138        let config = TuiConfig::load()?;
139        Self::with_config(config)
140    }
141
142    /// Create a new application instance with a specific configuration
143    pub fn with_config(config: TuiConfig) -> Result<Self> {
144        let theme_manager = ThemeManager::new();
145
146        // Load theme from config
147        theme_manager.load_from_config(&config)?;
148        let theme = theme_manager.current()?;
149
150        // Create widget integration with default terminal size
151        let widget_integration = WidgetIntegration::new(80, 24);
152
153        // Initialize accessibility features
154        let screen_reader = ScreenReaderAnnouncer::new(config.accessibility.screen_reader_enabled);
155        let keyboard_nav = KeyboardNavigationManager::new();
156        let focus_manager = FocusManager::new();
157
158        // Initialize provider integration
159        let provider_integration = crate::provider_integration::ProviderIntegration::with_provider(
160            config.provider.clone(),
161            config.model.clone(),
162        );
163
164        let app = Self {
165            mode: AppMode::Chat,
166            previous_mode: AppMode::Chat,
167            chat: ChatState::default(),
168            theme_manager,
169            theme,
170            config,
171            should_exit: false,
172            event_loop: EventLoop::new(),
173            renderer: Renderer::new(),
174            keybindings_enabled: true,
175            widget_integration,
176            screen_reader,
177            keyboard_nav,
178            focus_manager,
179            provider_integration,
180            image_integration: ImageIntegration::new(),
181            image_widget: crate::image_widget::ImageWidget::new(),
182        };
183
184        Ok(app)
185    }
186
187    /// Run the application
188    pub async fn run(&mut self) -> Result<()> {
189        tracing::info!("Starting RiceCoder TUI");
190
191        // Main event loop
192        while !self.should_exit {
193            // Poll for events
194            if let Some(event) = self.event_loop.poll().await? {
195                self.handle_event(event)?;
196            }
197
198            // Render the UI
199            self.renderer.render(self)?;
200        }
201
202        tracing::info!("RiceCoder TUI exited successfully");
203        Ok(())
204    }
205
206    /// Switch to a different mode
207    pub fn switch_mode(&mut self, mode: AppMode) {
208        if self.mode != mode {
209            tracing::info!("Switching mode from {:?} to {:?}", self.mode, mode);
210            self.previous_mode = self.mode;
211            self.mode = mode;
212
213            // Notify widget integration of mode switch
214            if let Err(e) = self
215                .widget_integration
216                .on_mode_switch(self.previous_mode, self.mode)
217            {
218                tracing::error!("Failed to switch mode: {}", e);
219            }
220        }
221    }
222
223    /// Switch to the next mode in the cycle
224    pub fn next_mode(&mut self) {
225        let next = self.mode.next();
226        self.switch_mode(next);
227    }
228
229    /// Switch to the previous mode in the cycle
230    pub fn previous_mode_switch(&mut self) {
231        let prev = self.mode.previous();
232        self.switch_mode(prev);
233    }
234
235    /// Toggle between current and previous mode
236    pub fn toggle_mode(&mut self) {
237        let prev = self.previous_mode;
238        self.switch_mode(prev);
239    }
240
241    /// Get the current mode display name
242    pub fn current_mode_name(&self) -> &'static str {
243        self.mode.display_name()
244    }
245
246    /// Get the current mode shortcut
247    pub fn current_mode_shortcut(&self) -> &'static str {
248        self.mode.shortcut()
249    }
250
251    /// Switch to a theme by name
252    pub fn switch_theme(&mut self, name: &str) -> Result<()> {
253        self.theme_manager.switch_by_name(name)?;
254        self.theme = self.theme_manager.current()?;
255        self.config.theme = name.to_string();
256        self.config.save()?;
257        tracing::info!("Switched to theme: {}", name);
258        Ok(())
259    }
260
261    /// Get available themes
262    pub fn available_themes(&self) -> Vec<&'static str> {
263        self.theme_manager.available_themes()
264    }
265
266    /// Get current theme name
267    pub fn current_theme_name(&self) -> Result<String> {
268        self.theme_manager.current_name()
269    }
270
271    /// Synchronize widget state
272    pub fn sync_widget_state(&mut self) {
273        // Create a temporary copy of app state for synchronization
274        let mode = self.mode;
275
276        // Sync state with widgets
277        self.widget_integration.widgets.prompt.context.mode = mode;
278        tracing::debug!("Widget state synchronized for mode: {:?}", mode);
279    }
280
281    /// Get the active widgets container
282    pub fn widgets(&self) -> &crate::integration::WidgetContainer {
283        &self.widget_integration.widgets
284    }
285
286    /// Get mutable access to the active widgets container
287    pub fn widgets_mut(&mut self) -> &mut crate::integration::WidgetContainer {
288        &mut self.widget_integration.widgets
289    }
290
291    /// Get the layout coordinator
292    pub fn layout(&self) -> &crate::integration::LayoutCoordinator {
293        &self.widget_integration.layout
294    }
295
296    /// Handle an event
297    fn handle_event(&mut self, event: Event) -> Result<()> {
298        match event {
299            Event::Key(key_event) => {
300                tracing::debug!("Key event: {:?}", key_event);
301                // Handle key events
302                if key_event.code == crate::event::KeyCode::Esc {
303                    self.should_exit = true;
304                }
305
306                // Tab navigation for keyboard accessibility
307                if key_event.code == crate::event::KeyCode::Tab {
308                    self.handle_tab_navigation(key_event.modifiers.shift);
309                    return Ok(());
310                }
311
312                // Mode switching with keyboard shortcuts
313                if self.keybindings_enabled {
314                    self.handle_mode_switching(key_event);
315                }
316            }
317            Event::Mouse(_mouse_event) => {
318                tracing::debug!("Mouse event");
319                // Handle mouse events
320            }
321            Event::Resize { width, height } => {
322                tracing::debug!("Resize event: {}x{}", width, height);
323                self.config.width = Some(width);
324                self.config.height = Some(height);
325
326                // Notify widget integration of resize
327                if let Err(e) = self.widget_integration.on_resize(width, height) {
328                    tracing::error!("Failed to handle resize: {}", e);
329                }
330            }
331            Event::Tick => {
332                // Handle tick event for periodic updates
333            }
334            Event::DragDrop { paths } => {
335                tracing::debug!("Drag-and-drop event with {} files", paths.len());
336                // Handle drag-and-drop events
337                // Requirements: 1.1 - Pass drag-and-drop events to ricecoder-images handler
338                self.handle_drag_drop_event(paths)?;
339            }
340        }
341        Ok(())
342    }
343
344    /// Handle a drag-and-drop event with file paths
345    ///
346    /// # Arguments
347    ///
348    /// * `paths` - File paths from the drag-and-drop event
349    ///
350    /// # Requirements
351    ///
352    /// - Req 1.1: Pass drag-and-drop events to ricecoder-images handler
353    /// - Req 1.1: Handle file path extraction
354    /// - Req 1.1: Handle multiple files in single drag-and-drop
355    /// - Req 5.1: Add image preview to prompt context
356    /// - Req 5.2: Display images in chat interface
357    fn handle_drag_drop_event(&mut self, paths: std::vec::Vec<std::path::PathBuf>) -> Result<()> {
358        tracing::info!("Processing drag-and-drop event with {} files", paths.len());
359
360        // Pass to image integration
361        let (added, errors) = self.image_integration.handle_drag_drop_event(paths);
362
363        // Update image widget with added images
364        // Requirements: 5.1 - Add image preview to prompt context
365        if !added.is_empty() {
366            self.image_widget.add_images(added.clone());
367            tracing::info!("Updated image widget with {} images", added.len());
368
369            // Also add to prompt context
370            // Requirements: 1.4 - Add images to prompt context
371            self.chat.prompt_context.add_images(added.clone());
372            tracing::info!("Added {} images to prompt context", added.len());
373        }
374
375        // Log results
376        if !added.is_empty() {
377            tracing::info!("Added {} images to prompt context", added.len());
378            for path in &added {
379                tracing::debug!("Added image: {}", path.display());
380            }
381        }
382
383        if !errors.is_empty() {
384            tracing::warn!("Encountered {} errors processing drag-and-drop", errors.len());
385            for error in &errors {
386                tracing::debug!("Error: {}", error);
387            }
388        }
389
390        Ok(())
391    }
392
393    /// Sync prompt context with current state
394    ///
395    /// # Requirements
396    ///
397    /// - Req 1.4: Add images to prompt context
398    /// - Req 5.1: Include images in message history
399    pub fn sync_prompt_context(&mut self) {
400        // Sync images from image_integration to prompt_context
401        let images = self.image_integration.get_images().to_vec();
402        self.chat.prompt_context.clear_images();
403        self.chat.prompt_context.add_images(images);
404
405        tracing::debug!(
406            "Synced prompt context: {} images",
407            self.chat.prompt_context.image_count()
408        );
409    }
410
411    /// Get the current prompt context
412    pub fn get_prompt_context(&self) -> &crate::prompt_context::PromptContext {
413        &self.chat.prompt_context
414    }
415
416    /// Get mutable access to the current prompt context
417    pub fn get_prompt_context_mut(&mut self) -> &mut crate::prompt_context::PromptContext {
418        &mut self.chat.prompt_context
419    }
420
421    /// Handle mode switching keyboard shortcuts
422    fn handle_mode_switching(&mut self, key_event: crate::event::KeyEvent) {
423        // Ctrl+1: Chat mode
424        if key_event.modifiers.ctrl && key_event.code == crate::event::KeyCode::Char('1') {
425            self.switch_mode(AppMode::Chat);
426            return;
427        }
428
429        // Ctrl+2: Command mode
430        if key_event.modifiers.ctrl && key_event.code == crate::event::KeyCode::Char('2') {
431            self.switch_mode(AppMode::Command);
432            return;
433        }
434
435        // Ctrl+3: Diff mode
436        if key_event.modifiers.ctrl && key_event.code == crate::event::KeyCode::Char('3') {
437            self.switch_mode(AppMode::Diff);
438            return;
439        }
440
441        // Ctrl+4: Help mode
442        if key_event.modifiers.ctrl && key_event.code == crate::event::KeyCode::Char('4') {
443            self.switch_mode(AppMode::Help);
444            return;
445        }
446
447        // Ctrl+M: Cycle to next mode
448        if key_event.modifiers.ctrl && key_event.code == crate::event::KeyCode::Char('m') {
449            self.next_mode();
450            return;
451        }
452
453        // Ctrl+Shift+M: Cycle to previous mode
454        if key_event.modifiers.ctrl
455            && key_event.modifiers.shift
456            && key_event.code == crate::event::KeyCode::Char('m')
457        {
458            self.previous_mode_switch();
459            return;
460        }
461
462        // Tab: Toggle between current and previous mode
463        if key_event.code == crate::event::KeyCode::Tab && key_event.modifiers.alt {
464            self.toggle_mode();
465        }
466    }
467
468    /// Switch mode with accessibility announcement
469    pub fn switch_mode_with_announcement(&mut self, mode: AppMode) {
470        self.switch_mode(mode);
471
472        // Announce mode change if screen reader is enabled
473        if self.config.accessibility.screen_reader_enabled {
474            self.screen_reader
475                .announce_state_change("Mode", &format!("switched to {}", mode.display_name()));
476        }
477    }
478
479    /// Announce a message to screen readers
480    pub fn announce(&mut self, message: impl Into<String>) {
481        if self.config.accessibility.announcements_enabled {
482            use crate::accessibility::AnnouncementPriority;
483            self.screen_reader
484                .announce(message, AnnouncementPriority::Normal);
485        }
486    }
487
488    /// Announce an error to screen readers
489    pub fn announce_error(&mut self, message: impl Into<String>) {
490        if self.config.accessibility.announcements_enabled {
491            self.screen_reader.announce_error(message);
492        }
493    }
494
495    /// Announce a success to screen readers
496    pub fn announce_success(&mut self, message: impl Into<String>) {
497        if self.config.accessibility.announcements_enabled {
498            self.screen_reader.announce_success(message);
499        }
500    }
501
502    /// Enable or disable screen reader support
503    pub fn set_screen_reader_enabled(&mut self, enabled: bool) {
504        self.config.accessibility.screen_reader_enabled = enabled;
505        if enabled {
506            self.screen_reader.enable();
507        } else {
508            self.screen_reader.disable();
509        }
510    }
511
512    /// Enable or disable high contrast mode
513    pub fn set_high_contrast_enabled(&mut self, enabled: bool) {
514        self.config.accessibility.high_contrast_enabled = enabled;
515        if enabled {
516            // Switch to high contrast theme
517            if let Err(e) = self.theme_manager.switch_by_name("high-contrast") {
518                tracing::warn!("Failed to switch to high contrast theme: {}", e);
519            }
520        }
521    }
522
523    /// Handle Tab key for keyboard navigation
524    pub fn handle_tab_navigation(&mut self, shift: bool) {
525        if shift {
526            // Shift+Tab: Focus previous element
527            if let Some(focused) = self.keyboard_nav.focus_previous() {
528                if self.config.accessibility.screen_reader_enabled {
529                    self.screen_reader.announce(
530                        format!("Focused: {}", focused.full_description()),
531                        crate::accessibility::AnnouncementPriority::Normal,
532                    );
533                }
534            }
535        } else {
536            // Tab: Focus next element
537            if let Some(focused) = self.keyboard_nav.focus_next() {
538                if self.config.accessibility.screen_reader_enabled {
539                    self.screen_reader.announce(
540                        format!("Focused: {}", focused.full_description()),
541                        crate::accessibility::AnnouncementPriority::Normal,
542                    );
543                }
544            }
545        }
546    }
547
548    /// Get the currently focused element description
549    pub fn get_focused_element_description(&self) -> Option<String> {
550        self.keyboard_nav
551            .current_focus()
552            .map(|alt| alt.full_description())
553    }
554
555    /// Register an element for keyboard navigation
556    pub fn register_keyboard_element(
557        &mut self,
558        alternative: crate::accessibility::TextAlternative,
559    ) {
560        self.keyboard_nav.register_element(alternative);
561    }
562
563    /// Clear all keyboard navigation elements
564    pub fn clear_keyboard_elements(&mut self) {
565        self.keyboard_nav.clear();
566    }
567
568    /// Enable or disable animations
569    pub fn set_animations_enabled(&mut self, enabled: bool) {
570        self.config.accessibility.animations.enabled = enabled;
571        self.config.animations = enabled;
572
573        if self.config.accessibility.screen_reader_enabled {
574            let state = if enabled { "enabled" } else { "disabled" };
575            self.screen_reader.announce(
576                format!("Animations {}", state),
577                crate::accessibility::AnnouncementPriority::Normal,
578            );
579        }
580    }
581
582    /// Enable or disable reduce motion (for accessibility)
583    pub fn set_reduce_motion(&mut self, enabled: bool) {
584        self.config.accessibility.animations.reduce_motion = enabled;
585
586        if self.config.accessibility.screen_reader_enabled {
587            let state = if enabled { "enabled" } else { "disabled" };
588            self.screen_reader.announce(
589                format!("Reduce motion {}", state),
590                crate::accessibility::AnnouncementPriority::Normal,
591            );
592        }
593    }
594
595    /// Set animation speed multiplier
596    pub fn set_animation_speed(&mut self, speed: f32) {
597        let clamped_speed = speed.clamp(0.1, 2.0);
598        self.config.accessibility.animations.speed = clamped_speed;
599
600        if self.config.accessibility.screen_reader_enabled {
601            self.screen_reader.announce(
602                format!("Animation speed set to {:.1}x", clamped_speed),
603                crate::accessibility::AnnouncementPriority::Normal,
604            );
605        }
606    }
607
608    /// Check if animations should be displayed
609    pub fn should_animate(&self) -> bool {
610        self.config.accessibility.animations.should_animate()
611    }
612
613    /// Get animation duration in milliseconds
614    pub fn animation_duration_ms(&self, base_ms: u32) -> u32 {
615        self.config.accessibility.animations.duration_ms(base_ms)
616    }
617
618    /// Announce a state change
619    pub fn announce_state_change(&mut self, event: StateChangeEvent) {
620        if self.config.accessibility.announcements_enabled {
621            self.screen_reader
622                .announce(event.announcement_text(), event.priority);
623        }
624    }
625
626    /// Set focus to an element and announce it
627    pub fn set_focus_with_announcement(&mut self, element_id: impl Into<String>) {
628        let id = element_id.into();
629        self.focus_manager.set_focus(&id);
630
631        if self.config.accessibility.screen_reader_enabled {
632            self.screen_reader.announce(
633                format!("Focus moved to {}", id),
634                crate::accessibility::AnnouncementPriority::Normal,
635            );
636        }
637    }
638
639    /// Restore previous focus
640    pub fn restore_focus(&mut self) {
641        if let Some(element_id) = self.focus_manager.restore_focus() {
642            if self.config.accessibility.screen_reader_enabled {
643                self.screen_reader.announce(
644                    format!("Focus restored to {}", element_id),
645                    crate::accessibility::AnnouncementPriority::Normal,
646                );
647            }
648        }
649    }
650
651    /// Announce an operation status
652    pub fn announce_operation_status(&mut self, operation: &str, status: &str) {
653        if self.config.accessibility.announcements_enabled {
654            let priority = if status.contains("error") || status.contains("failed") {
655                crate::accessibility::AnnouncementPriority::High
656            } else if status.contains("success") || status.contains("complete") {
657                crate::accessibility::AnnouncementPriority::Normal
658            } else {
659                crate::accessibility::AnnouncementPriority::Low
660            };
661
662            self.screen_reader
663                .announce(format!("{}: {}", operation, status), priority);
664        }
665    }
666}