use ratatui::Terminal;
use ratatui::backend::TestBackend;
use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::text::Span;
use super::app::App;
use super::editor::Mode;
use super::view::{self, CursorPlacement};
pub const WIDTH: u16 = 80;
pub const HEIGHT: u16 = 24;
pub struct Harness {
pub app: App,
}
pub struct Screen {
pub rows: Vec<String>,
pub cursor: Option<CursorPlacement>,
pub buffer: Buffer,
}
impl Screen {
pub fn status(&self) -> &str {
self.rows.last().map_or("", String::as_str)
}
}
impl Harness {
pub fn new(lines: &[&str]) -> Self {
Self::sized(lines, WIDTH, HEIGHT)
}
pub fn sized(lines: &[&str], width: u16, height: u16) -> Self {
let lines = lines.iter().map(|s| s.to_string()).collect();
let mut app = App::new(lines, None, true);
app.set_size(width, height);
Self { app }
}
pub fn at(lines: &[&str], row: usize, col: usize) -> Self {
let mut h = Self::new(lines);
h.app.editor.set_cursor(row, col);
h
}
pub fn inserting(lines: &[&str], row: usize, col: usize) -> Self {
let mut h = Self::at(lines, row, col);
if col >= h.app.editor.line_len(row) {
h.keys("A");
} else {
h.keys("i");
}
assert_eq!(h.cursor(), (row, col), "inserting() start");
h
}
pub fn keys(&mut self, seq: &str) -> &mut Self {
for k in parse_keys(seq) {
self.app.handle_key(k);
}
self
}
pub fn type_text(&mut self, text: &str) -> &mut Self {
for c in text.chars() {
self.app.handle_key(char_key(c));
}
self
}
pub fn lines(&self) -> Vec<String> {
self.app.lines().to_vec()
}
pub fn cursor(&self) -> (usize, usize) {
self.app.cursor()
}
pub fn mode(&self) -> Mode {
self.app.mode()
}
pub fn render(&self) -> Screen {
let (width, height) = self.app.size();
let mut terminal = Terminal::new(TestBackend::new(width, height)).expect("test terminal");
let mut cursor = None;
terminal
.draw(|f| cursor = view::draw(f, &self.app))
.expect("draw");
let backend = terminal.backend();
if let Some(p) = cursor {
assert!(backend.cursor_visible(), "placement without visible cursor");
assert_eq!(backend.cursor_position(), (p.x, p.y).into());
} else {
assert!(!backend.cursor_visible(), "cursor shown without placement");
}
let buffer = backend.buffer().clone();
Screen {
rows: rows(&buffer),
cursor,
buffer,
}
}
}
pub fn rows(buf: &Buffer) -> Vec<String> {
let area = buf.area;
(area.top()..area.bottom())
.map(|y| {
let mut row = String::new();
let mut x = area.left();
while x < area.right() {
let symbol = buf[(x, y)].symbol();
row.push_str(symbol);
x += u16::try_from(Span::raw(symbol).width().max(1)).unwrap_or(1);
}
row
})
.collect()
}
pub fn char_key(c: char) -> KeyEvent {
let mods = if c.is_uppercase() {
KeyModifiers::SHIFT
} else {
KeyModifiers::NONE
};
KeyEvent::new(KeyCode::Char(c), mods)
}
pub fn parse_keys(seq: &str) -> Vec<KeyEvent> {
let mut out = Vec::new();
let mut chars = seq.chars();
while let Some(c) = chars.next() {
if c != '<' {
out.push(char_key(c));
continue;
}
let name: String = chars.by_ref().take_while(|&c| c != '>').collect();
let lower = name.to_lowercase();
let code = match lower.as_str() {
"esc" => KeyCode::Esc,
"cr" | "enter" => KeyCode::Enter,
"bs" | "backspace" => KeyCode::Backspace,
"del" => KeyCode::Delete,
"tab" => KeyCode::Tab,
"left" => KeyCode::Left,
"right" => KeyCode::Right,
"up" => KeyCode::Up,
"down" => KeyCode::Down,
"home" => KeyCode::Home,
"end" => KeyCode::End,
"lt" => {
out.push(char_key('<'));
continue;
}
"space" => {
out.push(char_key(' '));
continue;
}
n => {
let mut rest = n.strip_prefix("c-").unwrap_or_default().chars();
match (rest.next(), rest.next()) {
(Some(c), None) => {
out.push(KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL));
continue;
}
_ => panic!("unknown key name <{name}>"),
}
}
};
out.push(KeyEvent::from(code));
}
out
}