detached_shell/
interactive.rs

1use crate::{NdsError, Result, Session, SessionManager};
2use crossterm::{
3    cursor::{Hide, MoveTo, Show},
4    event::{self, Event, KeyCode, KeyEvent},
5    execute,
6    style::{Color, Print, ResetColor, SetForegroundColor, Stylize},
7    terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
8};
9use std::io::{self, Write};
10use std::time::Duration;
11
12pub struct InteractivePicker {
13    sessions: Vec<Session>,
14    selected: usize,
15}
16
17impl InteractivePicker {
18    pub fn new() -> Result<Self> {
19        let sessions = SessionManager::list_sessions()?;
20        if sessions.is_empty() {
21            return Err(NdsError::SessionNotFound("No active sessions".to_string()));
22        }
23
24        Ok(Self {
25            sessions,
26            selected: 0,
27        })
28    }
29
30    pub fn run(&mut self) -> Result<Option<String>> {
31        // Enter alternate screen
32        let mut stdout = io::stdout();
33        terminal::enable_raw_mode()?;
34        execute!(stdout, EnterAlternateScreen, Hide)?;
35
36        let result = self.event_loop();
37
38        // Clean up
39        execute!(stdout, LeaveAlternateScreen, Show)?;
40        terminal::disable_raw_mode()?;
41
42        result
43    }
44
45    fn event_loop(&mut self) -> Result<Option<String>> {
46        let mut stdout = io::stdout();
47
48        loop {
49            self.draw(&mut stdout)?;
50
51            if event::poll(Duration::from_millis(100))? {
52                if let Event::Key(key) = event::read()? {
53                    match self.handle_key(key) {
54                        Some(session_id) => return Ok(Some(session_id)),
55                        None if key.code == KeyCode::Char('q') || key.code == KeyCode::Esc => {
56                            return Ok(None);
57                        }
58                        _ => {}
59                    }
60                }
61            }
62        }
63    }
64
65    fn handle_key(&mut self, key: KeyEvent) -> Option<String> {
66        match key.code {
67            KeyCode::Up | KeyCode::Char('k') => {
68                if self.selected > 0 {
69                    self.selected -= 1;
70                }
71            }
72            KeyCode::Down | KeyCode::Char('j') => {
73                if self.selected < self.sessions.len() - 1 {
74                    self.selected += 1;
75                }
76            }
77            KeyCode::Enter => {
78                return Some(self.sessions[self.selected].id.clone());
79            }
80            _ => {}
81        }
82        None
83    }
84
85    fn draw(&self, stdout: &mut io::Stdout) -> Result<()> {
86        execute!(stdout, Clear(ClearType::All), MoveTo(0, 0))?;
87
88        // Header
89        execute!(
90            stdout,
91            SetForegroundColor(Color::Cyan),
92            Print("NDS - Interactive Session Picker\n"),
93            ResetColor,
94            Print("─────────────────────────────────────────────────\n\n")
95        )?;
96
97        // Instructions
98        execute!(
99            stdout,
100            SetForegroundColor(Color::DarkGrey),
101            Print("↑/k: up  ↓/j: down  Enter: attach  q/Esc: quit\n\n"),
102            ResetColor
103        )?;
104
105        // Sessions list
106        for (i, session) in self.sessions.iter().enumerate() {
107            if i == self.selected {
108                execute!(stdout, SetForegroundColor(Color::Green), Print("▶ "))?;
109            } else {
110                execute!(stdout, Print("  "))?;
111            }
112
113            // Session info
114            let now = chrono::Utc::now().timestamp();
115            let created = session.created_at.timestamp();
116            let duration = now - created;
117            let uptime = format_duration(duration as u64);
118
119            let line = format!(
120                "{:<20} PID: {:<8} Uptime: {:<12} {}",
121                session.display_name(),
122                session.pid,
123                uptime,
124                if session.attached {
125                    "[ATTACHED]".green().to_string()
126                } else {
127                    "".to_string()
128                }
129            );
130
131            if i == self.selected {
132                execute!(stdout, Print(line.bold()), ResetColor)?;
133            } else {
134                execute!(stdout, Print(line))?;
135            }
136
137            execute!(stdout, Print("\n"))?;
138        }
139
140        // Footer
141        execute!(
142            stdout,
143            Print("\n─────────────────────────────────────────────────\n"),
144            SetForegroundColor(Color::DarkGrey),
145            Print(format!("{} active session(s)\n", self.sessions.len())),
146            ResetColor
147        )?;
148
149        stdout.flush()?;
150        Ok(())
151    }
152}
153
154fn format_duration(seconds: u64) -> String {
155    if seconds < 60 {
156        format!("{}s", seconds)
157    } else if seconds < 3600 {
158        format!("{}m", seconds / 60)
159    } else if seconds < 86400 {
160        format!("{}h {}m", seconds / 3600, (seconds % 3600) / 60)
161    } else {
162        format!("{}d {}h", seconds / 86400, (seconds % 86400) / 3600)
163    }
164}