detached_shell/
manager.rs1use chrono::{DateTime, Local, Utc};
2use std::fmt;
3
4use crate::error::Result;
5use crate::history_v2::SessionHistory;
6use crate::pty::PtyProcess;
7use crate::session::Session;
8
9pub struct SessionManager;
10
11impl SessionManager {
12    pub fn create_session() -> Result<Session> {
13        Self::create_session_with_name(None)
14    }
15
16    pub fn create_session_with_name(name: Option<String>) -> Result<Session> {
17        let session_id = uuid::Uuid::new_v4().to_string()[..8].to_string();
19
20        let session = PtyProcess::spawn_new_detached_with_name(&session_id, name)?;
22
23        let _ = SessionHistory::record_session_created(&session);
25
26        Ok(session)
27    }
28
29    pub fn attach_session(session_id: &str) -> Result<()> {
30        let mut current_session_id = session_id.to_string();
31
32        loop {
33            let mut session = Session::load(¤t_session_id)?;
35
36            if session.attached {
37                eprintln!(
40                    "Warning: Session {} appears to be already attached.",
41                    current_session_id
42                );
43                eprintln!("Attempting to attach anyway (previous connection may have been lost).");
44
45                session.attached = false;
47                session.save()?;
48            }
49
50            session.mark_attached()?;
52
53            let _ = SessionHistory::record_session_attached(&session);
55
56            let switch_to = PtyProcess::attach_to_session(&session)?;
58
59            let _ = session.mark_detached();
61
62            let _ = SessionHistory::record_session_detached(&session);
64
65            if let Some(new_session_id) = switch_to {
67                current_session_id = new_session_id;
69            } else {
70                return Ok(());
72            }
73        }
74    }
75
76    pub fn list_sessions() -> Result<Vec<Session>> {
77        Session::list_all()
78    }
79
80    pub fn kill_session(session_id: &str) -> Result<()> {
81        if let Ok(session) = Session::load(session_id) {
83            let _ = SessionHistory::record_session_killed(&session);
85        }
86
87        PtyProcess::kill_session(session_id)
88    }
89
90    pub fn get_session(session_id: &str) -> Result<Session> {
91        Session::load(session_id)
92    }
93
94    pub fn rename_session(session_id: &str, new_name: &str) -> Result<()> {
95        let mut session = Session::load(session_id)?;
96        let old_name = session.name.clone();
97
98        session.name = if new_name.trim().is_empty() {
99            None
100        } else {
101            Some(new_name.to_string())
102        };
103
104        if let Some(ref name) = session.name {
106            let _ = SessionHistory::record_session_renamed(&session, old_name, name.clone());
107        }
108
109        session.save()
110    }
111
112    pub fn cleanup_dead_sessions() -> Result<()> {
113        let sessions = Session::list_all()?;
114
115        for session in sessions {
116            if !Session::is_process_alive(session.pid) {
117                let _ = SessionHistory::record_session_crashed(&session);
119                Session::cleanup(&session.id)?;
120            }
121        }
122
123        Ok(())
124    }
125}
126
127pub struct SessionDisplay<'a> {
129    pub session: &'a Session,
130}
131
132impl<'a> SessionDisplay<'a> {
133    pub fn new(session: &'a Session) -> Self {
134        SessionDisplay { session }
135    }
136
137    fn format_duration(&self) -> String {
138        let now = Utc::now();
139        let duration = now - self.session.created_at;
140
141        if duration.num_days() > 0 {
142            format!("{}d", duration.num_days())
143        } else if duration.num_hours() > 0 {
144            format!("{}h", duration.num_hours())
145        } else if duration.num_minutes() > 0 {
146            format!("{}m", duration.num_minutes())
147        } else {
148            format!("{}s", duration.num_seconds())
149        }
150    }
151
152    fn format_time(&self) -> String {
153        let local_time: DateTime<Local> = self.session.created_at.into();
154        local_time.format("%H:%M:%S").to_string()
155    }
156}
157
158impl<'a> fmt::Display for SessionDisplay<'a> {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        let client_count = self.session.get_client_count();
162        let status = if client_count > 0 {
163            format!("attached({})", client_count)
164        } else {
165            "detached".to_string()
166        };
167
168        write!(
169            f,
170            "{:<20} {:<10} {:<12} {:<10} {:<20} {}",
171            self.session.display_name(),
172            self.session.pid,
173            status,
174            self.format_duration(),
175            self.format_time(),
176            self.session.working_dir
177        )
178    }
179}
180
181pub struct SessionTable {
182    sessions: Vec<Session>,
183}
184
185impl SessionTable {
186    pub fn new(sessions: Vec<Session>) -> Self {
187        SessionTable { sessions }
188    }
189
190    pub fn print(&self) {
191        if self.sessions.is_empty() {
192            println!("No active sessions");
193            return;
194        }
195
196        println!(
198            "{:<20} {:<10} {:<12} {:<10} {:<20} {}",
199            "SESSION", "PID", "STATUS", "UPTIME", "CREATED", "WORKING DIR"
200        );
201        println!("{}", "-".repeat(84));
202
203        for session in &self.sessions {
205            println!("{}", SessionDisplay::new(session));
206        }
207
208        println!("\nTotal: {} session(s)", self.sessions.len());
209    }
210}