From f9c8b3ae22c3dd222558c9030c6499cbbdce4ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Almeida?= Date: Thu, 21 Nov 2024 18:16:40 +0000 Subject: [PATCH 1/2] ratatui examples fixed. --- russh/Cargo.toml | 2 +- russh/examples/ratatui_app.rs | 124 +++++++++++++++++---------- russh/examples/ratatui_shared_app.rs | 104 +++++++++++++++------- 3 files changed, 153 insertions(+), 77 deletions(-) diff --git a/russh/Cargo.toml b/russh/Cargo.toml index 8b58ad83..429fad0a 100644 --- a/russh/Cargo.toml +++ b/russh/Cargo.toml @@ -74,7 +74,7 @@ rand = "0.8.5" shell-escape = "0.1" tokio-fd = "0.3" termion = "2" -ratatui = "0.26.0" +ratatui = "0.29.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] russh-sftp = "2.0.5" diff --git a/russh/examples/ratatui_app.rs b/russh/examples/ratatui_app.rs index 13454b26..94909fc6 100644 --- a/russh/examples/ratatui_app.rs +++ b/russh/examples/ratatui_app.rs @@ -7,10 +7,11 @@ use ratatui::backend::CrosstermBackend; use ratatui::layout::Rect; use ratatui::style::{Color, Style}; use ratatui::widgets::{Block, Borders, Clear, Paragraph}; -use ratatui::Terminal; +use ratatui::{Terminal, TerminalOptions, Viewport}; +use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use russh::keys::ssh_key::PublicKey; use russh::server::*; -use russh::{Channel, ChannelId}; +use russh::{Channel, ChannelId, Pty}; use tokio::sync::Mutex; type SshTerminal = Terminal>; @@ -25,12 +26,25 @@ impl App { } } -#[derive(Clone)] struct TerminalHandle { - handle: Handle, - // The sink collects the data which is finally flushed to the handle. + sender: UnboundedSender>, + // The sink collects the data which is finally sent to sender. sink: Vec, - channel_id: ChannelId, +} + +impl TerminalHandle { + async fn start(handle: Handle, channel_id: ChannelId) -> Self { + let (sender, mut receiver) = unbounded_channel::>(); + tokio::spawn(async move { + while let Some(data) = receiver.recv().await { + let result = handle.data(channel_id, data.into()).await; + if result.is_err() { + eprintln!("Failed to send data: {:?}", result); + } + } + }); + Self { sender, sink: Vec::new() } + } } // The crossterm backend writes to the terminal handle. @@ -41,15 +55,10 @@ impl std::io::Write for TerminalHandle { } fn flush(&mut self) -> std::io::Result<()> { - let handle = self.handle.clone(); - let channel_id = self.channel_id; - let data = self.sink.clone().into(); - futures::executor::block_on(async move { - let result = handle.data(channel_id, data).await; - if result.is_err() { - eprintln!("Failed to send data: {:?}", result); - } - }); + let result = self.sender.send(self.sink.clone()); + if result.is_err() { + return Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, result.unwrap_err())); + } self.sink.clear(); Ok(()) @@ -81,8 +90,8 @@ impl AppServer { terminal .draw(|f| { - let size = f.size(); - f.render_widget(Clear, size); + let area = f.area(); + f.render_widget(Clear, area); let style = match app.counter % 3 { 0 => Style::default().fg(Color::Red), 1 => Style::default().fg(Color::Green), @@ -94,7 +103,7 @@ impl AppServer { let block = Block::default() .title("Press 'c' to reset the counter!") .borders(Borders::ALL); - f.render_widget(paragraph.block(block), size); + f.render_widget(paragraph.block(block), area); }) .unwrap(); } @@ -135,20 +144,18 @@ impl Handler for AppServer { channel: Channel, session: &mut Session, ) -> Result { - { - let mut clients = self.clients.lock().await; - let terminal_handle = TerminalHandle { - handle: session.handle(), - sink: Vec::new(), - channel_id: channel.id(), - }; - - let backend = CrosstermBackend::new(terminal_handle.clone()); - let terminal = Terminal::new(backend)?; - let app = App::new(); - - clients.insert(self.id, (terminal, app)); - } + let terminal_handle = TerminalHandle::start(session.handle(), channel.id()).await; + + let backend = CrosstermBackend::new(terminal_handle); + + // the correct viewport area will be set when the client request a pty + let options = TerminalOptions { viewport: Viewport::Fixed(Rect::default()) }; + + let terminal = Terminal::with_options(backend, options)?; + let app = App::new(); + + let mut clients = self.clients.lock().await; + clients.insert(self.id, (terminal, app)); Ok(true) } @@ -192,17 +199,48 @@ impl Handler for AppServer { _: u32, _: &mut Session, ) -> Result<(), Self::Error> { - { - let mut clients = self.clients.lock().await; - let (terminal, _) = clients.get_mut(&self.id).unwrap(); - let rect = Rect { - x: 0, - y: 0, - width: col_width as u16, - height: row_height as u16, - }; - terminal.resize(rect)?; - } + let rect = Rect { + x: 0, + y: 0, + width: col_width as u16, + height: row_height as u16, + }; + + let mut clients = self.clients.lock().await; + let (terminal, _) = clients.get_mut(&self.id).unwrap(); + terminal.resize(rect)?; + + Ok(()) + } + + /// The client requests a pseudo-terminal with the given + /// specifications. + /// + /// **Note:** Success or failure should be communicated to the client by calling + /// `session.channel_success(channel)` or `session.channel_failure(channel)` respectively. + async fn pty_request( + &mut self, + channel: ChannelId, + _: &str, + col_width: u32, + row_height: u32, + _: u32, + _: u32, + _: &[(Pty, u32)], + session: &mut Session, + ) -> Result<(), Self::Error> { + let rect = Rect { + x: 0, + y: 0, + width: col_width as u16, + height: row_height as u16, + }; + + let mut clients = self.clients.lock().await; + let (terminal, _) = clients.get_mut(&self.id).unwrap(); + terminal.resize(rect)?; + + session.channel_success(channel)?; Ok(()) } diff --git a/russh/examples/ratatui_shared_app.rs b/russh/examples/ratatui_shared_app.rs index e09e745f..396af7cd 100644 --- a/russh/examples/ratatui_shared_app.rs +++ b/russh/examples/ratatui_shared_app.rs @@ -7,10 +7,11 @@ use ratatui::backend::CrosstermBackend; use ratatui::layout::Rect; use ratatui::style::{Color, Style}; use ratatui::widgets::{Block, Borders, Clear, Paragraph}; -use ratatui::Terminal; +use ratatui::{Terminal, TerminalOptions, Viewport}; +use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use russh::keys::ssh_key::PublicKey; use russh::server::*; -use russh::{Channel, ChannelId}; +use russh::{Channel, ChannelId, Pty}; use tokio::sync::Mutex; type SshTerminal = Terminal>; @@ -25,12 +26,25 @@ impl App { } } -#[derive(Clone)] struct TerminalHandle { - handle: Handle, - // The sink collects the data which is finally flushed to the handle. + sender: UnboundedSender>, + // The sink collects the data which is finally sent to sender. sink: Vec, - channel_id: ChannelId, +} + +impl TerminalHandle { + async fn start(handle: Handle, channel_id: ChannelId) -> Self { + let (sender, mut receiver) = unbounded_channel::>(); + tokio::spawn(async move { + while let Some(data) = receiver.recv().await { + let result = handle.data(channel_id, data.into()).await; + if result.is_err() { + eprintln!("Failed to send data: {:?}", result); + } + } + }); + Self { sender, sink: Vec::new() } + } } // The crossterm backend writes to the terminal handle. @@ -41,15 +55,10 @@ impl std::io::Write for TerminalHandle { } fn flush(&mut self) -> std::io::Result<()> { - let handle = self.handle.clone(); - let channel_id = self.channel_id; - let data = self.sink.clone().into(); - futures::executor::block_on(async move { - let result = handle.data(channel_id, data).await; - if result.is_err() { - eprintln!("Failed to send data: {:?}", result); - } - }); + let result = self.sender.send(self.sink.clone()); + if result.is_err() { + return Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, result.unwrap_err())); + } self.sink.clear(); Ok(()) @@ -83,8 +92,8 @@ impl AppServer { for (_, terminal) in clients.lock().await.iter_mut() { terminal .draw(|f| { - let size = f.size(); - f.render_widget(Clear, size); + let area = f.area(); + f.render_widget(Clear, area); let style = match counter % 3 { 0 => Style::default().fg(Color::Red), 1 => Style::default().fg(Color::Green), @@ -96,7 +105,7 @@ impl AppServer { let block = Block::default() .title("Press 'c' to reset the counter!") .borders(Borders::ALL); - f.render_widget(paragraph.block(block), size); + f.render_widget(paragraph.block(block), area); }) .unwrap(); } @@ -137,18 +146,17 @@ impl Handler for AppServer { channel: Channel, session: &mut Session, ) -> Result { - { - let mut clients = self.clients.lock().await; - let terminal_handle = TerminalHandle { - handle: session.handle(), - sink: Vec::new(), - channel_id: channel.id(), - }; - - let backend = CrosstermBackend::new(terminal_handle.clone()); - let terminal = Terminal::new(backend)?; - clients.insert(self.id, terminal); - } + let terminal_handle = TerminalHandle::start(session.handle(), channel.id()).await; + + let backend = CrosstermBackend::new(terminal_handle); + + // the correct viewport area will be set when the client request a pty + let options = TerminalOptions { viewport: Viewport::Fixed(Rect::default()) }; + + let terminal = Terminal::with_options(backend, options)?; + + let mut clients = self.clients.lock().await; + clients.insert(self.id, terminal); Ok(true) } @@ -191,18 +199,48 @@ impl Handler for AppServer { _: u32, _: &mut Session, ) -> Result<(), Self::Error> { - let mut terminal = { - let clients = self.clients.lock().await; - clients.get(&self.id).unwrap().clone() + let rect = Rect { + x: 0, + y: 0, + width: col_width as u16, + height: row_height as u16, }; + + let mut clients = self.clients.lock().await; + clients.get_mut(&self.id).unwrap().resize(rect)?; + + Ok(()) + } + + /// The client requests a pseudo-terminal with the given + /// specifications. + /// + /// **Note:** Success or failure should be communicated to the client by calling + /// `session.channel_success(channel)` or `session.channel_failure(channel)` respectively. + async fn pty_request( + &mut self, + channel: ChannelId, + _: &str, + col_width: u32, + row_height: u32, + _: u32, + _: u32, + _: &[(Pty, u32)], + session: &mut Session, + ) -> Result<(), Self::Error> { let rect = Rect { x: 0, y: 0, width: col_width as u16, height: row_height as u16, }; + + let mut clients = self.clients.lock().await; + let terminal = clients.get_mut(&self.id).unwrap(); terminal.resize(rect)?; + session.channel_success(channel)?; + Ok(()) } } From 75d72bc5fa0a425b2a5554203cd14bf9c5fb4dba Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 21 Nov 2024 19:38:01 +0100 Subject: [PATCH 2/2] fmt --- russh/examples/ratatui_app.rs | 16 ++++++++++++---- russh/examples/ratatui_shared_app.rs | 16 ++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/russh/examples/ratatui_app.rs b/russh/examples/ratatui_app.rs index 94909fc6..e63fe1cb 100644 --- a/russh/examples/ratatui_app.rs +++ b/russh/examples/ratatui_app.rs @@ -8,10 +8,10 @@ use ratatui::layout::Rect; use ratatui::style::{Color, Style}; use ratatui::widgets::{Block, Borders, Clear, Paragraph}; use ratatui::{Terminal, TerminalOptions, Viewport}; -use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use russh::keys::ssh_key::PublicKey; use russh::server::*; use russh::{Channel, ChannelId, Pty}; +use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use tokio::sync::Mutex; type SshTerminal = Terminal>; @@ -43,7 +43,10 @@ impl TerminalHandle { } } }); - Self { sender, sink: Vec::new() } + Self { + sender, + sink: Vec::new(), + } } } @@ -57,7 +60,10 @@ impl std::io::Write for TerminalHandle { fn flush(&mut self) -> std::io::Result<()> { let result = self.sender.send(self.sink.clone()); if result.is_err() { - return Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, result.unwrap_err())); + return Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + result.unwrap_err(), + )); } self.sink.clear(); @@ -149,7 +155,9 @@ impl Handler for AppServer { let backend = CrosstermBackend::new(terminal_handle); // the correct viewport area will be set when the client request a pty - let options = TerminalOptions { viewport: Viewport::Fixed(Rect::default()) }; + let options = TerminalOptions { + viewport: Viewport::Fixed(Rect::default()), + }; let terminal = Terminal::with_options(backend, options)?; let app = App::new(); diff --git a/russh/examples/ratatui_shared_app.rs b/russh/examples/ratatui_shared_app.rs index 396af7cd..e023d862 100644 --- a/russh/examples/ratatui_shared_app.rs +++ b/russh/examples/ratatui_shared_app.rs @@ -8,10 +8,10 @@ use ratatui::layout::Rect; use ratatui::style::{Color, Style}; use ratatui::widgets::{Block, Borders, Clear, Paragraph}; use ratatui::{Terminal, TerminalOptions, Viewport}; -use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use russh::keys::ssh_key::PublicKey; use russh::server::*; use russh::{Channel, ChannelId, Pty}; +use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use tokio::sync::Mutex; type SshTerminal = Terminal>; @@ -43,7 +43,10 @@ impl TerminalHandle { } } }); - Self { sender, sink: Vec::new() } + Self { + sender, + sink: Vec::new(), + } } } @@ -57,7 +60,10 @@ impl std::io::Write for TerminalHandle { fn flush(&mut self) -> std::io::Result<()> { let result = self.sender.send(self.sink.clone()); if result.is_err() { - return Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, result.unwrap_err())); + return Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + result.unwrap_err(), + )); } self.sink.clear(); @@ -151,7 +157,9 @@ impl Handler for AppServer { let backend = CrosstermBackend::new(terminal_handle); // the correct viewport area will be set when the client request a pty - let options = TerminalOptions { viewport: Viewport::Fixed(Rect::default()) }; + let options = TerminalOptions { + viewport: Viewport::Fixed(Rect::default()), + }; let terminal = Terminal::with_options(backend, options)?;