Skip to main content

term_session_server/
session.rs

1use portable_pty::{CommandBuilder, PtySize};
2use term_wm_pty_engine::{Pty, PtyResult};
3
4pub struct Session {
5    pub id: u64,
6    pub pty: Pty,
7    pub title: Option<String>,
8    pub exited: bool,
9    pub exit_code: Option<i32>,
10    pub cols: u16,
11    pub rows: u16,
12}
13
14fn default_shell_command() -> CommandBuilder {
15    #[cfg(not(windows))]
16    let shell = std::env::var("SHELL").unwrap_or_else(|_| "bash".to_string());
17    #[cfg(windows)]
18    let shell = std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string());
19    let mut cmd = CommandBuilder::new(shell);
20    if let Ok(cwd) = std::env::current_dir() {
21        cmd.cwd(cwd);
22    }
23    cmd
24}
25
26impl Session {
27    pub fn spawn(id: u64, cmd: Option<Vec<String>>, cols: u16, rows: u16) -> PtyResult<Self> {
28        let size = PtySize {
29            rows,
30            cols,
31            pixel_width: 0,
32            pixel_height: 0,
33        };
34        let pty = if let Some(cmd_parts) = &cmd {
35            let mut builder = CommandBuilder::new(&cmd_parts[0]);
36            for arg in &cmd_parts[1..] {
37                builder.arg(arg);
38            }
39            if let Ok(cwd) = std::env::current_dir() {
40                builder.cwd(cwd);
41            }
42            Pty::spawn(builder, size)?
43        } else {
44            Pty::spawn(default_shell_command(), size)?
45        };
46        Ok(Self {
47            id,
48            pty,
49            title: None,
50            exited: false,
51            exit_code: None,
52            cols,
53            rows,
54        })
55    }
56
57    pub fn read_output(&mut self) -> Vec<u8> {
58        // Clear dirty flag and wake the PTY reader thread from I/O burst budget parking
59        self.pty.screen();
60        // Sync title from the background engine (replaces manual OSC extraction)
61        if let Some(title) = self.pty.take_pending_title() {
62            self.title = Some(title);
63        }
64        self.pty.drain_pending()
65    }
66
67    /// Sync screen state without draining pending output.
68    /// Clears the dirty flag (waking the reader thread from I/O burst budget parking)
69    /// and syncs the title, but leaves accumulated bytes in the pending buffer so
70    /// they can be sent to a future subscriber.
71    pub fn sync_screen(&mut self) {
72        self.pty.screen();
73        if let Some(title) = self.pty.take_pending_title() {
74            self.title = Some(title);
75        }
76    }
77
78    pub fn check_exited(&mut self) -> bool {
79        if !self.exited && self.pty.has_exited() {
80            self.exited = true;
81            self.exit_code = self.pty.exit_status().map(|s| s.exit_code() as i32);
82            true
83        } else {
84            false
85        }
86    }
87
88    pub fn take_exit_code(&mut self) -> Option<i32> {
89        self.exit_code.take()
90    }
91
92    pub fn generate_snapshot(&mut self) -> Vec<u8> {
93        self.pty.generate_snapshot()
94    }
95}