Skip to content

Commit 33eac12

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 33eac12

7 files changed

Lines changed: 190 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, SessionRestoreEngine, SessionRestoreMode};
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
131+
.resize(shpool_vterm::Size { height: size.rows as usize, width: size.cols as usize });
132+
}
133+
134+
fn restore_buffer(&self) -> Vec<u8> {
135+
match self.mode {
136+
SessionRestoreMode::Simple => vec![],
137+
SessionRestoreMode::Screen => self.term.contents(shpool_vterm::ContentRegion::Screen),
138+
SessionRestoreMode::Lines(nlines) => {
139+
self.term.contents(shpool_vterm::ContentRegion::BottomLines(nlines as usize))
140+
}
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(
171+
scrollback_lines,
172+
shpool_vterm::Size { width: size.cols as usize, 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: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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::daemon::DaemonArgs;
9+
10+
#[test]
11+
#[timeout(30000)]
12+
fn screen_restore() -> anyhow::Result<()> {
13+
let mut daemon_proc = support::daemon::Proc::new("vterm_screen.toml", DaemonArgs::default())
14+
.context("starting daemon proc")?;
15+
let bidi_done_w = daemon_proc.events.take().unwrap().waiter(["daemon-bidi-stream-done"]);
16+
17+
{
18+
let mut attach_proc =
19+
daemon_proc.attach("sh1", Default::default()).context("starting attach proc")?;
20+
let mut line_matcher = attach_proc.line_matcher()?;
21+
22+
attach_proc.run_cmd("echo foo")?;
23+
line_matcher.scan_until_re("foo$")?;
24+
}
25+
26+
// wait until the daemon has noticed that the connection
27+
// has dropped before we attempt to open the connection again
28+
daemon_proc.events = Some(bidi_done_w.wait_final_event("daemon-bidi-stream-done")?);
29+
30+
{
31+
let mut attach_proc =
32+
daemon_proc.attach("sh1", Default::default()).context("starting attach proc")?;
33+
let mut line_matcher = attach_proc.line_matcher()?;
34+
35+
// the re-attach should redraw the screen for us, so we should
36+
// get a line with "foo" as part of the re-drawn screen.
37+
line_matcher.scan_until_re("foo$")?;
38+
39+
attach_proc.proc.kill()?;
40+
}
41+
42+
Ok(())
43+
}
44+
45+
#[test]
46+
#[timeout(30000)]
47+
fn lines_restore() -> anyhow::Result<()> {
48+
let mut daemon_proc = support::daemon::Proc::new("vterm_lines.toml", DaemonArgs::default())
49+
.context("starting daemon proc")?;
50+
let bidi_done_w = daemon_proc.events.take().unwrap().waiter(["daemon-bidi-stream-done"]);
51+
52+
{
53+
let mut attach_proc =
54+
daemon_proc.attach("sh1", Default::default()).context("starting attach proc")?;
55+
let mut line_matcher = attach_proc.line_matcher()?;
56+
57+
attach_proc.run_cmd("echo foo")?;
58+
line_matcher.scan_until_re("foo$")?;
59+
}
60+
61+
// wait until the daemon has noticed that the connection
62+
// has dropped before we attempt to open the connection again
63+
daemon_proc.events = Some(bidi_done_w.wait_final_event("daemon-bidi-stream-done")?);
64+
65+
{
66+
let mut attach_proc =
67+
daemon_proc.attach("sh1", Default::default()).context("starting attach proc")?;
68+
let mut line_matcher = attach_proc.line_matcher()?;
69+
70+
// the re-attach should redraw the last 2 lines for us, so we should
71+
// get a line with "foo" as part of the re-drawn screen.
72+
line_matcher.scan_until_re("foo$")?;
73+
}
74+
75+
Ok(())
76+
}

0 commit comments

Comments
 (0)