ricecoder_tui/
sessions.rs

1//! Session management and display widgets
2//!
3//! This module provides widgets for displaying and managing multiple sessions,
4//! including session tabs, list views, status indicators, and session switching.
5
6use std::collections::HashMap;
7
8/// Session status indicator
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum SessionStatus {
11    /// Session is active and running
12    Active,
13    /// Session is idle
14    Idle,
15    /// Session has unsaved changes
16    Dirty,
17    /// Session is loading
18    Loading,
19    /// Session has an error
20    Error,
21}
22
23impl SessionStatus {
24    /// Get the display symbol for the status
25    pub fn symbol(&self) -> &'static str {
26        match self {
27            SessionStatus::Active => "●",
28            SessionStatus::Idle => "○",
29            SessionStatus::Dirty => "◆",
30            SessionStatus::Loading => "◐",
31            SessionStatus::Error => "✕",
32        }
33    }
34
35    /// Get the display name for the status
36    pub fn display_name(&self) -> &'static str {
37        match self {
38            SessionStatus::Active => "Active",
39            SessionStatus::Idle => "Idle",
40            SessionStatus::Dirty => "Dirty",
41            SessionStatus::Loading => "Loading",
42            SessionStatus::Error => "Error",
43        }
44    }
45}
46
47/// Session information
48#[derive(Debug, Clone)]
49pub struct Session {
50    /// Unique session identifier
51    pub id: String,
52    /// Session name/title
53    pub name: String,
54    /// Session status
55    pub status: SessionStatus,
56    /// Last activity timestamp (seconds since epoch)
57    pub last_activity: u64,
58    /// Number of messages in session
59    pub message_count: usize,
60    /// Whether session has unsaved changes
61    pub has_changes: bool,
62    /// Session metadata
63    pub metadata: HashMap<String, String>,
64}
65
66impl Session {
67    /// Create a new session
68    pub fn new(id: String, name: String) -> Self {
69        Self {
70            id,
71            name,
72            status: SessionStatus::Idle,
73            last_activity: 0,
74            message_count: 0,
75            has_changes: false,
76            metadata: HashMap::new(),
77        }
78    }
79
80    /// Update session status
81    pub fn set_status(&mut self, status: SessionStatus) {
82        self.status = status;
83    }
84
85    /// Mark session as having changes
86    pub fn mark_dirty(&mut self) {
87        self.has_changes = true;
88        self.status = SessionStatus::Dirty;
89    }
90
91    /// Mark session as clean
92    pub fn mark_clean(&mut self) {
93        self.has_changes = false;
94        if self.status == SessionStatus::Dirty {
95            self.status = SessionStatus::Idle;
96        }
97    }
98
99    /// Update last activity timestamp
100    pub fn update_activity(&mut self) {
101        self.last_activity = std::time::SystemTime::now()
102            .duration_since(std::time::UNIX_EPOCH)
103            .map(|d| d.as_secs())
104            .unwrap_or(0);
105    }
106
107    /// Increment message count
108    pub fn add_message(&mut self) {
109        self.message_count += 1;
110        self.update_activity();
111    }
112}
113
114/// Session display mode
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum SessionDisplayMode {
117    /// Display sessions as tabs
118    Tabs,
119    /// Display sessions as a list
120    List,
121}
122
123/// Session widget for displaying and managing sessions
124#[derive(Debug, Clone)]
125pub struct SessionWidget {
126    /// All sessions
127    pub sessions: Vec<Session>,
128    /// Currently selected session index
129    pub selected_index: usize,
130    /// Display mode (tabs or list)
131    pub display_mode: SessionDisplayMode,
132    /// Whether the session panel is visible
133    pub visible: bool,
134    /// Scroll offset for list view
135    pub scroll_offset: usize,
136    /// Maximum visible sessions in list view
137    pub max_visible: usize,
138}
139
140impl SessionWidget {
141    /// Create a new session widget
142    pub fn new() -> Self {
143        Self {
144            sessions: Vec::new(),
145            selected_index: 0,
146            display_mode: SessionDisplayMode::Tabs,
147            visible: true,
148            scroll_offset: 0,
149            max_visible: 10,
150        }
151    }
152
153    /// Add a new session
154    pub fn add_session(&mut self, session: Session) {
155        self.sessions.push(session);
156        // New sessions become the current session
157        self.selected_index = self.sessions.len() - 1;
158    }
159
160    /// Remove a session by index
161    pub fn remove_session(&mut self, index: usize) -> Option<Session> {
162        if index < self.sessions.len() {
163            let removed = self.sessions.remove(index);
164
165            // Adjust selected index if needed
166            if self.selected_index >= self.sessions.len() && !self.sessions.is_empty() {
167                self.selected_index = self.sessions.len() - 1;
168            }
169
170            Some(removed)
171        } else {
172            None
173        }
174    }
175
176    /// Get the currently selected session
177    pub fn current_session(&self) -> Option<&Session> {
178        self.sessions.get(self.selected_index)
179    }
180
181    /// Get mutable reference to the currently selected session
182    pub fn current_session_mut(&mut self) -> Option<&mut Session> {
183        self.sessions.get_mut(self.selected_index)
184    }
185
186    /// Switch to a session by index
187    pub fn switch_to_session(&mut self, index: usize) -> bool {
188        if index < self.sessions.len() {
189            self.selected_index = index;
190            if let Some(session) = self.current_session_mut() {
191                session.set_status(SessionStatus::Active);
192            }
193            true
194        } else {
195            false
196        }
197    }
198
199    /// Switch to a session by ID
200    pub fn switch_to_session_by_id(&mut self, id: &str) -> bool {
201        if let Some(index) = self.sessions.iter().position(|s| s.id == id) {
202            self.switch_to_session(index)
203        } else {
204            false
205        }
206    }
207
208    /// Get the next session
209    pub fn next_session(&mut self) -> bool {
210        if self.sessions.is_empty() {
211            return false;
212        }
213        let next_index = (self.selected_index + 1) % self.sessions.len();
214        self.switch_to_session(next_index);
215        true
216    }
217
218    /// Get the previous session
219    pub fn previous_session(&mut self) -> bool {
220        if self.sessions.is_empty() {
221            return false;
222        }
223        let prev_index = if self.selected_index == 0 {
224            self.sessions.len() - 1
225        } else {
226            self.selected_index - 1
227        };
228        self.switch_to_session(prev_index);
229        true
230    }
231
232    /// Rename a session
233    pub fn rename_session(&mut self, index: usize, new_name: String) -> bool {
234        if let Some(session) = self.sessions.get_mut(index) {
235            session.name = new_name;
236            true
237        } else {
238            false
239        }
240    }
241
242    /// Rename the current session
243    pub fn rename_current_session(&mut self, new_name: String) -> bool {
244        self.rename_session(self.selected_index, new_name)
245    }
246
247    /// Toggle display mode between tabs and list
248    pub fn toggle_display_mode(&mut self) {
249        self.display_mode = match self.display_mode {
250            SessionDisplayMode::Tabs => SessionDisplayMode::List,
251            SessionDisplayMode::List => SessionDisplayMode::Tabs,
252        };
253    }
254
255    /// Set display mode
256    pub fn set_display_mode(&mut self, mode: SessionDisplayMode) {
257        self.display_mode = mode;
258    }
259
260    /// Toggle visibility
261    pub fn toggle_visibility(&mut self) {
262        self.visible = !self.visible;
263    }
264
265    /// Show the session panel
266    pub fn show(&mut self) {
267        self.visible = true;
268    }
269
270    /// Hide the session panel
271    pub fn hide(&mut self) {
272        self.visible = false;
273    }
274
275    /// Scroll up in list view
276    pub fn scroll_up(&mut self) {
277        if self.scroll_offset > 0 {
278            self.scroll_offset -= 1;
279        }
280    }
281
282    /// Scroll down in list view
283    pub fn scroll_down(&mut self) {
284        let max_scroll = self.sessions.len().saturating_sub(self.max_visible);
285        if self.scroll_offset < max_scroll {
286            self.scroll_offset += 1;
287        }
288    }
289
290    /// Get visible sessions for list view
291    pub fn visible_sessions(&self) -> Vec<&Session> {
292        self.sessions
293            .iter()
294            .skip(self.scroll_offset)
295            .take(self.max_visible)
296            .collect()
297    }
298
299    /// Get the number of sessions
300    pub fn session_count(&self) -> usize {
301        self.sessions.len()
302    }
303
304    /// Check if there are any sessions
305    pub fn has_sessions(&self) -> bool {
306        !self.sessions.is_empty()
307    }
308
309    /// Clear all sessions
310    pub fn clear(&mut self) {
311        self.sessions.clear();
312        self.selected_index = 0;
313        self.scroll_offset = 0;
314    }
315
316    /// Get session by ID
317    pub fn get_session(&self, id: &str) -> Option<&Session> {
318        self.sessions.iter().find(|s| s.id == id)
319    }
320
321    /// Get mutable session by ID
322    pub fn get_session_mut(&mut self, id: &str) -> Option<&mut Session> {
323        self.sessions.iter_mut().find(|s| s.id == id)
324    }
325
326    /// Get all session IDs
327    pub fn session_ids(&self) -> Vec<&str> {
328        self.sessions.iter().map(|s| s.id.as_str()).collect()
329    }
330
331    /// Get all session names
332    pub fn session_names(&self) -> Vec<&str> {
333        self.sessions.iter().map(|s| s.name.as_str()).collect()
334    }
335
336    /// Find session index by ID
337    pub fn find_session_index(&self, id: &str) -> Option<usize> {
338        self.sessions.iter().position(|s| s.id == id)
339    }
340
341    /// Get the selected session index
342    pub fn selected_index(&self) -> usize {
343        self.selected_index
344    }
345
346    /// Get the display mode
347    pub fn display_mode(&self) -> SessionDisplayMode {
348        self.display_mode
349    }
350
351    /// Check if visible
352    pub fn is_visible(&self) -> bool {
353        self.visible
354    }
355}
356
357impl Default for SessionWidget {
358    fn default() -> Self {
359        Self::new()
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn test_session_creation() {
369        let session = Session::new("session-1".to_string(), "Session 1".to_string());
370        assert_eq!(session.id, "session-1");
371        assert_eq!(session.name, "Session 1");
372        assert_eq!(session.status, SessionStatus::Idle);
373        assert_eq!(session.message_count, 0);
374        assert!(!session.has_changes);
375    }
376
377    #[test]
378    fn test_session_status_changes() {
379        let mut session = Session::new("session-1".to_string(), "Session 1".to_string());
380
381        session.set_status(SessionStatus::Active);
382        assert_eq!(session.status, SessionStatus::Active);
383
384        session.mark_dirty();
385        assert_eq!(session.status, SessionStatus::Dirty);
386        assert!(session.has_changes);
387
388        session.mark_clean();
389        assert!(!session.has_changes);
390    }
391
392    #[test]
393    fn test_session_widget_add_session() {
394        let mut widget = SessionWidget::new();
395        assert_eq!(widget.session_count(), 0);
396
397        let session = Session::new("session-1".to_string(), "Session 1".to_string());
398        widget.add_session(session);
399
400        assert_eq!(widget.session_count(), 1);
401        assert_eq!(widget.selected_index(), 0);
402    }
403
404    #[test]
405    fn test_session_widget_switch_session() {
406        let mut widget = SessionWidget::new();
407
408        widget.add_session(Session::new(
409            "session-1".to_string(),
410            "Session 1".to_string(),
411        ));
412        widget.add_session(Session::new(
413            "session-2".to_string(),
414            "Session 2".to_string(),
415        ));
416
417        // After adding session-2, it becomes the current session
418        assert_eq!(widget.selected_index(), 1);
419
420        widget.switch_to_session(0);
421        assert_eq!(widget.selected_index(), 0);
422
423        let current = widget.current_session().unwrap();
424        assert_eq!(current.id, "session-1");
425    }
426
427    #[test]
428    fn test_session_widget_next_previous() {
429        let mut widget = SessionWidget::new();
430
431        widget.add_session(Session::new(
432            "session-1".to_string(),
433            "Session 1".to_string(),
434        ));
435        widget.add_session(Session::new(
436            "session-2".to_string(),
437            "Session 2".to_string(),
438        ));
439        widget.add_session(Session::new(
440            "session-3".to_string(),
441            "Session 3".to_string(),
442        ));
443
444        // After adding session-3, it becomes the current session
445        assert_eq!(widget.selected_index(), 2);
446
447        widget.next_session();
448        assert_eq!(widget.selected_index(), 0); // Wraps around
449
450        widget.next_session();
451        assert_eq!(widget.selected_index(), 1);
452
453        widget.next_session();
454        assert_eq!(widget.selected_index(), 2);
455
456        widget.previous_session();
457        assert_eq!(widget.selected_index(), 1);
458    }
459
460    #[test]
461    fn test_session_widget_remove_session() {
462        let mut widget = SessionWidget::new();
463
464        widget.add_session(Session::new(
465            "session-1".to_string(),
466            "Session 1".to_string(),
467        ));
468        widget.add_session(Session::new(
469            "session-2".to_string(),
470            "Session 2".to_string(),
471        ));
472
473        assert_eq!(widget.session_count(), 2);
474
475        let removed = widget.remove_session(0);
476        assert!(removed.is_some());
477        assert_eq!(widget.session_count(), 1);
478        assert_eq!(widget.selected_index(), 0);
479    }
480
481    #[test]
482    fn test_session_widget_rename() {
483        let mut widget = SessionWidget::new();
484
485        widget.add_session(Session::new(
486            "session-1".to_string(),
487            "Session 1".to_string(),
488        ));
489
490        widget.rename_current_session("New Name".to_string());
491
492        let current = widget.current_session().unwrap();
493        assert_eq!(current.name, "New Name");
494    }
495
496    #[test]
497    fn test_session_widget_display_mode() {
498        let mut widget = SessionWidget::new();
499
500        assert_eq!(widget.display_mode(), SessionDisplayMode::Tabs);
501
502        widget.toggle_display_mode();
503        assert_eq!(widget.display_mode(), SessionDisplayMode::List);
504
505        widget.set_display_mode(SessionDisplayMode::Tabs);
506        assert_eq!(widget.display_mode(), SessionDisplayMode::Tabs);
507    }
508
509    #[test]
510    fn test_session_widget_visibility() {
511        let mut widget = SessionWidget::new();
512
513        assert!(widget.is_visible());
514
515        widget.hide();
516        assert!(!widget.is_visible());
517
518        widget.show();
519        assert!(widget.is_visible());
520
521        widget.toggle_visibility();
522        assert!(!widget.is_visible());
523    }
524
525    #[test]
526    fn test_session_status_symbols() {
527        assert_eq!(SessionStatus::Active.symbol(), "●");
528        assert_eq!(SessionStatus::Idle.symbol(), "○");
529        assert_eq!(SessionStatus::Dirty.symbol(), "◆");
530        assert_eq!(SessionStatus::Loading.symbol(), "◐");
531        assert_eq!(SessionStatus::Error.symbol(), "✕");
532    }
533
534    #[test]
535    fn test_session_widget_scroll() {
536        let mut widget = SessionWidget::new();
537        widget.max_visible = 3;
538
539        for i in 0..10 {
540            widget.add_session(Session::new(
541                format!("session-{}", i),
542                format!("Session {}", i),
543            ));
544        }
545
546        assert_eq!(widget.scroll_offset, 0);
547
548        widget.scroll_down();
549        assert_eq!(widget.scroll_offset, 1);
550
551        widget.scroll_up();
552        assert_eq!(widget.scroll_offset, 0);
553    }
554}