1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum AppMode {
18 Chat,
20 Command,
22 Diff,
24 Help,
26}
27
28impl AppMode {
29 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 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 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 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#[derive(Debug, Clone)]
72pub struct ChatState {
73 pub messages: Vec<String>,
75 pub input: String,
77 pub streaming: bool,
79 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
95pub struct App {
97 pub mode: AppMode,
99 pub previous_mode: AppMode,
101 pub chat: ChatState,
103 pub theme_manager: ThemeManager,
105 pub theme: Theme,
107 pub config: TuiConfig,
109 pub should_exit: bool,
111 pub event_loop: EventLoop,
113 pub renderer: Renderer,
115 pub keybindings_enabled: bool,
117 pub widget_integration: WidgetIntegration,
119 pub screen_reader: ScreenReaderAnnouncer,
121 pub keyboard_nav: KeyboardNavigationManager,
123 pub focus_manager: FocusManager,
125 pub provider_integration: crate::provider_integration::ProviderIntegration,
127 pub image_integration: ImageIntegration,
130 pub image_widget: crate::image_widget::ImageWidget,
133}
134
135impl App {
136 pub fn new() -> Result<Self> {
138 let config = TuiConfig::load()?;
139 Self::with_config(config)
140 }
141
142 pub fn with_config(config: TuiConfig) -> Result<Self> {
144 let theme_manager = ThemeManager::new();
145
146 theme_manager.load_from_config(&config)?;
148 let theme = theme_manager.current()?;
149
150 let widget_integration = WidgetIntegration::new(80, 24);
152
153 let screen_reader = ScreenReaderAnnouncer::new(config.accessibility.screen_reader_enabled);
155 let keyboard_nav = KeyboardNavigationManager::new();
156 let focus_manager = FocusManager::new();
157
158 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 pub async fn run(&mut self) -> Result<()> {
189 tracing::info!("Starting RiceCoder TUI");
190
191 while !self.should_exit {
193 if let Some(event) = self.event_loop.poll().await? {
195 self.handle_event(event)?;
196 }
197
198 self.renderer.render(self)?;
200 }
201
202 tracing::info!("RiceCoder TUI exited successfully");
203 Ok(())
204 }
205
206 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 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 pub fn next_mode(&mut self) {
225 let next = self.mode.next();
226 self.switch_mode(next);
227 }
228
229 pub fn previous_mode_switch(&mut self) {
231 let prev = self.mode.previous();
232 self.switch_mode(prev);
233 }
234
235 pub fn toggle_mode(&mut self) {
237 let prev = self.previous_mode;
238 self.switch_mode(prev);
239 }
240
241 pub fn current_mode_name(&self) -> &'static str {
243 self.mode.display_name()
244 }
245
246 pub fn current_mode_shortcut(&self) -> &'static str {
248 self.mode.shortcut()
249 }
250
251 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 pub fn available_themes(&self) -> Vec<&'static str> {
263 self.theme_manager.available_themes()
264 }
265
266 pub fn current_theme_name(&self) -> Result<String> {
268 self.theme_manager.current_name()
269 }
270
271 pub fn sync_widget_state(&mut self) {
273 let mode = self.mode;
275
276 self.widget_integration.widgets.prompt.context.mode = mode;
278 tracing::debug!("Widget state synchronized for mode: {:?}", mode);
279 }
280
281 pub fn widgets(&self) -> &crate::integration::WidgetContainer {
283 &self.widget_integration.widgets
284 }
285
286 pub fn widgets_mut(&mut self) -> &mut crate::integration::WidgetContainer {
288 &mut self.widget_integration.widgets
289 }
290
291 pub fn layout(&self) -> &crate::integration::LayoutCoordinator {
293 &self.widget_integration.layout
294 }
295
296 fn handle_event(&mut self, event: Event) -> Result<()> {
298 match event {
299 Event::Key(key_event) => {
300 tracing::debug!("Key event: {:?}", key_event);
301 if key_event.code == crate::event::KeyCode::Esc {
303 self.should_exit = true;
304 }
305
306 if key_event.code == crate::event::KeyCode::Tab {
308 self.handle_tab_navigation(key_event.modifiers.shift);
309 return Ok(());
310 }
311
312 if self.keybindings_enabled {
314 self.handle_mode_switching(key_event);
315 }
316 }
317 Event::Mouse(_mouse_event) => {
318 tracing::debug!("Mouse event");
319 }
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 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 }
334 Event::DragDrop { paths } => {
335 tracing::debug!("Drag-and-drop event with {} files", paths.len());
336 self.handle_drag_drop_event(paths)?;
339 }
340 }
341 Ok(())
342 }
343
344 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 let (added, errors) = self.image_integration.handle_drag_drop_event(paths);
362
363 if !added.is_empty() {
366 self.image_widget.add_images(added.clone());
367 tracing::info!("Updated image widget with {} images", added.len());
368
369 self.chat.prompt_context.add_images(added.clone());
372 tracing::info!("Added {} images to prompt context", added.len());
373 }
374
375 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 pub fn sync_prompt_context(&mut self) {
400 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 pub fn get_prompt_context(&self) -> &crate::prompt_context::PromptContext {
413 &self.chat.prompt_context
414 }
415
416 pub fn get_prompt_context_mut(&mut self) -> &mut crate::prompt_context::PromptContext {
418 &mut self.chat.prompt_context
419 }
420
421 fn handle_mode_switching(&mut self, key_event: crate::event::KeyEvent) {
423 if key_event.modifiers.ctrl && key_event.code == crate::event::KeyCode::Char('1') {
425 self.switch_mode(AppMode::Chat);
426 return;
427 }
428
429 if key_event.modifiers.ctrl && key_event.code == crate::event::KeyCode::Char('2') {
431 self.switch_mode(AppMode::Command);
432 return;
433 }
434
435 if key_event.modifiers.ctrl && key_event.code == crate::event::KeyCode::Char('3') {
437 self.switch_mode(AppMode::Diff);
438 return;
439 }
440
441 if key_event.modifiers.ctrl && key_event.code == crate::event::KeyCode::Char('4') {
443 self.switch_mode(AppMode::Help);
444 return;
445 }
446
447 if key_event.modifiers.ctrl && key_event.code == crate::event::KeyCode::Char('m') {
449 self.next_mode();
450 return;
451 }
452
453 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 if key_event.code == crate::event::KeyCode::Tab && key_event.modifiers.alt {
464 self.toggle_mode();
465 }
466 }
467
468 pub fn switch_mode_with_announcement(&mut self, mode: AppMode) {
470 self.switch_mode(mode);
471
472 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 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 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 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 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 pub fn set_high_contrast_enabled(&mut self, enabled: bool) {
514 self.config.accessibility.high_contrast_enabled = enabled;
515 if enabled {
516 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 pub fn handle_tab_navigation(&mut self, shift: bool) {
525 if shift {
526 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 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 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 pub fn register_keyboard_element(
557 &mut self,
558 alternative: crate::accessibility::TextAlternative,
559 ) {
560 self.keyboard_nav.register_element(alternative);
561 }
562
563 pub fn clear_keyboard_elements(&mut self) {
565 self.keyboard_nav.clear();
566 }
567
568 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 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 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 pub fn should_animate(&self) -> bool {
610 self.config.accessibility.animations.should_animate()
611 }
612
613 pub fn animation_duration_ms(&self, base_ms: u32) -> u32 {
615 self.config.accessibility.animations.duration_ms(base_ms)
616 }
617
618 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 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 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 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}