Skip to content

Commit 86c9470

Browse files
committed
feat: add experimental shpool-vterm engine
This patch integrates the new shpool-vterm crate with shpool as a replacement for shpool_vt100. For now, this is opt in, and I expect it to be pretty broken in lots of ways that we'll find out as people start trying it out. By default, we still use the old shpool_vt100 crate. This is progress on #46
1 parent e4d7b79 commit 86c9470

7 files changed

Lines changed: 192 additions & 6 deletions

File tree

Cargo.lock

Lines changed: 30 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libshpool/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ log = "0.4" # logging facade (not used directly, but required if we have tracing
3636
tracing = "0.1" # logging and performance monitoring facade
3737
rmp-serde = "1" # serialization for the control protocol
3838
shpool_vt100 = "0.1.3" # terminal emulation for the scrollback buffer
39+
shpool-vterm = "0.1.0" # alt terminal emulation for the scrollback buffer
3940
shell-words = "1" # parsing the -c/--cmd argument
4041
motd = { version = "0.2.2", default-features = false, features = [] } # getting the message-of-the-day
4142
termini = "1.0.0" # terminfo database

libshpool/src/config.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,12 @@ pub struct Config {
245245
/// existing session.
246246
pub session_restore_mode: Option<SessionRestoreMode>,
247247

248+
/// Selects the virtual terminal to use for lines and screen
249+
/// mode session restoration. By default, this is Vt100,
250+
/// but you can opt into the experimental Vterm engine
251+
/// if you want to try it out.
252+
pub session_restore_engine: Option<SessionRestoreEngine>,
253+
248254
/// The number of lines worth of output to keep in the output
249255
/// spool which is maintained along side a shell session.
250256
/// By default, 10000 lines.
@@ -312,6 +318,7 @@ impl Config {
312318
forward_env: self.forward_env.or(another.forward_env),
313319
initial_path: self.initial_path.or(another.initial_path),
314320
session_restore_mode: self.session_restore_mode.or(another.session_restore_mode),
321+
session_restore_engine: self.session_restore_engine.or(another.session_restore_engine),
315322
output_spool_lines: self.output_spool_lines.or(another.output_spool_lines),
316323
vt100_output_spool_width: self
317324
.vt100_output_spool_width
@@ -352,6 +359,16 @@ pub enum SessionRestoreMode {
352359
Lines(u16),
353360
}
354361

362+
#[derive(Deserialize, Debug, Clone, Default)]
363+
#[serde(rename_all = "lowercase")]
364+
pub enum SessionRestoreEngine {
365+
/// Use the shpool_vt100 crate for session restore.
366+
#[default]
367+
Vt100,
368+
/// Use the shpool-vterm crate for session restore.
369+
Vterm,
370+
}
371+
355372
#[derive(Deserialize, Debug, Clone, Default)]
356373
#[serde(rename_all = "lowercase")]
357374
pub enum MotdDisplayMode {
@@ -413,6 +430,12 @@ mod test {
413430
binding = "Ctrl-q a"
414431
action = "detach"
415432
"#,
433+
r#"
434+
session_restore_engine = "vt100"
435+
"#,
436+
r#"
437+
session_restore_engine = "vterm"
438+
"#,
416439
];
417440

418441
for case in cases.into_iter() {

libshpool/src/session_restore.rs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
use shpool_protocol::TtySize;
1616
use tracing::info;
1717

18-
use crate::config::{self, SessionRestoreMode};
18+
use crate::config::{self, SessionRestoreMode, SessionRestoreEngine};
1919

2020
// To prevent data getting dropped, we set this to be large, but we don't want
2121
// to use u16::MAX, since the vt100 crate eagerly fills in its rows, and doing
@@ -119,24 +119,59 @@ impl SessionSpool for Vt100Lines {
119119
}
120120
}
121121

122+
/// A spool that restores the last screenful of content using shpool-vterm.
123+
pub struct Vterm {
124+
term: shpool_vterm::Term,
125+
mode: SessionRestoreMode,
126+
}
127+
128+
impl SessionSpool for Vterm {
129+
fn resize(&mut self, size: TtySize) {
130+
self.term.resize(shpool_vterm::Size {
131+
height: size.rows as usize,
132+
width: size.cols as usize,
133+
});
134+
}
135+
136+
fn restore_buffer(&self) -> Vec<u8> {
137+
match self.mode {
138+
SessionRestoreMode::Simple => vec![],
139+
SessionRestoreMode::Screen => self.term.contents(shpool_vterm::ContentRegion::Screen),
140+
SessionRestoreMode::Lines(nlines) => self.term.contents(shpool_vterm::ContentRegion::BottomLines(nlines as usize)),
141+
}
142+
}
143+
144+
fn process(&mut self, bytes: &[u8]) {
145+
self.term.process(bytes);
146+
}
147+
}
148+
122149
/// Creates a spool given a `mode`.
123150
pub fn new(
124151
config: config::Manager,
125152
size: &TtySize,
126153
scrollback_lines: usize,
127154
) -> Box<dyn SessionSpool + 'static> {
155+
let restore_engine = config.get().session_restore_engine.clone().unwrap_or_default();
128156
let mode = config.get().session_restore_mode.clone().unwrap_or_default();
129157
let vterm_width = config.vterm_width();
130-
match mode {
131-
SessionRestoreMode::Simple => Box::new(NullSpool),
132-
SessionRestoreMode::Screen => Box::new(Vt100Screen {
158+
match (mode, restore_engine) {
159+
(SessionRestoreMode::Simple, _) => Box::new(NullSpool),
160+
(SessionRestoreMode::Screen, SessionRestoreEngine::Vt100) => Box::new(Vt100Screen {
133161
parser: shpool_vt100::Parser::new(size.rows, vterm_width, scrollback_lines),
134162
config,
135163
}),
136-
SessionRestoreMode::Lines(nlines) => Box::new(Vt100Lines {
164+
(SessionRestoreMode::Lines(nlines), SessionRestoreEngine::Vt100) => Box::new(Vt100Lines {
137165
parser: shpool_vt100::Parser::new(size.rows, vterm_width, scrollback_lines),
138166
nlines,
139167
config,
140168
}),
169+
(mode, SessionRestoreEngine::Vterm) => Box::new(Vterm {
170+
term: shpool_vterm::Term::new(scrollback_lines, shpool_vterm::Size{
171+
width: size.cols as usize,
172+
height: size.rows as usize,
173+
}),
174+
mode,
175+
}),
141176
}
142177
}

shpool/tests/data/vterm_lines.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
norc = true
2+
noecho = true
3+
shell = "/bin/bash"
4+
session_restore_mode = { lines = 5 }
5+
session_restore_engine = "vterm"
6+
prompt_prefix = ""
7+
8+
[env]
9+
PS1 = "prompt> "
10+
TERM = ""
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
norc = true
2+
noecho = true
3+
shell = "/bin/bash"
4+
session_restore_mode = "screen"
5+
session_restore_engine = "vterm"
6+
prompt_prefix = ""
7+
8+
[env]
9+
PS1 = "prompt> "
10+
TERM = ""

shpool/tests/vterm.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#![allow(clippy::literal_string_with_formatting_args)]
2+
3+
use anyhow::Context;
4+
use ntest::timeout;
5+
6+
mod support;
7+
8+
use crate::support::{
9+
daemon::DaemonArgs,
10+
};
11+
12+
#[test]
13+
#[timeout(30000)]
14+
fn screen_restore() -> anyhow::Result<()> {
15+
let mut daemon_proc = support::daemon::Proc::new("vterm_screen.toml", DaemonArgs::default())
16+
.context("starting daemon proc")?;
17+
let bidi_done_w = daemon_proc.events.take().unwrap().waiter(["daemon-bidi-stream-done"]);
18+
19+
{
20+
let mut attach_proc =
21+
daemon_proc.attach("sh1", Default::default()).context("starting attach proc")?;
22+
let mut line_matcher = attach_proc.line_matcher()?;
23+
24+
attach_proc.run_cmd("echo foo")?;
25+
line_matcher.scan_until_re("foo$")?;
26+
}
27+
28+
// wait until the daemon has noticed that the connection
29+
// has dropped before we attempt to open the connection again
30+
daemon_proc.events = Some(bidi_done_w.wait_final_event("daemon-bidi-stream-done")?);
31+
32+
{
33+
let mut attach_proc =
34+
daemon_proc.attach("sh1", Default::default()).context("starting attach proc")?;
35+
let mut line_matcher = attach_proc.line_matcher()?;
36+
37+
// the re-attach should redraw the screen for us, so we should
38+
// get a line with "foo" as part of the re-drawn screen.
39+
line_matcher.scan_until_re("foo$")?;
40+
41+
attach_proc.proc.kill()?;
42+
}
43+
44+
Ok(())
45+
}
46+
47+
#[test]
48+
#[timeout(30000)]
49+
fn lines_restore() -> anyhow::Result<()> {
50+
let mut daemon_proc = support::daemon::Proc::new("vterm_lines.toml", DaemonArgs::default())
51+
.context("starting daemon proc")?;
52+
let bidi_done_w = daemon_proc.events.take().unwrap().waiter(["daemon-bidi-stream-done"]);
53+
54+
{
55+
let mut attach_proc =
56+
daemon_proc.attach("sh1", Default::default()).context("starting attach proc")?;
57+
let mut line_matcher = attach_proc.line_matcher()?;
58+
59+
attach_proc.run_cmd("echo foo")?;
60+
line_matcher.scan_until_re("foo$")?;
61+
}
62+
63+
// wait until the daemon has noticed that the connection
64+
// has dropped before we attempt to open the connection again
65+
daemon_proc.events = Some(bidi_done_w.wait_final_event("daemon-bidi-stream-done")?);
66+
67+
{
68+
let mut attach_proc =
69+
daemon_proc.attach("sh1", Default::default()).context("starting attach proc")?;
70+
let mut line_matcher = attach_proc.line_matcher()?;
71+
72+
// the re-attach should redraw the last 2 lines for us, so we should
73+
// get a line with "foo" as part of the re-drawn screen.
74+
line_matcher.scan_until_re("foo$")?;
75+
}
76+
77+
Ok(())
78+
}

0 commit comments

Comments
 (0)