Skip to content

Commit

Permalink
ratatui examples fixed. (#388)
Browse files Browse the repository at this point in the history
Co-authored-by: Eugene <[email protected]>
  • Loading branch information
ogedei-khan and Eugeny authored Nov 21, 2024
1 parent 981cf7b commit 2d8c08a
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 77 deletions.
2 changes: 1 addition & 1 deletion russh/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
132 changes: 89 additions & 43 deletions russh/examples/ratatui_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 russh::keys::ssh_key::PublicKey;
use russh::server::*;
use russh::{Channel, ChannelId};
use russh::{Channel, ChannelId, Pty};
use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
use tokio::sync::Mutex;

type SshTerminal = Terminal<CrosstermBackend<TerminalHandle>>;
Expand All @@ -25,12 +26,28 @@ impl App {
}
}

#[derive(Clone)]
struct TerminalHandle {
handle: Handle,
// The sink collects the data which is finally flushed to the handle.
sender: UnboundedSender<Vec<u8>>,
// The sink collects the data which is finally sent to sender.
sink: Vec<u8>,
channel_id: ChannelId,
}

impl TerminalHandle {
async fn start(handle: Handle, channel_id: ChannelId) -> Self {
let (sender, mut receiver) = unbounded_channel::<Vec<u8>>();
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.
Expand All @@ -41,15 +58,13 @@ 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(())
Expand Down Expand Up @@ -81,8 +96,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),
Expand All @@ -94,7 +109,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();
}
Expand Down Expand Up @@ -135,20 +150,20 @@ impl Handler for AppServer {
channel: Channel<Msg>,
session: &mut Session,
) -> Result<bool, Self::Error> {
{
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)
}
Expand Down Expand Up @@ -192,17 +207,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(())
}
Expand Down
112 changes: 79 additions & 33 deletions russh/examples/ratatui_shared_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 russh::keys::ssh_key::PublicKey;
use russh::server::*;
use russh::{Channel, ChannelId};
use russh::{Channel, ChannelId, Pty};
use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
use tokio::sync::Mutex;

type SshTerminal = Terminal<CrosstermBackend<TerminalHandle>>;
Expand All @@ -25,12 +26,28 @@ impl App {
}
}

#[derive(Clone)]
struct TerminalHandle {
handle: Handle,
// The sink collects the data which is finally flushed to the handle.
sender: UnboundedSender<Vec<u8>>,
// The sink collects the data which is finally sent to sender.
sink: Vec<u8>,
channel_id: ChannelId,
}

impl TerminalHandle {
async fn start(handle: Handle, channel_id: ChannelId) -> Self {
let (sender, mut receiver) = unbounded_channel::<Vec<u8>>();
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.
Expand All @@ -41,15 +58,13 @@ 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(())
Expand Down Expand Up @@ -83,8 +98,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),
Expand All @@ -96,7 +111,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();
}
Expand Down Expand Up @@ -137,18 +152,19 @@ impl Handler for AppServer {
channel: Channel<Msg>,
session: &mut Session,
) -> Result<bool, Self::Error> {
{
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)
}
Expand Down Expand Up @@ -191,18 +207,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(())
}
}
Expand Down

0 comments on commit 2d8c08a

Please sign in to comment.