diff --git a/rabby-runtime/src/terminal_session/mod.rs b/rabby-runtime/src/terminal_session/mod.rs index f44a326..b6f1607 100644 --- a/rabby-runtime/src/terminal_session/mod.rs +++ b/rabby-runtime/src/terminal_session/mod.rs @@ -64,6 +64,31 @@ impl TerminalSessionRegistry { cols: u16, rows: u16, ) -> Result { + self.spawn_program(shell.as_ref(), &[], cols, rows) + } + + pub fn spawn_ssh( + &mut self, + host: impl AsRef, + user: Option<&str>, + port: u16, + cols: u16, + rows: u16, + ) -> Result { + let args = build_ssh_pty_args(host.as_ref(), user, port)?; + self.spawn_program("ssh", &args, cols, rows) + } + + fn spawn_program( + &mut self, + program: &str, + args: &[String], + cols: u16, + rows: u16, + ) -> Result { + if program.trim().is_empty() { + return Err(TerminalSessionError::Io("program is required".to_string())); + } let size = session_size(cols, rows)?; let pty_system = native_pty_system(); let pair = pty_system @@ -74,7 +99,10 @@ impl TerminalSessionRegistry { pixel_height: 0, }) .map_err(|err| TerminalSessionError::Io(err.to_string()))?; - let command = CommandBuilder::new(shell.as_ref()); + let mut command = CommandBuilder::new(program); + for arg in args { + command.arg(arg); + } let child = pair .slave .spawn_command(command) @@ -193,6 +221,31 @@ impl TerminalSessionRegistry { } } +pub fn build_ssh_pty_args( + host: &str, + user: Option<&str>, + port: u16, +) -> Result, TerminalSessionError> { + if host.trim().is_empty() { + return Err(TerminalSessionError::Io("ssh host is required".to_string())); + } + if port == 0 { + return Err(TerminalSessionError::Io("ssh port is required".to_string())); + } + let destination = match user.map(str::trim).filter(|value| !value.is_empty()) { + Some(user) => format!("{user}@{}", host.trim()), + None => host.trim().to_string(), + }; + Ok(vec![ + "-tt".to_string(), + "-o".to_string(), + "BatchMode=yes".to_string(), + "-p".to_string(), + port.to_string(), + destination, + ]) +} + fn session_size(cols: u16, rows: u16) -> Result { if cols == 0 || rows == 0 { return Err(TerminalSessionError::InvalidSize); @@ -240,4 +293,29 @@ mod tests { assert_eq!(err, TerminalSessionError::MissingSession(999)); } + + #[test] + fn ssh_pty_args_enable_tty_and_batch_mode() { + let args = build_ssh_pty_args("example.com", Some("alice"), 2222).unwrap(); + + assert_eq!( + args, + vec![ + "-tt", + "-o", + "BatchMode=yes", + "-p", + "2222", + "alice@example.com" + ] + ); + } + + #[test] + fn ssh_pty_args_reject_blank_host() { + assert_eq!( + build_ssh_pty_args(" ", None, 22).unwrap_err(), + TerminalSessionError::Io("ssh host is required".to_string()) + ); + } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index e52612e..c8f18bc 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -118,6 +118,31 @@ fn terminal_session_start( .map_err(|err| err.to_string()) } +#[tauri::command] +fn terminal_session_start_ssh( + state: tauri::State, + host: String, + user: Option, + port: Option, + cols: Option, + rows: Option, +) -> Result { + let mut registry = state + .0 + .lock() + .map_err(|_| "terminal registry lock poisoned".to_string())?; + registry + .spawn_ssh( + host, + user.as_deref(), + port.unwrap_or(22), + cols.unwrap_or(80), + rows.unwrap_or(24), + ) + .map(|id| id.0) + .map_err(|err| err.to_string()) +} + #[tauri::command] fn terminal_session_write( state: tauri::State, @@ -229,6 +254,7 @@ fn main() { load_app_config, save_app_config, terminal_session_start, + terminal_session_start_ssh, terminal_session_write, terminal_session_read_until, terminal_session_resize,