1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
//! A terminal session whose program runs in another process, reached through a carrier: a desktop
//! whose programs outlive the connection to its screen keeps them in a process of their own, and a
//! screen that attaches draws them through one of these.
use std::io::{self, Read};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use super::terminal_session::{Backend, SCROLLBACK, SIZE, Shared, TerminalSession, lock, read_output};
/// What a [`TerminalSession`] whose program lives in another process sends to it: the keys and
/// pastes, the size of the screen and the request to end it. The program's output comes the other
/// way, as the reader given to [`TerminalSession::attach`].
///
/// The application implements it on whatever reaches the program, a socket to the process that
/// runs it, say, with the framing that process reads. The session calls it on the thread that
/// writes, resizes or ends; a call that waits holds that thread, so a carrier that cannot write at
/// once is better off queueing the bytes than blocking.
pub trait TerminalCarrier: Send + 'static {
/// Sends `bytes` to the program as its input: the person's keys, a paste, a report the
/// terminal makes on its own account.
///
/// # Errors
///
/// Whatever went wrong on the way; the session hands it back to the caller of
/// [`TerminalSession::write`] or [`TerminalSession::paste`].
fn write(&mut self, bytes: &[u8]) -> io::Result<()>;
/// Tells the program its screen is now `columns` by `rows`, as a pseudo-terminal's resize
/// does. Called by the session's watch, never while drawing.
///
/// # Errors
///
/// Whatever went wrong on the way; the session draws the new size either way.
fn resize(&mut self, columns: u16, rows: u16) -> io::Result<()>;
/// Asks for the program to be ended: [`TerminalSession::kill`] and
/// [`TerminalSession::terminate`] of an attached session come here. Letting the session go
/// does not, since the program is meant to outlive the screen that showed it.
fn end(&mut self);
/// The program's exit code, asked once its output has ended: what the process that ran it
/// says it ended with, or `None` when it cannot say, as when the connection to it closed with
/// the program still running. [`TerminalWatch`](super::TerminalWatch) reports it as
/// [`Exited`](super::TerminalChange::Exited). Default: `None`.
fn exit_code(&mut self) -> Option<u32> {
None
}
}
/// How to attach a [`TerminalSession`] to a program in another process: the screen size, the
/// scrollback and how often output is reported. Made by [`TerminalSession::attach`].
///
/// The screen must start at the size the program's own session has, since what it is sent first,
/// that session's [`snapshot`](TerminalSession::snapshot), draws a screen of that size; a
/// [`Terminal`](super::Terminal) widget resizes it to its own size afterwards, as it does any
/// session.
#[must_use]
pub struct AttachedBuilder<R, C> {
output: R,
carrier: C,
size: (u16, u16),
scrollback: usize,
coalesce: Duration,
}
impl<R: Read + Send + 'static, C: TerminalCarrier> AttachedBuilder<R, C> {
/// The size of the screen the program draws on now, in columns and rows. Sizes below 2 count
/// as 2. Default: 80 × 24.
pub fn size(mut self, columns: u16, rows: u16) -> Self {
self.size = (rows, columns);
self
}
/// Lines kept above the screen for scrolling back; `0` keeps none. To keep all of the history
/// a snapshot sends, keep at least as many as the program's own session does. Default: 5000.
pub fn scrollback(mut self, lines: usize) -> Self {
self.scrollback = lines;
self
}
/// Reports output at most once per `interval`, as
/// [`TerminalBuilder::coalesce`](super::TerminalBuilder::coalesce) does. Default: zero.
pub fn coalesce(mut self, interval: Duration) -> Self {
self.coalesce = interval;
self
}
/// Starts reading the program's output and returns the session.
pub fn start(self) -> TerminalSession {
let size = (self.size.0.max(2), self.size.1.max(2));
let shared = Shared::new(size, self.scrollback, self.coalesce);
let backend = Arc::new(Mutex::new(Backend::Carried(Box::new(self.carrier))));
read_output(Box::new(self.output), Arc::clone(&shared), Arc::downgrade(&backend), None, |backend| {
let backend = backend.upgrade()?;
match &mut *lock(&backend) {
Backend::Carried(carrier) => carrier.exit_code(),
Backend::Pty(_) => None,
}
});
TerminalSession { shared, backend, pid: None }
}
}
impl TerminalSession {
/// Starts describing a session whose program runs in another process: `output` brings the
/// bytes the program writes, as that process relays them, and `carrier` takes the input, the
/// size and the request to end it. Finish with [`AttachedBuilder::start`].
///
/// The session is drawn and watched exactly like one that runs its program here: a
/// [`Terminal`](super::Terminal) widget shows it and sends keys through the carrier, and its
/// [`TerminalWatch`](super::TerminalWatch) reports output, the title, the folder, the bell,
/// notifications and copied text, and the end once `output` ends, with the code
/// [`TerminalCarrier::exit_code`] gives.
///
/// Two things differ. The program's questions to the terminal, such as where the cursor is or
/// which colours it draws, are not answered here: the session in the program's own process
/// answered them already, and a second answer would reach the program's input twice. And
/// letting the last handle go leaves the program running, since it lives elsewhere and is
/// meant to outlive the screen; [`kill`](Self::kill) and [`terminate`](Self::terminate) ask
/// the carrier to [end](TerminalCarrier::end) it. There is no [`pid`](Self::pid) here.
///
/// To draw what the program's own session draws, `output` starts with that session's
/// [`snapshot`](Self::snapshot), on a screen of the same [`size`](AttachedBuilder::size), and
/// goes on with the bytes the program writes after it, which the process that runs it can
/// hear with [`TerminalBuilder::tee`](super::TerminalBuilder::tee).
///
/// ```no_run
/// use std::io;
/// use std::os::unix::net::UnixStream;
/// use qframe::widgets::{TerminalCarrier, TerminalSession};
///
/// /// Input and sizes go to the process that runs the program, one message a line.
/// struct Socket(UnixStream);
///
/// impl TerminalCarrier for Socket {
/// fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
/// use io::Write;
/// writeln!(self.0, "keys {}", bytes.len())?;
/// self.0.write_all(bytes)
/// }
/// fn resize(&mut self, columns: u16, rows: u16) -> io::Result<()> {
/// use io::Write;
/// writeln!(self.0, "size {columns} {rows}")
/// }
/// fn end(&mut self) {
/// use io::Write;
/// let _ = writeln!(self.0, "end");
/// }
/// }
///
/// let control = UnixStream::connect("desk-1.control")?;
/// let output = UnixStream::connect("desk-1.output")?;
/// let session = TerminalSession::attach(output, Socket(control)).size(120, 40).start();
/// # Ok::<(), io::Error>(())
/// ```
pub fn attach<R: Read + Send + 'static, C: TerminalCarrier>(output: R, carrier: C) -> AttachedBuilder<R, C> {
AttachedBuilder { output, carrier, size: SIZE, scrollback: SCROLLBACK, coalesce: Duration::ZERO }
}
/// The short form of [`TerminalSession::attach`] with every option at its default: an 80 × 24
/// screen keeping 5000 lines of scrollback.
pub fn attached(output: impl Read + Send + 'static, carrier: impl TerminalCarrier) -> TerminalSession {
Self::attach(output, carrier).start()
}
}
#[cfg(test)]
#[path = "terminal_attached_tests.rs"]
mod tests;