Skip to main content

term_session_server/
session.rs

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