Skip to content

Commit 2d8c08a

Browse files
ogedei-khanEugeny
andauthored
ratatui examples fixed. (#388)
Co-authored-by: Eugene <inbox@null.page>
1 parent 981cf7b commit 2d8c08a

File tree

3 files changed

+169
-77
lines changed

3 files changed

+169
-77
lines changed

russh/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ rand = "0.8.5"
7474
shell-escape = "0.1"
7575
tokio-fd = "0.3"
7676
termion = "2"
77-
ratatui = "0.26.0"
77+
ratatui = "0.29.0"
7878

7979
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
8080
russh-sftp = "2.0.5"

russh/examples/ratatui_app.rs

Lines changed: 89 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ use ratatui::backend::CrosstermBackend;
77
use ratatui::layout::Rect;
88
use ratatui::style::{Color, Style};
99
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
10-
use ratatui::Terminal;
10+
use ratatui::{Terminal, TerminalOptions, Viewport};
1111
use russh::keys::ssh_key::PublicKey;
1212
use russh::server::*;
13-
use russh::{Channel, ChannelId};
13+
use russh::{Channel, ChannelId, Pty};
14+
use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
1415
use tokio::sync::Mutex;
1516

1617
type SshTerminal = Terminal<CrosstermBackend<TerminalHandle>>;
@@ -25,12 +26,28 @@ impl App {
2526
}
2627
}
2728

28-
#[derive(Clone)]
2929
struct TerminalHandle {
30-
handle: Handle,
31-
// The sink collects the data which is finally flushed to the handle.
30+
sender: UnboundedSender<Vec<u8>>,
31+
// The sink collects the data which is finally sent to sender.
3232
sink: Vec<u8>,
33-
channel_id: ChannelId,
33+
}
34+
35+
impl TerminalHandle {
36+
async fn start(handle: Handle, channel_id: ChannelId) -> Self {
37+
let (sender, mut receiver) = unbounded_channel::<Vec<u8>>();
38+
tokio::spawn(async move {
39+
while let Some(data) = receiver.recv().await {
40+
let result = handle.data(channel_id, data.into()).await;
41+
if result.is_err() {
42+
eprintln!("Failed to send data: {:?}", result);
43+
}
44+
}
45+
});
46+
Self {
47+
sender,
48+
sink: Vec::new(),
49+
}
50+
}
3451
}
3552

3653
// The crossterm backend writes to the terminal handle.
@@ -41,15 +58,13 @@ impl std::io::Write for TerminalHandle {
4158
}
4259

4360
fn flush(&mut self) -> std::io::Result<()> {
44-
let handle = self.handle.clone();
45-
let channel_id = self.channel_id;
46-
let data = self.sink.clone().into();
47-
futures::executor::block_on(async move {
48-
let result = handle.data(channel_id, data).await;
49-
if result.is_err() {
50-
eprintln!("Failed to send data: {:?}", result);
51-
}
52-
});
61+
let result = self.sender.send(self.sink.clone());
62+
if result.is_err() {
63+
return Err(std::io::Error::new(
64+
std::io::ErrorKind::BrokenPipe,
65+
result.unwrap_err(),
66+
));
67+
}
5368

5469
self.sink.clear();
5570
Ok(())
@@ -81,8 +96,8 @@ impl AppServer {
8196

8297
terminal
8398
.draw(|f| {
84-
let size = f.size();
85-
f.render_widget(Clear, size);
99+
let area = f.area();
100+
f.render_widget(Clear, area);
86101
let style = match app.counter % 3 {
87102
0 => Style::default().fg(Color::Red),
88103
1 => Style::default().fg(Color::Green),
@@ -94,7 +109,7 @@ impl AppServer {
94109
let block = Block::default()
95110
.title("Press 'c' to reset the counter!")
96111
.borders(Borders::ALL);
97-
f.render_widget(paragraph.block(block), size);
112+
f.render_widget(paragraph.block(block), area);
98113
})
99114
.unwrap();
100115
}
@@ -135,20 +150,20 @@ impl Handler for AppServer {
135150
channel: Channel<Msg>,
136151
session: &mut Session,
137152
) -> Result<bool, Self::Error> {
138-
{
139-
let mut clients = self.clients.lock().await;
140-
let terminal_handle = TerminalHandle {
141-
handle: session.handle(),
142-
sink: Vec::new(),
143-
channel_id: channel.id(),
144-
};
145-
146-
let backend = CrosstermBackend::new(terminal_handle.clone());
147-
let terminal = Terminal::new(backend)?;
148-
let app = App::new();
149-
150-
clients.insert(self.id, (terminal, app));
151-
}
153+
let terminal_handle = TerminalHandle::start(session.handle(), channel.id()).await;
154+
155+
let backend = CrosstermBackend::new(terminal_handle);
156+
157+
// the correct viewport area will be set when the client request a pty
158+
let options = TerminalOptions {
159+
viewport: Viewport::Fixed(Rect::default()),
160+
};
161+
162+
let terminal = Terminal::with_options(backend, options)?;
163+
let app = App::new();
164+
165+
let mut clients = self.clients.lock().await;
166+
clients.insert(self.id, (terminal, app));
152167

153168
Ok(true)
154169
}
@@ -192,17 +207,48 @@ impl Handler for AppServer {
192207
_: u32,
193208
_: &mut Session,
194209
) -> Result<(), Self::Error> {
195-
{
196-
let mut clients = self.clients.lock().await;
197-
let (terminal, _) = clients.get_mut(&self.id).unwrap();
198-
let rect = Rect {
199-
x: 0,
200-
y: 0,
201-
width: col_width as u16,
202-
height: row_height as u16,
203-
};
204-
terminal.resize(rect)?;
205-
}
210+
let rect = Rect {
211+
x: 0,
212+
y: 0,
213+
width: col_width as u16,
214+
height: row_height as u16,
215+
};
216+
217+
let mut clients = self.clients.lock().await;
218+
let (terminal, _) = clients.get_mut(&self.id).unwrap();
219+
terminal.resize(rect)?;
220+
221+
Ok(())
222+
}
223+
224+
/// The client requests a pseudo-terminal with the given
225+
/// specifications.
226+
///
227+
/// **Note:** Success or failure should be communicated to the client by calling
228+
/// `session.channel_success(channel)` or `session.channel_failure(channel)` respectively.
229+
async fn pty_request(
230+
&mut self,
231+
channel: ChannelId,
232+
_: &str,
233+
col_width: u32,
234+
row_height: u32,
235+
_: u32,
236+
_: u32,
237+
_: &[(Pty, u32)],
238+
session: &mut Session,
239+
) -> Result<(), Self::Error> {
240+
let rect = Rect {
241+
x: 0,
242+
y: 0,
243+
width: col_width as u16,
244+
height: row_height as u16,
245+
};
246+
247+
let mut clients = self.clients.lock().await;
248+
let (terminal, _) = clients.get_mut(&self.id).unwrap();
249+
terminal.resize(rect)?;
250+
251+
session.channel_success(channel)?;
206252

207253
Ok(())
208254
}

russh/examples/ratatui_shared_app.rs

Lines changed: 79 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ use ratatui::backend::CrosstermBackend;
77
use ratatui::layout::Rect;
88
use ratatui::style::{Color, Style};
99
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
10-
use ratatui::Terminal;
10+
use ratatui::{Terminal, TerminalOptions, Viewport};
1111
use russh::keys::ssh_key::PublicKey;
1212
use russh::server::*;
13-
use russh::{Channel, ChannelId};
13+
use russh::{Channel, ChannelId, Pty};
14+
use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
1415
use tokio::sync::Mutex;
1516

1617
type SshTerminal = Terminal<CrosstermBackend<TerminalHandle>>;
@@ -25,12 +26,28 @@ impl App {
2526
}
2627
}
2728

28-
#[derive(Clone)]
2929
struct TerminalHandle {
30-
handle: Handle,
31-
// The sink collects the data which is finally flushed to the handle.
30+
sender: UnboundedSender<Vec<u8>>,
31+
// The sink collects the data which is finally sent to sender.
3232
sink: Vec<u8>,
33-
channel_id: ChannelId,
33+
}
34+
35+
impl TerminalHandle {
36+
async fn start(handle: Handle, channel_id: ChannelId) -> Self {
37+
let (sender, mut receiver) = unbounded_channel::<Vec<u8>>();
38+
tokio::spawn(async move {
39+
while let Some(data) = receiver.recv().await {
40+
let result = handle.data(channel_id, data.into()).await;
41+
if result.is_err() {
42+
eprintln!("Failed to send data: {:?}", result);
43+
}
44+
}
45+
});
46+
Self {
47+
sender,
48+
sink: Vec::new(),
49+
}
50+
}
3451
}
3552

3653
// The crossterm backend writes to the terminal handle.
@@ -41,15 +58,13 @@ impl std::io::Write for TerminalHandle {
4158
}
4259

4360
fn flush(&mut self) -> std::io::Result<()> {
44-
let handle = self.handle.clone();
45-
let channel_id = self.channel_id;
46-
let data = self.sink.clone().into();
47-
futures::executor::block_on(async move {
48-
let result = handle.data(channel_id, data).await;
49-
if result.is_err() {
50-
eprintln!("Failed to send data: {:?}", result);
51-
}
52-
});
61+
let result = self.sender.send(self.sink.clone());
62+
if result.is_err() {
63+
return Err(std::io::Error::new(
64+
std::io::ErrorKind::BrokenPipe,
65+
result.unwrap_err(),
66+
));
67+
}
5368

5469
self.sink.clear();
5570
Ok(())
@@ -83,8 +98,8 @@ impl AppServer {
8398
for (_, terminal) in clients.lock().await.iter_mut() {
8499
terminal
85100
.draw(|f| {
86-
let size = f.size();
87-
f.render_widget(Clear, size);
101+
let area = f.area();
102+
f.render_widget(Clear, area);
88103
let style = match counter % 3 {
89104
0 => Style::default().fg(Color::Red),
90105
1 => Style::default().fg(Color::Green),
@@ -96,7 +111,7 @@ impl AppServer {
96111
let block = Block::default()
97112
.title("Press 'c' to reset the counter!")
98113
.borders(Borders::ALL);
99-
f.render_widget(paragraph.block(block), size);
114+
f.render_widget(paragraph.block(block), area);
100115
})
101116
.unwrap();
102117
}
@@ -137,18 +152,19 @@ impl Handler for AppServer {
137152
channel: Channel<Msg>,
138153
session: &mut Session,
139154
) -> Result<bool, Self::Error> {
140-
{
141-
let mut clients = self.clients.lock().await;
142-
let terminal_handle = TerminalHandle {
143-
handle: session.handle(),
144-
sink: Vec::new(),
145-
channel_id: channel.id(),
146-
};
147-
148-
let backend = CrosstermBackend::new(terminal_handle.clone());
149-
let terminal = Terminal::new(backend)?;
150-
clients.insert(self.id, terminal);
151-
}
155+
let terminal_handle = TerminalHandle::start(session.handle(), channel.id()).await;
156+
157+
let backend = CrosstermBackend::new(terminal_handle);
158+
159+
// the correct viewport area will be set when the client request a pty
160+
let options = TerminalOptions {
161+
viewport: Viewport::Fixed(Rect::default()),
162+
};
163+
164+
let terminal = Terminal::with_options(backend, options)?;
165+
166+
let mut clients = self.clients.lock().await;
167+
clients.insert(self.id, terminal);
152168

153169
Ok(true)
154170
}
@@ -191,18 +207,48 @@ impl Handler for AppServer {
191207
_: u32,
192208
_: &mut Session,
193209
) -> Result<(), Self::Error> {
194-
let mut terminal = {
195-
let clients = self.clients.lock().await;
196-
clients.get(&self.id).unwrap().clone()
210+
let rect = Rect {
211+
x: 0,
212+
y: 0,
213+
width: col_width as u16,
214+
height: row_height as u16,
197215
};
216+
217+
let mut clients = self.clients.lock().await;
218+
clients.get_mut(&self.id).unwrap().resize(rect)?;
219+
220+
Ok(())
221+
}
222+
223+
/// The client requests a pseudo-terminal with the given
224+
/// specifications.
225+
///
226+
/// **Note:** Success or failure should be communicated to the client by calling
227+
/// `session.channel_success(channel)` or `session.channel_failure(channel)` respectively.
228+
async fn pty_request(
229+
&mut self,
230+
channel: ChannelId,
231+
_: &str,
232+
col_width: u32,
233+
row_height: u32,
234+
_: u32,
235+
_: u32,
236+
_: &[(Pty, u32)],
237+
session: &mut Session,
238+
) -> Result<(), Self::Error> {
198239
let rect = Rect {
199240
x: 0,
200241
y: 0,
201242
width: col_width as u16,
202243
height: row_height as u16,
203244
};
245+
246+
let mut clients = self.clients.lock().await;
247+
let terminal = clients.get_mut(&self.id).unwrap();
204248
terminal.resize(rect)?;
205249

250+
session.channel_success(channel)?;
251+
206252
Ok(())
207253
}
208254
}

0 commit comments

Comments
 (0)