init
This commit is contained in:
+207
@@ -0,0 +1,207 @@
|
||||
use crate::{config::Config, input, pane, render};
|
||||
use base64::Engine;
|
||||
use egui::{Event, Key, Modifiers};
|
||||
use libghostty_vt::{
|
||||
fmt,
|
||||
kitty::graphics::{self, ImageFormat, PlacementIterator},
|
||||
};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
fn plain(term: &libghostty_vt::Terminal<'_, '_>) -> String {
|
||||
let mut formatter =
|
||||
fmt::Formatter::new(term, fmt::FormatterOptions::new().with_trim(true)).unwrap();
|
||||
let bytes = formatter.format_alloc(None).unwrap();
|
||||
String::from_utf8_lossy(&bytes).into_owned()
|
||||
}
|
||||
|
||||
fn key(key: Key, modifiers: Modifiers) -> Event {
|
||||
Event::Key {
|
||||
key,
|
||||
physical_key: Some(key),
|
||||
pressed: true,
|
||||
repeat: false,
|
||||
modifiers,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_preserves_unicode_colors_and_application_input_modes() {
|
||||
let mut term = pane::terminal(&Config::default()).unwrap();
|
||||
term.vt_write("hello \x1b[31mred\x1b[0m \u{754c}e\u{301}\r\n".as_bytes());
|
||||
assert!(plain(&term).contains("hello red \u{754c}e\u{301}"));
|
||||
let events = [
|
||||
key(Key::A, Modifiers::NONE),
|
||||
Event::Text("a".into()),
|
||||
key(Key::B, Modifiers::NONE),
|
||||
Event::Text("b".into()),
|
||||
];
|
||||
assert_eq!(input::encode(&term, &events).unwrap(), b"ab");
|
||||
assert_eq!(
|
||||
input::encode(&term, &[key(Key::C, Modifiers::CTRL)]).unwrap(),
|
||||
b"\x03"
|
||||
);
|
||||
assert_eq!(
|
||||
input::encode(&term, &[key(Key::ArrowUp, Modifiers::NONE)]).unwrap(),
|
||||
b"\x1b[A"
|
||||
);
|
||||
term.vt_write(b"\x1b[?1h");
|
||||
assert_eq!(
|
||||
input::encode(&term, &[key(Key::ArrowUp, Modifiers::NONE)]).unwrap(),
|
||||
b"\x1bOA"
|
||||
);
|
||||
term.resize(12, 6, 9, 20).unwrap();
|
||||
assert!(plain(&term).contains("hello red"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queries_return_to_the_pty_and_alternate_screen_restores() {
|
||||
let mut term = pane::terminal(&Config::default()).unwrap();
|
||||
let response = Rc::new(RefCell::new(vec![]));
|
||||
let writer = response.clone();
|
||||
term.on_pty_write(move |_, bytes| writer.borrow_mut().extend_from_slice(bytes))
|
||||
.unwrap();
|
||||
term.vt_write(b"hello\x1b[6n");
|
||||
assert_eq!(*response.borrow(), b"\x1b[1;6R");
|
||||
term.vt_write(b"\x1b[?1049hother screen\x1b[?1049l");
|
||||
assert!(plain(&term).contains("hello"));
|
||||
assert!(!plain(&term).contains("other screen"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_images_decode_render_reuse_and_delete() {
|
||||
graphics::set_png_decoder(Some(Box::new(render::PngDecoder))).unwrap();
|
||||
let mut term = pane::terminal(&Config::default()).unwrap();
|
||||
let replies = Rc::new(RefCell::new(vec![]));
|
||||
let output = replies.clone();
|
||||
term.on_pty_write(move |_, data| output.borrow_mut().extend_from_slice(data))
|
||||
.unwrap();
|
||||
// Split delivery across VT writes, including the control string terminator.
|
||||
term.vt_write(b"\x1b_Ga=T,f=24,s=2,v=1,i=7,c=4,r=2;//8A");
|
||||
term.vt_write(b"AP8A\x1b\\");
|
||||
assert!(String::from_utf8_lossy(&replies.borrow()).contains("OK"));
|
||||
{
|
||||
let graphics = term.kitty_graphics().unwrap();
|
||||
let image = graphics.image(7).expect("RGB image stored");
|
||||
assert_eq!(image.format().unwrap(), ImageFormat::Rgb);
|
||||
assert_eq!(image.data().unwrap(), [255, 255, 0, 0, 255, 0]);
|
||||
let mut it = PlacementIterator::new().unwrap();
|
||||
let mut ps = it.update(&graphics).unwrap();
|
||||
let info = ps
|
||||
.next()
|
||||
.unwrap()
|
||||
.placement_render_info(&image, &term)
|
||||
.unwrap();
|
||||
assert!(info.viewport_visible);
|
||||
assert_eq!((info.pixel_width, info.pixel_height), (32, 32));
|
||||
}
|
||||
let mut png = vec![];
|
||||
{
|
||||
let mut encoder = png::Encoder::new(&mut png, 2, 1);
|
||||
encoder.set_color(png::ColorType::Rgb);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
encoder
|
||||
.write_header()
|
||||
.unwrap()
|
||||
.write_image_data(&[255, 0, 0, 0, 255, 0])
|
||||
.unwrap();
|
||||
}
|
||||
let payload = base64::engine::general_purpose::STANDARD.encode(png);
|
||||
let split = payload.len() / 8 * 4;
|
||||
term.vt_write(
|
||||
format!(
|
||||
"\x1b_Ga=T,f=100,i=8,c=2,r=1,m=1;{}\x1b\\",
|
||||
&payload[..split]
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
term.vt_write(format!("\x1b_Gm=0;{}\x1b\\", &payload[split..]).as_bytes());
|
||||
{
|
||||
let graphics = term.kitty_graphics().unwrap();
|
||||
let image = graphics.image(8).expect("PNG image stored");
|
||||
assert_eq!(image.format().unwrap(), ImageFormat::Rgba);
|
||||
assert_eq!(image.data().unwrap(), [255, 0, 0, 255, 0, 255, 0, 255]);
|
||||
}
|
||||
let ctx = egui::Context::default();
|
||||
let mut renderer = render::Renderer::new().unwrap();
|
||||
let rect = egui::Rect::from_min_size(egui::pos2(50.0, 60.0), egui::vec2(400.0, 240.0));
|
||||
let mut paint = || {
|
||||
ctx.run(egui::RawInput::default(), |ctx| {
|
||||
let painter = ctx
|
||||
.layer_painter(egui::LayerId::background())
|
||||
.with_clip_rect(rect);
|
||||
renderer
|
||||
.paint(
|
||||
&term,
|
||||
&painter,
|
||||
rect,
|
||||
egui::vec2(8.0, 16.0),
|
||||
&egui::FontId::monospace(12.0),
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
};
|
||||
let first = paint();
|
||||
assert_eq!(
|
||||
first
|
||||
.textures_delta
|
||||
.set
|
||||
.iter()
|
||||
.filter(|(id, _)| matches!(id,egui::TextureId::Managed(n) if *n>0))
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert!(first.shapes.iter().all(|s| rect.contains_rect(s.clip_rect)));
|
||||
let second = paint();
|
||||
assert!(
|
||||
second.textures_delta.set.is_empty(),
|
||||
"unchanged images must reuse GPU textures"
|
||||
);
|
||||
term.vt_write(b"\x1b_Ga=d,d=I,i=7\x1b\\");
|
||||
assert!(term.kitty_graphics().unwrap().image(7).is_none());
|
||||
assert!(term.kitty_graphics().unwrap().image(8).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pty_executes_input_and_bracketed_paste_is_sanitized() {
|
||||
let config = Config {
|
||||
shell: vec!["/bin/sh".into()],
|
||||
..Default::default()
|
||||
};
|
||||
let ctx = egui::Context::default();
|
||||
let mut pane = pane::Pane::new(&config, &ctx).unwrap();
|
||||
pane.send(b"printf '\\033[31mpty-%s\\033[0m\\n' 'success'\r".to_vec())
|
||||
.unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
while Instant::now() < deadline {
|
||||
pane.drain(&ctx);
|
||||
if plain(&pane.terminal).contains("pty-success") {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
assert!(plain(&pane.terminal).contains("pty-success"));
|
||||
pane.pty.resize(91, 31, 9, 20).unwrap();
|
||||
pane.send(b"stty size\r".to_vec()).unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
while Instant::now() < deadline {
|
||||
pane.drain(&ctx);
|
||||
if plain(&pane.terminal).contains("31 91") {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
assert!(plain(&pane.terminal).contains("31 91"));
|
||||
pane.send(b"exit\r".to_vec()).unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
while !pane.dead && Instant::now() < deadline {
|
||||
pane.drain(&ctx);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
assert!(pane.dead);
|
||||
assert!(pane.send(b"after exit".to_vec()).is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user