detached_shell/
interactive.rs1use 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 let mut stdout = io::stdout();
33 terminal::enable_raw_mode()?;
34 execute!(stdout, EnterAlternateScreen, Hide)?;
35
36 let result = self.event_loop();
37
38 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 execute!(
90 stdout,
91 SetForegroundColor(Color::Cyan),
92 Print("NDS - Interactive Session Picker\n"),
93 ResetColor,
94 Print("─────────────────────────────────────────────────\n\n")
95 )?;
96
97 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 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 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 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}