1
//! [`tui::transport::Transport`] that drives the TUI runtime over a
2
//! russh channel.
3
//!
4
//! The bridge bridges three impedance mismatches:
5
//!
6
//! - **sync ↔ async.** `tui::run_loop` is sync; russh's
7
//!   `Handle::data` is async. We capture a `tokio::runtime::Handle`
8
//!   at construction and `block_on` from inside the spawned blocking
9
//!   task that runs the loop.
10
//! - **single byte stream ↔ ratatui frames + raw passthrough.** The
11
//!   ratatui [`CrosstermBackend`] writes ANSI cell escapes; chart
12
//!   code emits kitty graphics APC bytes that must reach the same
13
//!   stream but bypass ratatui's grid. We expose the second path via
14
//!   [`Transport::write_passthrough`] using a separate
15
//!   [`ChannelWriter`] clone so the two emit paths don't fight over
16
//!   the same buffer.
17
//! - **input from `Handler::data` ↔ blocking `Transport::poll`.** The
18
//!   handler shoves [`RawEvent`]s into an `mpsc` channel; the
19
//!   transport's `poll` blocks the runtime thread on `recv` with a
20
//!   tokio timeout via the captured runtime handle.
21

            
22
use ratatui::Terminal;
23
use ratatui::backend::CrosstermBackend;
24
use russh::ChannelId;
25
use russh::server::Handle;
26
use std::io::{self, Write};
27
use std::time::Duration;
28
use tokio::sync::mpsc::UnboundedReceiver;
29
use tui::transport::{RawEvent, Transport};
30

            
31
/// `std::io::Write` that forwards bytes to a russh channel via the
32
/// session's [`Handle`]. `flush` drives the async `Handle::data`
33
/// call from sync code by `block_on`-ing on the runtime captured at
34
/// construction.
35
pub struct ChannelWriter {
36
    handle: Handle,
37
    channel: ChannelId,
38
    runtime: tokio::runtime::Handle,
39
    buffer: Vec<u8>,
40
}
41

            
42
impl ChannelWriter {
43
    pub fn new(handle: Handle, channel: ChannelId, runtime: tokio::runtime::Handle) -> Self {
44
        Self {
45
            handle,
46
            channel,
47
            runtime,
48
            buffer: Vec::with_capacity(4096),
49
        }
50
    }
51

            
52
    fn drain_buffer(&mut self) -> io::Result<()> {
53
        if self.buffer.is_empty() {
54
            return Ok(());
55
        }
56
        let bytes = std::mem::take(&mut self.buffer);
57
        let result = self.runtime.block_on(self.handle.data(self.channel, bytes));
58
        match result {
59
            Ok(()) => Ok(()),
60
            Err(_) => Err(io::Error::new(
61
                io::ErrorKind::BrokenPipe,
62
                "ssh channel closed",
63
            )),
64
        }
65
    }
66
}
67

            
68
impl Write for ChannelWriter {
69
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
70
        self.buffer.extend_from_slice(buf);
71
        Ok(buf.len())
72
    }
73

            
74
    fn flush(&mut self) -> io::Result<()> {
75
        self.drain_buffer()
76
    }
77
}
78

            
79
/// SSH-side transport. Wraps a [`CrosstermBackend`] backed by a
80
/// [`ChannelWriter`] and dequeues input events the handler pushed
81
/// into the session's mpsc.
82
pub struct SshTransport {
83
    terminal: Terminal<CrosstermBackend<ChannelWriter>>,
84
    inputs: UnboundedReceiver<RawEvent>,
85
    passthrough: ChannelWriter,
86
    handle: Handle,
87
    channel: ChannelId,
88
    runtime: tokio::runtime::Handle,
89
    term: Option<String>,
90
}
91

            
92
impl SshTransport {
93
    /// Build a transport bound to the given russh `channel` and
94
    /// `handle`. `term` is the terminal type the peer announced via
95
    /// `pty-req` (e.g. `xterm-kitty`); pass `None` if no PTY was
96
    /// requested.
97
    ///
98
    /// # Errors
99
    ///
100
    /// Returns `Err` if `ratatui::Terminal::new` rejects the
101
    /// backend. Practically never — `CrosstermBackend::new` is
102
    /// infallible.
103
    pub fn new(
104
        handle: Handle,
105
        channel: ChannelId,
106
        runtime: tokio::runtime::Handle,
107
        inputs: UnboundedReceiver<RawEvent>,
108
        term: Option<String>,
109
        cols: u16,
110
        rows: u16,
111
    ) -> io::Result<Self> {
112
        let backend_writer = ChannelWriter::new(handle.clone(), channel, runtime.clone());
113
        let passthrough = ChannelWriter::new(handle.clone(), channel, runtime.clone());
114
        let backend = CrosstermBackend::new(backend_writer);
115
        let mut terminal = Terminal::with_options(
116
            backend,
117
            ratatui::TerminalOptions {
118
                viewport: ratatui::Viewport::Fixed(ratatui::layout::Rect::new(0, 0, cols, rows)),
119
            },
120
        )?;
121
        terminal.clear()?;
122
        Ok(Self {
123
            terminal,
124
            inputs,
125
            passthrough,
126
            handle,
127
            channel,
128
            runtime,
129
            term,
130
        })
131
    }
132
}
133

            
134
impl Transport for SshTransport {
135
    type Backend = CrosstermBackend<ChannelWriter>;
136

            
137
    fn terminal_mut(&mut self) -> &mut Terminal<Self::Backend> {
138
        &mut self.terminal
139
    }
140

            
141
    fn poll(&mut self, timeout: Duration) -> io::Result<Option<RawEvent>> {
142
        self.runtime.block_on(async {
143
            match tokio::time::timeout(timeout, self.inputs.recv()).await {
144
                Ok(Some(ev)) => Ok(Some(ev)),
145
                Ok(None) => Err(io::Error::new(
146
                    io::ErrorKind::UnexpectedEof,
147
                    "ssh channel closed",
148
                )),
149
                Err(_) => Ok(None),
150
            }
151
        })
152
    }
153

            
154
    fn finish(mut self) -> io::Result<()> {
155
        // Best-effort screen reset so the user's prompt comes back
156
        // clean. We don't enter raw mode on the client (the client's
157
        // own terminal is in raw mode) so we only need a reset, not
158
        // a full crossterm teardown.
159
        let _ = self.passthrough.write_all(b"\x1b[?25h\x1b[0m\r\n");
160
        let _ = self.passthrough.flush();
161
        let _ = self.runtime.block_on(self.handle.close(self.channel));
162
        Ok(())
163
    }
164

            
165
    fn write_passthrough(&mut self, bytes: &[u8]) -> io::Result<()> {
166
        self.passthrough.write_all(bytes)?;
167
        self.passthrough.flush()
168
    }
169

            
170
    fn client_term(&self) -> Option<&str> {
171
        self.term.as_deref()
172
    }
173
}