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
//! The bytes that draw a terminal session's screen again from nothing: its scrollback, its screen,
//! the screen beneath when a program is on the alternate one, the cursor and the modes the
//! program turned on.
use super::terminal_notice::Heard;
use super::terminal_session::{TerminalSession, lock};
/// Leaves the alternate screen, putting back the cursor saved when it was entered.
const LEAVE_ALTERNATE: &[u8] = b"\x1b[?1049l";
/// Saves the cursor and enters the alternate screen, cleared.
const ENTER_ALTERNATE: &[u8] = b"\x1b[?1049h";
/// Ends a line of scrollback: attributes off, so the next line starts plain, and down a row.
const END_OF_LINE: &[u8] = b"\x1b[m\r\n";
/// Turns on the focus reports of xterm's mode 1004.
const FOCUS_REPORTS: &[u8] = b"\x1b[?1004h";
impl TerminalSession {
/// Bytes that, given to an empty session with the same screen size, draw what this one
/// draws: its scrollback, oldest line first, its screen with every colour and attribute, the
/// screen beneath when the program is on the alternate one, the cursor, and the modes the
/// program turned on, application keys, bracketed paste, mouse reporting, focus reports and
/// the kitty keyboard flags. Taken while the session's screen is locked, so it is one moment
/// of it.
///
/// It is what an [attached](Self::attach) session is sent first, before the bytes the program
/// writes after it: a screen that attaches to a program running elsewhere draws it as it was
/// when it left, then follows it. Leaving the alternate screen later shows the screen beneath
/// as it was, since that is drawn first.
///
/// The whole scrollback the session keeps is sent, which its
/// [`scrollback`](super::TerminalBuilder::scrollback) bounds; a line that wrapped arrives as a
/// line of its own. What the program said about itself, its title and folder, is reported by
/// its watch as it happens and is not repeated here.
///
/// A process that relays the program's output through a
/// [`tee`](super::TerminalBuilder::tee) listener to screens that attach takes the snapshot with
/// [`snapshot_then`](Self::snapshot_then) instead, so no chunk falls between the snapshot and
/// the relay.
#[must_use]
pub fn snapshot(&self) -> Vec<u8> {
snapshot(&self.parser())
}
/// Takes the [`snapshot`](Self::snapshot) and hands it to `attach` while no chunk of the
/// program's output is on its way from the [`tee`](super::TerminalBuilder::tee) listener to
/// this session's screen, and returns what `attach` returns.
///
/// The listener hears each chunk before the screen has it. A screen that attaches with a plain
/// `snapshot` in that moment finds the chunk in neither the snapshot nor its relay, and lacks
/// it until the program draws that part again. Here every chunk is either in the snapshot or
/// heard after `attach` returns, so a relay that `attach` starts misses nothing:
///
/// ```no_run
/// use std::io::{self, Write};
/// use std::sync::{Arc, Mutex};
/// use qframe::widgets::TerminalSession;
///
/// let relay: Arc<Mutex<Option<io::PipeWriter>>> = Arc::default();
/// let heard = Arc::clone(&relay);
/// let session = TerminalSession::builder("htop")
/// .tee(move |bytes| {
/// if let Some(pipe) = heard.lock().unwrap().as_mut() {
/// let _ = pipe.write_all(bytes);
/// }
/// })
/// .spawn()?;
///
/// // A screen attaches: it reads from a pipe that starts with the snapshot and goes on with
/// // what follows. It reads before the snapshot is written, which can be more than a pipe holds.
/// let (output, mut pipe) = io::pipe()?;
/// # struct Back;
/// # impl qframe::widgets::TerminalCarrier for Back {
/// # fn write(&mut self, _: &[u8]) -> io::Result<()> { Ok(()) }
/// # fn resize(&mut self, _: u16, _: u16) -> io::Result<()> { Ok(()) }
/// # fn end(&mut self) {}
/// # }
/// # let back_to_the_program = Back;
/// let screen = TerminalSession::attached(output, back_to_the_program);
/// session.snapshot_then(|snapshot| {
/// pipe.write_all(snapshot)?;
/// *relay.lock().unwrap() = Some(pipe);
/// io::Result::Ok(())
/// })?;
/// # Ok::<(), io::Error>(())
/// ```
///
/// The program's output waits while `attach` runs, so it only writes the snapshot out and sets
/// the relay. Take any lock the listener takes inside `attach`, never around this call: the
/// reading thread holds the wait while the listener runs, and a lock held around the call
/// would have each wait for the other. The screen itself stays free, so a widget showing this
/// session keeps drawing meanwhile.
pub fn snapshot_then<T>(&self, attach: impl FnOnce(&[u8]) -> T) -> T {
let _between = lock(&self.shared.feed);
let bytes = self.snapshot();
attach(&bytes)
}
}
/// The bytes of [`TerminalSession::snapshot`] for the screen `parser` holds.
pub(super) fn snapshot(parser: &vt100::Parser<Heard>) -> Vec<u8> {
let live = parser.screen();
let (rows, columns) = live.size();
let alternate = live.alternate_screen();
let heard = parser.callbacks();
// A copy to read from, so the screen a person may be scrolling back through is left where it is.
let mut copy = vt100::Parser::new(rows, columns, 0);
*copy.screen_mut() = live.clone();
if alternate {
// The screen beneath is not shown while the program is on the alternate one; the copy
// leaves it to read it.
copy.process(LEAVE_ALTERNATE);
}
let main = copy.screen_mut();
let mut out = Vec::new();
// The oldest kept line is as far back as the view goes; each one is the top row there.
main.set_scrollback(usize::MAX);
let kept = main.scrollback();
for back in (1..=kept).rev() {
main.set_scrollback(back);
if let Some(row) = main.rows_formatted(0, columns).next() {
out.extend_from_slice(&row);
}
out.extend_from_slice(END_OF_LINE);
}
if kept > 0 {
// Every line above ends with a line break, and the screen scrolls a line into the
// scrollback only for a break on its last row: as many more as the screen has rows less
// one push all of them up, and the screen is drawn over the blank rows left.
for _ in 1..rows {
out.extend_from_slice(b"\r\n");
}
}
main.set_scrollback(0);
out.extend_from_slice(&main.contents_formatted());
push_keyboard(&mut out, heard.keyboard.pushed(false));
if alternate {
// Saved on entering, so leaving later puts the cursor back where the screen beneath had it.
out.extend_from_slice(&main.cursor_state_formatted());
out.extend_from_slice(ENTER_ALTERNATE);
out.extend_from_slice(&live.contents_formatted());
push_keyboard(&mut out, heard.keyboard.pushed(true));
}
out.extend_from_slice(&live.input_mode_formatted());
if heard.focus.reporting() {
out.extend_from_slice(FOCUS_REPORTS);
}
out.extend_from_slice(&live.cursor_state_formatted());
out
}
/// Pushes the kitty keyboard flags `flags`, oldest first, on the screen in use.
fn push_keyboard(out: &mut Vec<u8>, flags: &[u16]) {
for flags in flags {
out.extend_from_slice(format!("\x1b[>{flags}u").as_bytes());
}
}