ricecoder_tui/
integration.rs

1//! Widget integration and coordination
2//!
3//! This module handles wiring all widgets together, managing state synchronization,
4//! and coordinating layout between different UI components.
5
6use crate::app::{App, AppMode};
7use crate::components::{DialogWidget, ListWidget, MenuWidget, SplitViewWidget, TabWidget};
8use crate::diff::DiffWidget;
9use crate::layout::Rect;
10use crate::prompt::PromptWidget;
11use crate::widgets::ChatWidget;
12use anyhow::Result;
13
14/// Widget container for managing all active widgets
15pub struct WidgetContainer {
16    /// Chat widget
17    pub chat: ChatWidget,
18    /// Diff widget
19    pub diff: DiffWidget,
20    /// Prompt widget
21    pub prompt: PromptWidget,
22    /// Menu widget
23    pub menu: MenuWidget,
24    /// List widget
25    pub list: ListWidget,
26    /// Dialog widget (optional)
27    pub dialog: Option<DialogWidget>,
28    /// Split view widget (optional)
29    pub split_view: Option<SplitViewWidget>,
30    /// Tab widget (optional)
31    pub tabs: Option<TabWidget>,
32}
33
34impl WidgetContainer {
35    /// Create a new widget container
36    pub fn new() -> Self {
37        Self {
38            chat: ChatWidget::new(),
39            diff: DiffWidget::new(),
40            prompt: PromptWidget::new(),
41            menu: MenuWidget::new(),
42            list: ListWidget::new(),
43            dialog: None,
44            split_view: None,
45            tabs: None,
46        }
47    }
48
49    /// Reset all widgets to initial state
50    pub fn reset_all(&mut self) {
51        self.chat.clear();
52        self.diff = DiffWidget::new();
53        self.prompt = PromptWidget::new();
54        self.menu.clear();
55        self.list.clear();
56        self.dialog = None;
57        self.split_view = None;
58        self.tabs = None;
59    }
60
61    /// Get the active widget based on app mode
62    pub fn get_active_widget_mut(&mut self, mode: AppMode) -> Option<&mut dyn std::any::Any> {
63        match mode {
64            AppMode::Chat => Some(&mut self.chat as &mut dyn std::any::Any),
65            AppMode::Diff => Some(&mut self.diff as &mut dyn std::any::Any),
66            AppMode::Command => Some(&mut self.prompt as &mut dyn std::any::Any),
67            AppMode::Help => Some(&mut self.menu as &mut dyn std::any::Any),
68        }
69    }
70}
71
72impl Default for WidgetContainer {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78/// Layout coordinator for managing widget positioning
79pub struct LayoutCoordinator {
80    /// Terminal width
81    pub width: u16,
82    /// Terminal height
83    pub height: u16,
84    /// Minimum width requirement
85    pub min_width: u16,
86    /// Minimum height requirement
87    pub min_height: u16,
88}
89
90impl LayoutCoordinator {
91    /// Create a new layout coordinator
92    pub fn new(width: u16, height: u16) -> Self {
93        Self {
94            width,
95            height,
96            min_width: 80,
97            min_height: 24,
98        }
99    }
100
101    /// Check if terminal size is valid
102    pub fn is_valid(&self) -> bool {
103        self.width >= self.min_width && self.height >= self.min_height
104    }
105
106    /// Get layout for chat mode
107    pub fn layout_chat(&self) -> Result<ChatLayout> {
108        if !self.is_valid() {
109            return Err(anyhow::anyhow!(
110                "Terminal too small: {}x{}",
111                self.width,
112                self.height
113            ));
114        }
115
116        let prompt_height = 3;
117        let chat_height = self.height.saturating_sub(prompt_height);
118
119        Ok(ChatLayout {
120            chat_area: Rect {
121                x: 0,
122                y: 0,
123                width: self.width,
124                height: chat_height,
125            },
126            prompt_area: Rect {
127                x: 0,
128                y: chat_height,
129                width: self.width,
130                height: prompt_height,
131            },
132        })
133    }
134
135    /// Get layout for diff mode
136    pub fn layout_diff(&self) -> Result<DiffLayout> {
137        if !self.is_valid() {
138            return Err(anyhow::anyhow!(
139                "Terminal too small: {}x{}",
140                self.width,
141                self.height
142            ));
143        }
144
145        let prompt_height = 3;
146        let diff_height = self.height.saturating_sub(prompt_height);
147
148        Ok(DiffLayout {
149            diff_area: Rect {
150                x: 0,
151                y: 0,
152                width: self.width,
153                height: diff_height,
154            },
155            prompt_area: Rect {
156                x: 0,
157                y: diff_height,
158                width: self.width,
159                height: prompt_height,
160            },
161        })
162    }
163
164    /// Get layout for command mode
165    pub fn layout_command(&self) -> Result<CommandLayout> {
166        if !self.is_valid() {
167            return Err(anyhow::anyhow!(
168                "Terminal too small: {}x{}",
169                self.width,
170                self.height
171            ));
172        }
173
174        let prompt_height = 3;
175        let menu_height = self.height.saturating_sub(prompt_height);
176
177        Ok(CommandLayout {
178            menu_area: Rect {
179                x: 0,
180                y: 0,
181                width: self.width,
182                height: menu_height,
183            },
184            prompt_area: Rect {
185                x: 0,
186                y: menu_height,
187                width: self.width,
188                height: prompt_height,
189            },
190        })
191    }
192
193    /// Get layout for help mode
194    pub fn layout_help(&self) -> Result<HelpLayout> {
195        if !self.is_valid() {
196            return Err(anyhow::anyhow!(
197                "Terminal too small: {}x{}",
198                self.width,
199                self.height
200            ));
201        }
202
203        let prompt_height = 3;
204        let help_height = self.height.saturating_sub(prompt_height);
205
206        Ok(HelpLayout {
207            help_area: Rect {
208                x: 0,
209                y: 0,
210                width: self.width,
211                height: help_height,
212            },
213            prompt_area: Rect {
214                x: 0,
215                y: help_height,
216                width: self.width,
217                height: prompt_height,
218            },
219        })
220    }
221
222    /// Update terminal size
223    pub fn update_size(&mut self, width: u16, height: u16) {
224        self.width = width;
225        self.height = height;
226    }
227}
228
229impl Default for LayoutCoordinator {
230    fn default() -> Self {
231        Self::new(80, 24)
232    }
233}
234
235/// Chat mode layout
236#[derive(Debug, Clone)]
237pub struct ChatLayout {
238    /// Chat display area
239    pub chat_area: Rect,
240    /// Prompt input area
241    pub prompt_area: Rect,
242}
243
244/// Diff mode layout
245#[derive(Debug, Clone)]
246pub struct DiffLayout {
247    /// Diff display area
248    pub diff_area: Rect,
249    /// Prompt input area
250    pub prompt_area: Rect,
251}
252
253/// Command mode layout
254#[derive(Debug, Clone)]
255pub struct CommandLayout {
256    /// Menu display area
257    pub menu_area: Rect,
258    /// Prompt input area
259    pub prompt_area: Rect,
260}
261
262/// Help mode layout
263#[derive(Debug, Clone)]
264pub struct HelpLayout {
265    /// Help display area
266    pub help_area: Rect,
267    /// Prompt input area
268    pub prompt_area: Rect,
269}
270
271/// State synchronizer for keeping widgets in sync
272pub struct StateSynchronizer;
273
274impl StateSynchronizer {
275    /// Synchronize chat state with prompt
276    pub fn sync_chat_to_prompt(chat: &ChatWidget, _prompt: &mut PromptWidget) {
277        // Update prompt context based on chat state
278        if !chat.messages.is_empty() {
279            // Could update prompt indicators based on chat state
280            tracing::debug!("Syncing chat state to prompt");
281        }
282    }
283
284    /// Synchronize prompt state with chat
285    pub fn sync_prompt_to_chat(prompt: &PromptWidget, _chat: &mut ChatWidget) {
286        // Update chat based on prompt input
287        if !prompt.input.is_empty() {
288            tracing::debug!("Syncing prompt input to chat: {}", prompt.input);
289        }
290    }
291
292    /// Synchronize diff state with prompt
293    pub fn sync_diff_to_prompt(diff: &DiffWidget, _prompt: &mut PromptWidget) {
294        // Update prompt context based on diff state
295        let approved = diff.approved_hunks().len();
296        let total = diff.hunks.len();
297        tracing::debug!("Diff state: {}/{} hunks approved", approved, total);
298    }
299
300    /// Synchronize app state across all widgets
301    pub fn sync_app_state(app: &App, widgets: &mut WidgetContainer) {
302        // Update all widgets based on app state
303        match app.mode {
304            AppMode::Chat => {
305                Self::sync_chat_to_prompt(&widgets.chat, &mut widgets.prompt);
306            }
307            AppMode::Diff => {
308                Self::sync_diff_to_prompt(&widgets.diff, &mut widgets.prompt);
309            }
310            AppMode::Command => {
311                // Command mode specific sync
312                tracing::debug!("Syncing command mode state");
313            }
314            AppMode::Help => {
315                // Help mode specific sync
316                tracing::debug!("Syncing help mode state");
317            }
318        }
319    }
320}
321
322/// Widget integration manager
323pub struct WidgetIntegration {
324    /// Widget container
325    pub widgets: WidgetContainer,
326    /// Layout coordinator
327    pub layout: LayoutCoordinator,
328}
329
330impl WidgetIntegration {
331    /// Create a new widget integration manager
332    pub fn new(width: u16, height: u16) -> Self {
333        Self {
334            widgets: WidgetContainer::new(),
335            layout: LayoutCoordinator::new(width, height),
336        }
337    }
338
339    /// Initialize widgets for the app
340    pub fn initialize(&mut self, app: &App) -> Result<()> {
341        // Initialize prompt with context
342        self.widgets.prompt.context.mode = app.mode;
343        self.widgets.prompt.context.project_name = Some("ricecoder".to_string());
344
345        // Initialize chat widget
346        self.widgets.chat = ChatWidget::new();
347
348        // Initialize diff widget
349        self.widgets.diff = DiffWidget::new();
350
351        // Initialize menu widget
352        self.widgets.menu = MenuWidget::new();
353
354        tracing::info!("Widget integration initialized");
355        Ok(())
356    }
357
358    /// Handle mode switch
359    pub fn on_mode_switch(&mut self, old_mode: AppMode, new_mode: AppMode) -> Result<()> {
360        tracing::info!("Mode switch: {:?} -> {:?}", old_mode, new_mode);
361
362        // Update prompt context
363        self.widgets.prompt.context.mode = new_mode;
364
365        // Mode-specific initialization
366        match new_mode {
367            AppMode::Chat => {
368                // Ensure chat widget is ready
369                if self.widgets.chat.messages.is_empty() {
370                    tracing::debug!("Chat mode: initializing empty chat");
371                }
372            }
373            AppMode::Diff => {
374                // Ensure diff widget is ready
375                if self.widgets.diff.hunks.is_empty() {
376                    tracing::debug!("Diff mode: no hunks loaded");
377                }
378            }
379            AppMode::Command => {
380                // Ensure menu widget is ready
381                if self.widgets.menu.is_empty() {
382                    tracing::debug!("Command mode: initializing menu");
383                }
384            }
385            AppMode::Help => {
386                // Ensure help is ready
387                tracing::debug!("Help mode: showing help");
388            }
389        }
390
391        Ok(())
392    }
393
394    /// Handle terminal resize
395    pub fn on_resize(&mut self, width: u16, height: u16) -> Result<()> {
396        self.layout.update_size(width, height);
397
398        if !self.layout.is_valid() {
399            tracing::warn!("Terminal size too small: {}x{}", width, height);
400            return Err(anyhow::anyhow!("Terminal too small: {}x{}", width, height));
401        }
402
403        tracing::debug!("Terminal resized to {}x{}", width, height);
404        Ok(())
405    }
406
407    /// Synchronize state across all widgets
408    pub fn sync_state(&mut self, app: &App) {
409        StateSynchronizer::sync_app_state(app, &mut self.widgets);
410    }
411
412    /// Get layout for current mode
413    pub fn get_layout(&self, mode: AppMode) -> Result<LayoutInfo> {
414        match mode {
415            AppMode::Chat => {
416                let layout = self.layout.layout_chat()?;
417                Ok(LayoutInfo::Chat(layout))
418            }
419            AppMode::Diff => {
420                let layout = self.layout.layout_diff()?;
421                Ok(LayoutInfo::Diff(layout))
422            }
423            AppMode::Command => {
424                let layout = self.layout.layout_command()?;
425                Ok(LayoutInfo::Command(layout))
426            }
427            AppMode::Help => {
428                let layout = self.layout.layout_help()?;
429                Ok(LayoutInfo::Help(layout))
430            }
431        }
432    }
433}
434
435impl Default for WidgetIntegration {
436    fn default() -> Self {
437        Self::new(80, 24)
438    }
439}
440
441/// Layout information for different modes
442#[derive(Debug, Clone)]
443pub enum LayoutInfo {
444    /// Chat mode layout
445    Chat(ChatLayout),
446    /// Diff mode layout
447    Diff(DiffLayout),
448    /// Command mode layout
449    Command(CommandLayout),
450    /// Help mode layout
451    Help(HelpLayout),
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    #[test]
459    fn test_widget_container_creation() {
460        let container = WidgetContainer::new();
461        assert!(container.chat.messages.is_empty());
462        assert!(container.diff.hunks.is_empty());
463        assert!(container.dialog.is_none());
464    }
465
466    #[test]
467    fn test_widget_container_reset() {
468        let mut container = WidgetContainer::new();
469        container
470            .chat
471            .add_message(crate::widgets::Message::user("test"));
472        assert_eq!(container.chat.messages.len(), 1);
473
474        container.reset_all();
475        assert!(container.chat.messages.is_empty());
476    }
477
478    #[test]
479    fn test_layout_coordinator_creation() {
480        let coordinator = LayoutCoordinator::new(80, 24);
481        assert_eq!(coordinator.width, 80);
482        assert_eq!(coordinator.height, 24);
483        assert!(coordinator.is_valid());
484    }
485
486    #[test]
487    fn test_layout_coordinator_invalid_size() {
488        let coordinator = LayoutCoordinator::new(40, 12);
489        assert!(!coordinator.is_valid());
490    }
491
492    #[test]
493    fn test_layout_coordinator_chat_layout() {
494        let coordinator = LayoutCoordinator::new(80, 24);
495        let layout = coordinator.layout_chat().unwrap();
496        assert_eq!(layout.chat_area.width, 80);
497        assert_eq!(layout.prompt_area.height, 3);
498    }
499
500    #[test]
501    fn test_layout_coordinator_diff_layout() {
502        let coordinator = LayoutCoordinator::new(80, 24);
503        let layout = coordinator.layout_diff().unwrap();
504        assert_eq!(layout.diff_area.width, 80);
505        assert_eq!(layout.prompt_area.height, 3);
506    }
507
508    #[test]
509    fn test_layout_coordinator_command_layout() {
510        let coordinator = LayoutCoordinator::new(80, 24);
511        let layout = coordinator.layout_command().unwrap();
512        assert_eq!(layout.menu_area.width, 80);
513        assert_eq!(layout.prompt_area.height, 3);
514    }
515
516    #[test]
517    fn test_layout_coordinator_help_layout() {
518        let coordinator = LayoutCoordinator::new(80, 24);
519        let layout = coordinator.layout_help().unwrap();
520        assert_eq!(layout.help_area.width, 80);
521        assert_eq!(layout.prompt_area.height, 3);
522    }
523
524    #[test]
525    fn test_layout_coordinator_update_size() {
526        let mut coordinator = LayoutCoordinator::new(80, 24);
527        coordinator.update_size(120, 40);
528        assert_eq!(coordinator.width, 120);
529        assert_eq!(coordinator.height, 40);
530    }
531
532    #[test]
533    fn test_widget_integration_creation() {
534        let integration = WidgetIntegration::new(80, 24);
535        assert_eq!(integration.layout.width, 80);
536        assert_eq!(integration.layout.height, 24);
537    }
538
539    #[test]
540    fn test_widget_integration_on_resize() {
541        let mut integration = WidgetIntegration::new(80, 24);
542        let result = integration.on_resize(100, 30);
543        assert!(result.is_ok());
544        assert_eq!(integration.layout.width, 100);
545        assert_eq!(integration.layout.height, 30);
546    }
547
548    #[test]
549    fn test_widget_integration_on_resize_invalid() {
550        let mut integration = WidgetIntegration::new(80, 24);
551        let result = integration.on_resize(40, 12);
552        assert!(result.is_err());
553    }
554
555    #[test]
556    fn test_widget_integration_mode_switch() {
557        let mut integration = WidgetIntegration::new(80, 24);
558        let result = integration.on_mode_switch(AppMode::Chat, AppMode::Diff);
559        assert!(result.is_ok());
560        assert_eq!(integration.widgets.prompt.context.mode, AppMode::Diff);
561    }
562
563    #[test]
564    fn test_widget_integration_get_layout_chat() {
565        let integration = WidgetIntegration::new(80, 24);
566        let layout = integration.get_layout(AppMode::Chat);
567        assert!(layout.is_ok());
568        match layout.unwrap() {
569            LayoutInfo::Chat(_) => {}
570            _ => panic!("Expected Chat layout"),
571        }
572    }
573
574    #[test]
575    fn test_widget_integration_get_layout_diff() {
576        let integration = WidgetIntegration::new(80, 24);
577        let layout = integration.get_layout(AppMode::Diff);
578        assert!(layout.is_ok());
579        match layout.unwrap() {
580            LayoutInfo::Diff(_) => {}
581            _ => panic!("Expected Diff layout"),
582        }
583    }
584
585    #[test]
586    fn test_widget_integration_get_layout_command() {
587        let integration = WidgetIntegration::new(80, 24);
588        let layout = integration.get_layout(AppMode::Command);
589        assert!(layout.is_ok());
590        match layout.unwrap() {
591            LayoutInfo::Command(_) => {}
592            _ => panic!("Expected Command layout"),
593        }
594    }
595
596    #[test]
597    fn test_widget_integration_get_layout_help() {
598        let integration = WidgetIntegration::new(80, 24);
599        let layout = integration.get_layout(AppMode::Help);
600        assert!(layout.is_ok());
601        match layout.unwrap() {
602            LayoutInfo::Help(_) => {}
603            _ => panic!("Expected Help layout"),
604        }
605    }
606
607    #[test]
608    fn test_state_synchronizer_sync_chat_to_prompt() {
609        let chat = crate::widgets::ChatWidget::new();
610        let mut prompt = crate::prompt::PromptWidget::new();
611        StateSynchronizer::sync_chat_to_prompt(&chat, &mut prompt);
612        // Should not panic
613    }
614
615    #[test]
616    fn test_state_synchronizer_sync_prompt_to_chat() {
617        let prompt = crate::prompt::PromptWidget::new();
618        let mut chat = crate::widgets::ChatWidget::new();
619        StateSynchronizer::sync_prompt_to_chat(&prompt, &mut chat);
620        // Should not panic
621    }
622
623    #[test]
624    fn test_state_synchronizer_sync_diff_to_prompt() {
625        let diff = crate::diff::DiffWidget::new();
626        let mut prompt = crate::prompt::PromptWidget::new();
627        StateSynchronizer::sync_diff_to_prompt(&diff, &mut prompt);
628        // Should not panic
629    }
630
631    #[test]
632    fn test_layout_info_variants() {
633        let chat_layout = ChatLayout {
634            chat_area: Rect {
635                x: 0,
636                y: 0,
637                width: 80,
638                height: 21,
639            },
640            prompt_area: Rect {
641                x: 0,
642                y: 21,
643                width: 80,
644                height: 3,
645            },
646        };
647        let _info = LayoutInfo::Chat(chat_layout);
648
649        let diff_layout = DiffLayout {
650            diff_area: Rect {
651                x: 0,
652                y: 0,
653                width: 80,
654                height: 21,
655            },
656            prompt_area: Rect {
657                x: 0,
658                y: 21,
659                width: 80,
660                height: 3,
661            },
662        };
663        let _info = LayoutInfo::Diff(diff_layout);
664
665        let command_layout = CommandLayout {
666            menu_area: Rect {
667                x: 0,
668                y: 0,
669                width: 80,
670                height: 21,
671            },
672            prompt_area: Rect {
673                x: 0,
674                y: 21,
675                width: 80,
676                height: 3,
677            },
678        };
679        let _info = LayoutInfo::Command(command_layout);
680
681        let help_layout = HelpLayout {
682            help_area: Rect {
683                x: 0,
684                y: 0,
685                width: 80,
686                height: 21,
687            },
688            prompt_area: Rect {
689                x: 0,
690                y: 21,
691                width: 80,
692                height: 3,
693            },
694        };
695        let _info = LayoutInfo::Help(help_layout);
696    }
697}