1use 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
14pub struct WidgetContainer {
16 pub chat: ChatWidget,
18 pub diff: DiffWidget,
20 pub prompt: PromptWidget,
22 pub menu: MenuWidget,
24 pub list: ListWidget,
26 pub dialog: Option<DialogWidget>,
28 pub split_view: Option<SplitViewWidget>,
30 pub tabs: Option<TabWidget>,
32}
33
34impl WidgetContainer {
35 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 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 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
78pub struct LayoutCoordinator {
80 pub width: u16,
82 pub height: u16,
84 pub min_width: u16,
86 pub min_height: u16,
88}
89
90impl LayoutCoordinator {
91 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 pub fn is_valid(&self) -> bool {
103 self.width >= self.min_width && self.height >= self.min_height
104 }
105
106 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 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 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 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 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#[derive(Debug, Clone)]
237pub struct ChatLayout {
238 pub chat_area: Rect,
240 pub prompt_area: Rect,
242}
243
244#[derive(Debug, Clone)]
246pub struct DiffLayout {
247 pub diff_area: Rect,
249 pub prompt_area: Rect,
251}
252
253#[derive(Debug, Clone)]
255pub struct CommandLayout {
256 pub menu_area: Rect,
258 pub prompt_area: Rect,
260}
261
262#[derive(Debug, Clone)]
264pub struct HelpLayout {
265 pub help_area: Rect,
267 pub prompt_area: Rect,
269}
270
271pub struct StateSynchronizer;
273
274impl StateSynchronizer {
275 pub fn sync_chat_to_prompt(chat: &ChatWidget, _prompt: &mut PromptWidget) {
277 if !chat.messages.is_empty() {
279 tracing::debug!("Syncing chat state to prompt");
281 }
282 }
283
284 pub fn sync_prompt_to_chat(prompt: &PromptWidget, _chat: &mut ChatWidget) {
286 if !prompt.input.is_empty() {
288 tracing::debug!("Syncing prompt input to chat: {}", prompt.input);
289 }
290 }
291
292 pub fn sync_diff_to_prompt(diff: &DiffWidget, _prompt: &mut PromptWidget) {
294 let approved = diff.approved_hunks().len();
296 let total = diff.hunks.len();
297 tracing::debug!("Diff state: {}/{} hunks approved", approved, total);
298 }
299
300 pub fn sync_app_state(app: &App, widgets: &mut WidgetContainer) {
302 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 tracing::debug!("Syncing command mode state");
313 }
314 AppMode::Help => {
315 tracing::debug!("Syncing help mode state");
317 }
318 }
319 }
320}
321
322pub struct WidgetIntegration {
324 pub widgets: WidgetContainer,
326 pub layout: LayoutCoordinator,
328}
329
330impl WidgetIntegration {
331 pub fn new(width: u16, height: u16) -> Self {
333 Self {
334 widgets: WidgetContainer::new(),
335 layout: LayoutCoordinator::new(width, height),
336 }
337 }
338
339 pub fn initialize(&mut self, app: &App) -> Result<()> {
341 self.widgets.prompt.context.mode = app.mode;
343 self.widgets.prompt.context.project_name = Some("ricecoder".to_string());
344
345 self.widgets.chat = ChatWidget::new();
347
348 self.widgets.diff = DiffWidget::new();
350
351 self.widgets.menu = MenuWidget::new();
353
354 tracing::info!("Widget integration initialized");
355 Ok(())
356 }
357
358 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 self.widgets.prompt.context.mode = new_mode;
364
365 match new_mode {
367 AppMode::Chat => {
368 if self.widgets.chat.messages.is_empty() {
370 tracing::debug!("Chat mode: initializing empty chat");
371 }
372 }
373 AppMode::Diff => {
374 if self.widgets.diff.hunks.is_empty() {
376 tracing::debug!("Diff mode: no hunks loaded");
377 }
378 }
379 AppMode::Command => {
380 if self.widgets.menu.is_empty() {
382 tracing::debug!("Command mode: initializing menu");
383 }
384 }
385 AppMode::Help => {
386 tracing::debug!("Help mode: showing help");
388 }
389 }
390
391 Ok(())
392 }
393
394 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 pub fn sync_state(&mut self, app: &App) {
409 StateSynchronizer::sync_app_state(app, &mut self.widgets);
410 }
411
412 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#[derive(Debug, Clone)]
443pub enum LayoutInfo {
444 Chat(ChatLayout),
446 Diff(DiffLayout),
448 Command(CommandLayout),
450 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 }
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 }
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 }
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}