1use chrono::Local;
5use ratatui::buffer::Buffer;
6use ratatui::crossterm::event::KeyCode;
7use ratatui::crossterm::event::KeyEvent;
8use ratatui::layout::Alignment;
9use ratatui::layout::Constraint;
10use ratatui::layout::Direction;
11use ratatui::layout::Layout;
12use ratatui::layout::Rect;
13use ratatui::style::Color;
14use ratatui::style::Modifier;
15use ratatui::style::Style;
16use ratatui::text::Line;
17use ratatui::text::Span;
18use ratatui::widgets::Block;
19use ratatui::widgets::BorderType;
20use ratatui::widgets::Borders;
21use ratatui::widgets::Clear;
22use ratatui::widgets::Gauge;
23use ratatui::widgets::Paragraph;
24use ratatui::widgets::Widget;
25use ratatui::widgets::WidgetRef;
26use ratatui::widgets::Wrap;
27use std::path::PathBuf;
28use std::time::Duration;
29use std::time::Instant;
30use uuid::Uuid;
31
32#[derive(Debug, Clone)]
34pub struct SaveSessionState {
35 pub session_name: String,
37 pub description: String,
39 pub focused_field: usize,
41 pub cursor_pos: usize,
43 pub save_location: PathBuf,
45 pub show_error: Option<String>,
47 pub save_progress: f64,
49 pub saving: bool,
51 pub save_start_time: Option<Instant>,
53 pub session_id: Option<Uuid>,
55 pub default_name: String,
57 pub show_success: bool,
59 pub estimated_time: Option<Duration>,
61}
62
63impl SaveSessionState {
64 pub fn new() -> Self {
65 let save_location = dirs::home_dir()
66 .map(|p| p.join(".agcodex/history"))
67 .unwrap_or_else(|| PathBuf::from(".agcodex/history"));
68
69 let default_name = Local::now().format("%Y-%m-%d_%H-%M").to_string();
71
72 Self {
73 session_name: default_name.clone(),
74 description: String::new(),
75 focused_field: 0,
76 cursor_pos: default_name.len(),
77 save_location,
78 show_error: None,
79 save_progress: 0.0,
80 saving: false,
81 save_start_time: None,
82 session_id: None,
83 default_name,
84 show_success: false,
85 estimated_time: None,
86 }
87 }
88
89 pub fn handle_key_event(&mut self, key: KeyEvent) -> SaveSessionAction {
91 if self.saving {
92 if key.code == KeyCode::Esc {
94 return SaveSessionAction::Cancel;
95 }
96 return SaveSessionAction::None;
97 }
98
99 if self.show_success {
100 return SaveSessionAction::Close;
102 }
103
104 match key.code {
105 KeyCode::Esc => SaveSessionAction::Cancel,
106 KeyCode::Enter => {
107 match self.focused_field {
108 0 | 1 => {
109 if self.focused_field == 0 && !self.session_name.trim().is_empty() {
111 self.focused_field = 1;
112 self.cursor_pos = self.description.len();
113 } else if self.focused_field == 1 {
114 self.focused_field = 2; }
116 SaveSessionAction::None
117 }
118 2 => {
119 if self.validate() {
121 SaveSessionAction::Save
122 } else {
123 SaveSessionAction::None
124 }
125 }
126 3 => SaveSessionAction::Cancel, _ => SaveSessionAction::None,
128 }
129 }
130 KeyCode::Tab => {
131 self.next_field();
132 SaveSessionAction::None
133 }
134 KeyCode::BackTab => {
135 self.prev_field();
136 SaveSessionAction::None
137 }
138 KeyCode::Char(c) => {
139 if self.focused_field <= 1 {
140 let current_text = if self.focused_field == 0 {
142 &self.session_name
143 } else {
144 &self.description
145 };
146
147 if current_text.len() < 100 {
148 self.insert_char(c);
149 }
150 }
151 SaveSessionAction::None
152 }
153 KeyCode::Backspace => {
154 if self.focused_field <= 1 {
155 self.delete_char();
156 }
157 SaveSessionAction::None
158 }
159 KeyCode::Delete => {
160 if self.focused_field <= 1 {
161 self.delete_char_forward();
162 }
163 SaveSessionAction::None
164 }
165 KeyCode::Left => {
166 if self.focused_field <= 1 {
167 self.move_cursor_left();
168 }
169 SaveSessionAction::None
170 }
171 KeyCode::Right => {
172 if self.focused_field <= 1 {
173 self.move_cursor_right();
174 }
175 SaveSessionAction::None
176 }
177 KeyCode::Home => {
178 if self.focused_field <= 1 {
179 self.cursor_pos = 0;
180 }
181 SaveSessionAction::None
182 }
183 KeyCode::End => {
184 if self.focused_field <= 1 {
185 self.cursor_pos = self.current_field_text().len();
186 }
187 SaveSessionAction::None
188 }
189 _ => SaveSessionAction::None,
190 }
191 }
192
193 fn next_field(&mut self) {
194 self.focused_field = (self.focused_field + 1) % 4;
195 if self.focused_field <= 1 {
196 self.cursor_pos = self.current_field_text().len();
197 }
198 }
199
200 fn prev_field(&mut self) {
201 self.focused_field = if self.focused_field == 0 {
202 3
203 } else {
204 self.focused_field - 1
205 };
206 if self.focused_field <= 1 {
207 self.cursor_pos = self.current_field_text().len();
208 }
209 }
210
211 fn current_field_text(&self) -> &str {
212 match self.focused_field {
213 0 => &self.session_name,
214 1 => &self.description,
215 _ => "",
216 }
217 }
218
219 fn current_field_text_mut(&mut self) -> &mut String {
220 match self.focused_field {
221 0 => &mut self.session_name,
222 1 => &mut self.description,
223 _ => panic!("Invalid field for text mutation"),
224 }
225 }
226
227 fn insert_char(&mut self, c: char) {
228 if self.focused_field <= 1 {
229 let cursor_pos = self.cursor_pos;
230 let text = self.current_field_text_mut();
231 text.insert(cursor_pos, c);
232 self.cursor_pos = cursor_pos + c.len_utf8();
233 self.show_error = None;
234 }
235 }
236
237 fn delete_char(&mut self) {
238 if self.focused_field <= 1 && self.cursor_pos > 0 {
239 let cursor_pos = self.cursor_pos;
240 let text = self.current_field_text_mut();
241 let char_boundary = text
242 .char_indices()
243 .rev()
244 .find(|(idx, _)| *idx < cursor_pos)
245 .map(|(idx, _)| idx)
246 .unwrap_or(0);
247 text.drain(char_boundary..cursor_pos);
248 self.cursor_pos = char_boundary;
249 self.show_error = None;
250 }
251 }
252
253 fn delete_char_forward(&mut self) {
254 if self.focused_field <= 1 {
255 let cursor_pos = self.cursor_pos;
256 let text = self.current_field_text_mut();
257 if cursor_pos < text.len() {
258 let next_char_boundary = text
259 .char_indices()
260 .find(|(idx, _)| *idx > cursor_pos)
261 .map(|(idx, _)| idx)
262 .unwrap_or(text.len());
263 text.drain(cursor_pos..next_char_boundary);
264 }
265 }
266 }
267
268 fn move_cursor_left(&mut self) {
269 if self.focused_field <= 1 && self.cursor_pos > 0 {
270 let text = self.current_field_text();
271 self.cursor_pos = text
272 .char_indices()
273 .rev()
274 .find(|(idx, _)| *idx < self.cursor_pos)
275 .map(|(idx, _)| idx)
276 .unwrap_or(0);
277 }
278 }
279
280 fn move_cursor_right(&mut self) {
281 if self.focused_field <= 1 {
282 let text = self.current_field_text();
283 if self.cursor_pos < text.len() {
284 self.cursor_pos = text
285 .char_indices()
286 .find(|(idx, _)| *idx > self.cursor_pos)
287 .map(|(idx, _)| idx)
288 .unwrap_or(text.len());
289 }
290 }
291 }
292
293 pub fn validate(&mut self) -> bool {
295 let name = self.session_name.trim();
296 if name.is_empty() {
297 self.show_error = Some("Session name cannot be empty".to_string());
298 self.focused_field = 0;
299 return false;
300 }
301
302 if name
304 .chars()
305 .any(|c| matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|'))
306 {
307 self.show_error = Some("Session name contains invalid characters".to_string());
308 self.focused_field = 0;
309 return false;
310 }
311
312 if name.len() > 100 {
314 self.show_error = Some("Session name too long (max 100 characters)".to_string());
315 self.focused_field = 0;
316 return false;
317 }
318
319 self.show_error = None;
320 true
321 }
322
323 pub fn start_save(&mut self, session_id: Uuid) {
325 self.saving = true;
326 self.save_progress = 0.0;
327 self.save_start_time = Some(Instant::now());
328 self.session_id = Some(session_id);
329 self.show_error = None;
330 }
331
332 pub fn update_progress(&mut self, progress: f64) {
334 self.save_progress = progress.clamp(0.0, 1.0);
335
336 if let Some(start_time) = self.save_start_time {
338 let elapsed = start_time.elapsed();
339 if progress > 0.0 && progress < 1.0 {
340 let total_estimated = elapsed.as_secs_f64() / progress;
341 let remaining = total_estimated - elapsed.as_secs_f64();
342 self.estimated_time = Some(Duration::from_secs_f64(remaining));
343 }
344 }
345 }
346
347 pub const fn complete_save(&mut self) {
349 self.saving = false;
350 self.save_progress = 1.0;
351 self.show_success = true;
352 self.estimated_time = None;
353 }
354
355 pub fn set_error(&mut self, error: String) {
357 self.show_error = Some(error);
358 self.saving = false;
359 self.save_progress = 0.0;
360 self.estimated_time = None;
361 }
362
363 pub fn reset(&mut self) {
365 *self = Self::new();
366 }
367}
368
369impl Default for SaveSessionState {
370 fn default() -> Self {
371 Self::new()
372 }
373}
374
375#[derive(Debug, Clone, PartialEq)]
377pub enum SaveSessionAction {
378 None,
379 Save,
380 Cancel,
381 Close,
382}
383
384pub struct SaveSessionDialog<'a> {
386 state: &'a SaveSessionState,
387}
388
389impl<'a> SaveSessionDialog<'a> {
390 pub const fn new(state: &'a SaveSessionState) -> Self {
391 Self { state }
392 }
393
394 fn render_input_field(
395 &self,
396 area: Rect,
397 buf: &mut Buffer,
398 title: &str,
399 text: &str,
400 placeholder: &str,
401 focused: bool,
402 cursor_pos: usize,
403 ) {
404 let block = Block::default()
405 .title(title)
406 .borders(Borders::ALL)
407 .border_type(BorderType::Rounded)
408 .border_style(if focused {
409 Style::default()
410 .fg(Color::Cyan)
411 .add_modifier(Modifier::BOLD)
412 } else {
413 Style::default().fg(Color::Gray)
414 });
415
416 let inner = block.inner(area);
417 block.render(area, buf);
418
419 let content = if text.is_empty() && !focused {
421 Span::styled(
422 placeholder,
423 Style::default()
424 .fg(Color::DarkGray)
425 .add_modifier(Modifier::ITALIC),
426 )
427 } else {
428 Span::raw(text)
429 };
430
431 let paragraph = Paragraph::new(Line::from(content));
432 paragraph.render(inner, buf);
433
434 if focused && inner.width > 0 {
436 let visible_cursor = cursor_pos.min(text.len());
437 let cursor_x = inner.x + (visible_cursor as u16).min(inner.width - 1);
438 if cursor_x < inner.right()
439 && let Some(cell) = buf.cell_mut((cursor_x, inner.y))
440 {
441 cell.set_style(Style::default().bg(Color::White).fg(Color::Black));
442 }
443 }
444 }
445
446 fn render_button(
447 &self,
448 area: Rect,
449 buf: &mut Buffer,
450 text: &str,
451 focused: bool,
452 disabled: bool,
453 ) {
454 let style = if disabled {
455 Style::default().fg(Color::DarkGray)
456 } else if focused {
457 Style::default()
458 .bg(Color::Cyan)
459 .fg(Color::Black)
460 .add_modifier(Modifier::BOLD)
461 } else {
462 Style::default().fg(Color::White)
463 };
464
465 let block = Block::default()
466 .borders(Borders::ALL)
467 .border_type(BorderType::Rounded)
468 .border_style(if focused && !disabled {
469 Style::default().fg(Color::Cyan)
470 } else {
471 Style::default().fg(Color::DarkGray)
472 });
473
474 let paragraph = Paragraph::new(text)
475 .alignment(Alignment::Center)
476 .style(style);
477
478 let inner_area = block.inner(area);
479 block.render(area, buf);
480 paragraph.render(inner_area, buf);
481 }
482
483 fn render_progress_bar(&self, area: Rect, buf: &mut Buffer) {
484 let progress_text = if let Some(duration) = self.state.estimated_time {
485 format!(
486 "Saving... {:.0}% - {}s remaining",
487 self.state.save_progress * 100.0,
488 duration.as_secs()
489 )
490 } else {
491 format!("Saving... {:.0}%", self.state.save_progress * 100.0)
492 };
493
494 let gauge = Gauge::default()
495 .block(
496 Block::default()
497 .borders(Borders::ALL)
498 .border_type(BorderType::Rounded)
499 .title(" Progress "),
500 )
501 .gauge_style(Style::default().fg(Color::Cyan).bg(Color::Black))
502 .percent((self.state.save_progress * 100.0) as u16)
503 .label(progress_text);
504
505 gauge.render(area, buf);
506 }
507
508 fn render_success_message(&self, area: Rect, buf: &mut Buffer) {
509 let block = Block::default()
510 .borders(Borders::ALL)
511 .border_type(BorderType::Double)
512 .border_style(Style::default().fg(Color::Green))
513 .title(" Success ");
514
515 let inner = block.inner(area);
516 block.render(area, buf);
517
518 let success_text = vec![
519 Line::from(""),
520 Line::from(vec![
521 Span::raw("✓ "),
522 Span::styled(
523 "Session saved successfully!",
524 Style::default()
525 .fg(Color::Green)
526 .add_modifier(Modifier::BOLD),
527 ),
528 ]),
529 Line::from(""),
530 Line::from(vec![
531 Span::raw("Name: "),
532 Span::styled(&self.state.session_name, Style::default().fg(Color::White)),
533 ]),
534 Line::from(vec![
535 Span::raw("Location: "),
536 Span::styled(
537 self.state.save_location.display().to_string(),
538 Style::default().fg(Color::DarkGray),
539 ),
540 ]),
541 Line::from(""),
542 Line::from(Span::styled(
543 "Press any key to continue",
544 Style::default()
545 .fg(Color::DarkGray)
546 .add_modifier(Modifier::ITALIC),
547 )),
548 ];
549
550 let paragraph = Paragraph::new(success_text)
551 .alignment(Alignment::Center)
552 .wrap(Wrap { trim: true });
553
554 paragraph.render(inner, buf);
555 }
556}
557
558impl<'a> WidgetRef for SaveSessionDialog<'a> {
559 fn render_ref(&self, area: Rect, buf: &mut Buffer) {
560 Clear.render(area, buf);
562
563 let dialog_width = 70;
565 let dialog_height = if self.state.show_success {
566 12
567 } else if self.state.saving {
568 10
569 } else {
570 18
571 };
572
573 let x = area.width.saturating_sub(dialog_width) / 2;
574 let y = area.height.saturating_sub(dialog_height) / 2;
575 let dialog_area = Rect::new(x, y, dialog_width, dialog_height);
576
577 if self.state.show_success {
579 self.render_success_message(dialog_area, buf);
580 return;
581 }
582
583 let block = Block::default()
585 .title(" Save Session ")
586 .borders(Borders::ALL)
587 .border_type(BorderType::Double)
588 .border_style(Style::default().fg(Color::Blue));
589
590 let inner = block.inner(dialog_area);
591 block.render(dialog_area, buf);
592
593 if self.state.saving {
594 let layout = Layout::default()
596 .direction(Direction::Vertical)
597 .constraints([
598 Constraint::Length(2), Constraint::Length(1), Constraint::Length(3), Constraint::Length(1), Constraint::Length(1), ])
604 .split(inner);
605
606 let info_text = format!("Saving session: {}", self.state.session_name);
607 let info = Paragraph::new(info_text)
608 .alignment(Alignment::Center)
609 .style(Style::default().fg(Color::White));
610 info.render(layout[0], buf);
611
612 self.render_progress_bar(layout[2], buf);
613
614 let cancel_hint = Paragraph::new("Press Esc to cancel")
615 .alignment(Alignment::Center)
616 .style(Style::default().fg(Color::DarkGray));
617 cancel_hint.render(layout[4], buf);
618 } else {
619 let layout = Layout::default()
621 .direction(Direction::Vertical)
622 .constraints([
623 Constraint::Length(2), Constraint::Length(1), Constraint::Length(3), Constraint::Length(3), Constraint::Length(1), Constraint::Length(2), Constraint::Length(1), Constraint::Length(3), ])
632 .split(inner);
633
634 let location_text = format!("📁 {}", self.state.save_location.display());
636 let location = Paragraph::new(location_text)
637 .style(Style::default().fg(Color::DarkGray))
638 .alignment(Alignment::Center);
639 location.render(layout[0], buf);
640
641 self.render_input_field(
643 layout[2],
644 buf,
645 "Session Name",
646 &self.state.session_name,
647 &format!("e.g., {}", self.state.default_name),
648 self.state.focused_field == 0,
649 if self.state.focused_field == 0 {
650 self.state.cursor_pos
651 } else {
652 0
653 },
654 );
655
656 self.render_input_field(
658 layout[3],
659 buf,
660 "Description (optional)",
661 &self.state.description,
662 "Brief description of this session...",
663 self.state.focused_field == 1,
664 if self.state.focused_field == 1 {
665 self.state.cursor_pos
666 } else {
667 0
668 },
669 );
670
671 if let Some(ref error) = self.state.show_error {
673 let error_text = format!("⚠ {}", error);
674 let error_paragraph = Paragraph::new(error_text)
675 .style(Style::default().fg(Color::Red))
676 .alignment(Alignment::Center)
677 .wrap(Wrap { trim: true });
678 error_paragraph.render(layout[5], buf);
679 }
680
681 let button_layout = Layout::default()
683 .direction(Direction::Horizontal)
684 .constraints([
685 Constraint::Percentage(20),
686 Constraint::Percentage(25),
687 Constraint::Percentage(10),
688 Constraint::Percentage(25),
689 Constraint::Percentage(20),
690 ])
691 .split(layout[7]);
692
693 self.render_button(
694 button_layout[1],
695 buf,
696 "💾 Save",
697 self.state.focused_field == 2,
698 self.state.session_name.trim().is_empty(),
699 );
700
701 self.render_button(
702 button_layout[3],
703 buf,
704 "Cancel",
705 self.state.focused_field == 3,
706 false,
707 );
708 }
709
710 let help_text = if self.state.saving {
712 "Saving session... Please wait"
713 } else if self.state.show_success {
714 "Session saved! Press any key to continue"
715 } else {
716 "Tab: Navigate • Enter: Confirm • Esc: Cancel • Ctrl+S: Quick Save"
717 };
718
719 let help_area = Rect::new(
720 dialog_area.x,
721 dialog_area.y + dialog_area.height,
722 dialog_area.width,
723 1,
724 );
725
726 if help_area.y < area.height {
727 let help = Paragraph::new(help_text)
728 .style(Style::default().fg(Color::DarkGray))
729 .alignment(Alignment::Center);
730 help.render(help_area, buf);
731 }
732 }
733}