diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 978537a..736a5ef 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2991,6 +2991,7 @@ checksum = "47c225751e8fbfaaaac5572a80e25d0a0921e9cf408c55509526161b5609157c" dependencies = [ "ironrdp-connector", "ironrdp-core", + "ironrdp-displaycontrol", "ironrdp-graphics", "ironrdp-input", "ironrdp-pdu", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0eca1fd..5578d2d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -65,7 +65,7 @@ ureq = "3" png = "0.17" # RDP (IronRDP) -ironrdp = { version = "0.14", features = ["connector", "session", "graphics", "input"] } +ironrdp = { version = "0.14", features = ["connector", "session", "graphics", "input", "displaycontrol"] } ironrdp-tokio = { version = "0.8", features = ["reqwest-rustls-ring"] } ironrdp-tls = { version = "0.2", features = ["rustls"] } tokio-rustls = "0.26" diff --git a/src-tauri/src/commands/rdp_commands.rs b/src-tauri/src/commands/rdp_commands.rs index 808cfbc..0a9858c 100644 --- a/src-tauri/src/commands/rdp_commands.rs +++ b/src-tauri/src/commands/rdp_commands.rs @@ -101,6 +101,19 @@ pub fn rdp_send_clipboard( state.rdp.send_clipboard(&session_id, &text) } +/// Resize the RDP session's desktop resolution. +/// Sends a Display Control Virtual Channel request to the server. +/// The server will re-render at the new resolution and send updated frames. +#[tauri::command] +pub fn rdp_resize( + session_id: String, + width: u16, + height: u16, + state: State<'_, AppState>, +) -> Result<(), String> { + state.rdp.resize(&session_id, width, height) +} + /// Disconnect an RDP session. /// /// Sends a graceful shutdown to the RDP server and removes the session. diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8b70f17..b68cb2e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -224,7 +224,7 @@ pub fn run() { commands::credentials::list_credentials, commands::credentials::create_password, commands::credentials::create_ssh_key, commands::credentials::delete_credential, commands::credentials::decrypt_password, commands::credentials::decrypt_ssh_key, commands::ssh_commands::connect_ssh, commands::ssh_commands::connect_ssh_with_key, commands::ssh_commands::ssh_write, commands::ssh_commands::ssh_resize, commands::ssh_commands::disconnect_ssh, commands::ssh_commands::disconnect_session, commands::ssh_commands::list_ssh_sessions, commands::sftp_commands::sftp_list, commands::sftp_commands::sftp_read_file, commands::sftp_commands::sftp_write_file, commands::sftp_commands::sftp_mkdir, commands::sftp_commands::sftp_delete, commands::sftp_commands::sftp_rename, - commands::rdp_commands::connect_rdp, commands::rdp_commands::rdp_get_frame, commands::rdp_commands::rdp_send_mouse, commands::rdp_commands::rdp_send_key, commands::rdp_commands::rdp_send_clipboard, commands::rdp_commands::disconnect_rdp, commands::rdp_commands::list_rdp_sessions, + commands::rdp_commands::connect_rdp, commands::rdp_commands::rdp_get_frame, commands::rdp_commands::rdp_send_mouse, commands::rdp_commands::rdp_send_key, commands::rdp_commands::rdp_send_clipboard, commands::rdp_commands::rdp_resize, commands::rdp_commands::disconnect_rdp, commands::rdp_commands::list_rdp_sessions, commands::theme_commands::list_themes, commands::theme_commands::get_theme, commands::pty_commands::list_available_shells, commands::pty_commands::spawn_local_shell, commands::pty_commands::pty_write, commands::pty_commands::pty_resize, commands::pty_commands::disconnect_pty, commands::mcp_commands::mcp_list_sessions, commands::mcp_commands::mcp_terminal_read, commands::mcp_commands::mcp_terminal_execute, commands::mcp_commands::mcp_get_session_context, commands::mcp_commands::mcp_bridge_path, diff --git a/src-tauri/src/rdp/mod.rs b/src-tauri/src/rdp/mod.rs index c972cf6..93affda 100644 --- a/src-tauri/src/rdp/mod.rs +++ b/src-tauri/src/rdp/mod.rs @@ -63,6 +63,7 @@ enum InputEvent { pressed: bool, }, Clipboard(String), + Resize { width: u16, height: u16 }, Disconnect, } @@ -299,6 +300,11 @@ impl RdpService { handle.input_tx.send(InputEvent::Key { scancode, pressed }).map_err(|_| format!("RDP session {} input channel closed", session_id)) } + pub fn resize(&self, session_id: &str, width: u16, height: u16) -> Result<(), String> { + let handle = self.sessions.get(session_id).ok_or_else(|| format!("RDP session {} not found", session_id))?; + handle.input_tx.send(InputEvent::Resize { width, height }).map_err(|_| format!("RDP session {} input channel closed", session_id)) + } + pub fn disconnect(&self, session_id: &str) -> Result<(), String> { let handle = self.sessions.get(session_id).ok_or_else(|| format!("RDP session {} not found", session_id))?; let _ = handle.input_tx.send(InputEvent::Disconnect); @@ -386,7 +392,7 @@ async fn establish_connection(config: connector::Config, hostname: &str, port: u Ok((connection_result, upgraded_framed)) } -async fn run_active_session(connection_result: ConnectionResult, framed: UpgradedFramed, front_buffer: Arc>>, dirty_region: Arc>>, frame_dirty: Arc, mut input_rx: mpsc::UnboundedReceiver, width: u16, height: u16, app_handle: tauri::AppHandle, session_id: String) -> Result<(), String> { +async fn run_active_session(connection_result: ConnectionResult, framed: UpgradedFramed, front_buffer: Arc>>, dirty_region: Arc>>, frame_dirty: Arc, mut input_rx: mpsc::UnboundedReceiver, mut width: u16, mut height: u16, app_handle: tauri::AppHandle, session_id: String) -> Result<(), String> { let (mut reader, mut writer) = split_tokio_framed(framed); let mut image = DecodedImage::new(PixelFormat::RgbA32, width, height); let mut active_stage = ActiveStage::new(connection_result); @@ -438,6 +444,24 @@ async fn run_active_session(connection_result: ConnectionResult, framed: Upgrade } all_outputs } + Some(InputEvent::Resize { width: new_w, height: new_h }) => { + // Ensure dimensions are within RDP spec (200-8192, even width) + let w = (new_w.max(200).min(8192) & !1) as u32; + let h = new_h.max(200).min(8192) as u32; + if let Some(Ok(resize_frame)) = active_stage.encode_resize(w, h, None, None) { + writer.write_all(&resize_frame).await.map_err(|e| format!("Failed to send resize: {}", e))?; + // Reallocate image and front buffer for new dimensions + image = DecodedImage::new(PixelFormat::RgbA32, w as u16, h as u16); + let buf_size = w as usize * h as usize * 4; + let mut new_buf = vec![0u8; buf_size]; + for pixel in new_buf.chunks_exact_mut(4) { pixel[3] = 255; } + *front_buffer.write().unwrap_or_else(|e| e.into_inner()) = new_buf; + width = w as u16; + height = h as u16; + info!("RDP session {} resized to {}x{}", session_id, width, height); + } + Vec::new() + } } } }; diff --git a/src/components/rdp/RdpView.vue b/src/components/rdp/RdpView.vue index 8fb2a3a..75d87e6 100644 --- a/src/components/rdp/RdpView.vue +++ b/src/components/rdp/RdpView.vue @@ -29,6 +29,7 @@