mirror of
https://github.com/FliegendeWurst/cursive.git
synced 2024-11-09 10:50:40 +00:00
Test backend (#310)
This commit is contained in:
parent
bddb1a9dde
commit
4a1fc3e49d
135
examples/select_test.rs
Normal file
135
examples/select_test.rs
Normal file
@ -0,0 +1,135 @@
|
||||
// We'll do some automated tests on interface identical to one in select.rs
|
||||
//
|
||||
// To run this example call:
|
||||
// cargo test --example select_test -- --nocapture
|
||||
|
||||
fn main() {
|
||||
print!("To run this example call:\n$ cargo test --example select_test -- --nocapture\n");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
extern crate cursive;
|
||||
|
||||
use cursive::align::HAlign;
|
||||
use cursive::event::EventResult;
|
||||
use cursive::traits::*;
|
||||
use cursive::views::*;
|
||||
use cursive::*;
|
||||
use cursive::backend::puppet::observed::ObservedScreen;
|
||||
use cursive::event::Event;
|
||||
use std::cell::RefCell;
|
||||
use std::time::Duration;
|
||||
use cursive::event::Key;
|
||||
use self::cursive::backend::puppet::observed::{ObservedCell, ObservedPieceInterface};
|
||||
|
||||
pub struct BasicSetup {
|
||||
siv: Cursive,
|
||||
screen_stream: crossbeam_channel::Receiver<ObservedScreen>,
|
||||
input: crossbeam_channel::Sender<Option<Event>>,
|
||||
last_screen: RefCell<Option<ObservedScreen>>,
|
||||
}
|
||||
|
||||
impl BasicSetup {
|
||||
pub fn new() -> Self {
|
||||
let mut select = SelectView::new()
|
||||
// Center the text horizontally
|
||||
.h_align(HAlign::Center)
|
||||
// Use keyboard to jump to the pressed letters
|
||||
.autojump();
|
||||
|
||||
// Read the list of cities from separate file, and fill the view with it.
|
||||
// (We include the file at compile-time to avoid runtime read errors.)
|
||||
let content = include_str!("../assets/cities.txt");
|
||||
select.add_all_str(content.lines());
|
||||
|
||||
// Sets the callback for when "Enter" is pressed.
|
||||
select.set_on_submit(show_next_window);
|
||||
|
||||
// Let's override the `j` and `k` keys for navigation
|
||||
let select = OnEventView::new(select)
|
||||
.on_pre_event_inner('k', |s, _| {
|
||||
s.select_up(1);
|
||||
Some(EventResult::Consumed(None))
|
||||
})
|
||||
.on_pre_event_inner('j', |s, _| {
|
||||
s.select_down(1);
|
||||
Some(EventResult::Consumed(None))
|
||||
});
|
||||
|
||||
let size = Vec2::new(80, 16);
|
||||
let backend = backend::puppet::Backend::init(Some(size));
|
||||
let sink = backend.stream();
|
||||
let input = backend.input();
|
||||
let mut siv = Cursive::new(|| backend);
|
||||
|
||||
// Let's add a BoxView to keep the list at a reasonable size
|
||||
// (it can scroll anyway).
|
||||
siv.add_layer(
|
||||
Dialog::around(select.scrollable().fixed_size((20, 10)))
|
||||
.title("Where are you from?"),
|
||||
);
|
||||
|
||||
input.send(Some(Event::Refresh)).unwrap();
|
||||
siv.step();
|
||||
|
||||
BasicSetup {
|
||||
siv,
|
||||
screen_stream: sink,
|
||||
input,
|
||||
last_screen : RefCell::new(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn last_screen(&self) -> Option<ObservedScreen> {
|
||||
while let Ok(screen) = self.screen_stream.try_recv() {
|
||||
self.last_screen.replace(Some(screen));
|
||||
}
|
||||
|
||||
self.last_screen.borrow().clone()
|
||||
}
|
||||
|
||||
pub fn dump_debug(&self) {
|
||||
self.last_screen().as_ref().map(|s| {
|
||||
s.print_stdout()
|
||||
});
|
||||
}
|
||||
|
||||
pub fn hit_keystroke(&mut self, key: Key) {
|
||||
self.input.send(Some(Event::Key(key))).unwrap();
|
||||
self.siv.step();
|
||||
}
|
||||
}
|
||||
|
||||
// Let's put the callback in a separate function to keep it clean,
|
||||
// but it's not required.
|
||||
fn show_next_window(siv: &mut Cursive, city: &str) {
|
||||
siv.pop_layer();
|
||||
let text = format!("{} is a great city!", city);
|
||||
siv.add_layer(
|
||||
Dialog::around(TextView::new(text)).button("Quit", |s| s.quit()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn displays() {
|
||||
let mut s = BasicSetup::new();
|
||||
let mut screen = s.last_screen().unwrap();
|
||||
s.dump_debug();
|
||||
assert_eq!(screen.find_occurences("Where are you from").len(), 1);
|
||||
assert_eq!(screen.find_occurences("Some random string").len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interacts() {
|
||||
let mut s = BasicSetup::new();
|
||||
s.hit_keystroke(Key::Down);
|
||||
s.hit_keystroke(Key::Enter);
|
||||
|
||||
let mut screen = s.last_screen().unwrap();
|
||||
s.dump_debug();
|
||||
assert_eq!(screen.find_occurences("Abu Dhabi is a great city!").len(), 1);
|
||||
assert_eq!(screen.find_occurences("Abidjan").len(), 0);
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ pub mod blt;
|
||||
pub mod crossterm;
|
||||
pub mod curses;
|
||||
pub mod termion;
|
||||
pub mod puppet;
|
||||
|
||||
/// Trait defining the required methods to be a backend.
|
||||
///
|
||||
|
154
src/backend/puppet/mod.rs
Normal file
154
src/backend/puppet/mod.rs
Normal file
@ -0,0 +1,154 @@
|
||||
//! Puppet backend
|
||||
use crossbeam_channel::{self, Receiver, Sender, TryRecvError};
|
||||
|
||||
use crate::backend;
|
||||
use crate::backend::puppet::observed::ObservedCell;
|
||||
use crate::backend::puppet::observed::ObservedScreen;
|
||||
use crate::backend::puppet::observed::ObservedStyle;
|
||||
use crate::event::Event;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use crate::theme;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
use crate::vec::Vec2;
|
||||
|
||||
pub mod observed;
|
||||
pub mod observed_screen_view;
|
||||
mod static_values;
|
||||
|
||||
use static_values::*;
|
||||
|
||||
/// Puppet backend for testing.
|
||||
pub struct Backend {
|
||||
inner_sender: Sender<Option<Event>>,
|
||||
inner_receiver: Receiver<Option<Event>>,
|
||||
prev_frame: RefCell<Option<ObservedScreen>>,
|
||||
current_frame: RefCell<ObservedScreen>,
|
||||
size: RefCell<Vec2>,
|
||||
current_style: RefCell<Rc<ObservedStyle>>,
|
||||
screen_channel: (Sender<ObservedScreen>, Receiver<ObservedScreen>),
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
/// Creates new Puppet backend of given or default size.
|
||||
pub fn init(size_op: Option<Vec2>) -> Box<Backend>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let (inner_sender, inner_receiver) = crossbeam_channel::unbounded();
|
||||
let size = size_op.unwrap_or(*DEFAULT_SIZE);
|
||||
|
||||
let mut backend = Backend {
|
||||
inner_sender,
|
||||
inner_receiver,
|
||||
prev_frame: RefCell::new(None),
|
||||
current_frame: RefCell::new(ObservedScreen::new(size)),
|
||||
size: RefCell::new(size),
|
||||
current_style: RefCell::new(Rc::new(DEFAULT_OBSERVED_STYLE.clone())),
|
||||
screen_channel: crossbeam_channel::unbounded(),
|
||||
};
|
||||
|
||||
{
|
||||
use backend::Backend;
|
||||
backend.refresh();
|
||||
}
|
||||
|
||||
Box::new(backend)
|
||||
}
|
||||
|
||||
/// Returns current ObservedStyle
|
||||
pub fn current_style(&self) -> Rc<ObservedStyle> {
|
||||
self.current_style.borrow().clone()
|
||||
}
|
||||
|
||||
/// Ouput stream of consecutive frames rendered by Puppet backend
|
||||
pub fn stream(&self) -> Receiver<ObservedScreen> {
|
||||
self.screen_channel.1.clone()
|
||||
}
|
||||
|
||||
/// Input stream to inject artificial input to Puppet backend.
|
||||
pub fn input(&self) -> Sender<Option<Event>> {
|
||||
self.inner_sender.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl backend::Backend for Backend {
|
||||
|
||||
fn poll_event(&mut self) -> Option<Event> {
|
||||
match self.inner_receiver.try_recv() {
|
||||
Ok(event) => event,
|
||||
Err(TryRecvError::Empty) => None,
|
||||
Err(e) => panic!(e)
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&mut self) {}
|
||||
|
||||
fn refresh(&mut self) {
|
||||
let size = self.size.get_mut().clone();
|
||||
let current_frame =
|
||||
self.current_frame.replace(ObservedScreen::new(size));
|
||||
self.prev_frame.replace(Some(current_frame.clone()));
|
||||
self.screen_channel.0.send(current_frame).unwrap();
|
||||
}
|
||||
|
||||
fn has_colors(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn screen_size(&self) -> Vec2 {
|
||||
self.size.borrow().clone()
|
||||
}
|
||||
|
||||
fn print_at(&self, pos: Vec2, text: &str) {
|
||||
let style = self.current_style.borrow().clone();
|
||||
let mut screen = self.current_frame.borrow_mut();
|
||||
let mut offset: usize = 0;
|
||||
|
||||
for (idx, grapheme) in text.graphemes(true).enumerate() {
|
||||
let cpos = pos + Vec2::new(idx + offset, 0);
|
||||
screen[cpos] = Some(ObservedCell::new(
|
||||
cpos,
|
||||
style.clone(),
|
||||
Some(grapheme.to_string()),
|
||||
));
|
||||
|
||||
for _ in 0..grapheme.width() - 1 {
|
||||
offset += 1;
|
||||
let spos = pos + Vec2::new(idx + offset, 0);
|
||||
screen[spos] = Some(ObservedCell::new(spos, style.clone(), None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&self, clear_color: theme::Color) {
|
||||
let mut cloned_style = (*self.current_style()).clone();
|
||||
let mut screen = self.current_frame.borrow_mut();
|
||||
cloned_style.colors.back = clear_color;
|
||||
screen.clear(&Rc::new(cloned_style))
|
||||
}
|
||||
|
||||
// This sets the Colours and returns the previous colours
|
||||
// to allow you to set them back when you're done.
|
||||
fn set_color(&self, new_colors: theme::ColorPair) -> theme::ColorPair {
|
||||
let mut copied_style = (*self.current_style()).clone();
|
||||
let old_colors = copied_style.colors;
|
||||
copied_style.colors = new_colors;
|
||||
self.current_style.replace(Rc::new(copied_style));
|
||||
|
||||
old_colors
|
||||
}
|
||||
|
||||
fn set_effect(&self, effect: theme::Effect) {
|
||||
let mut copied_style = (*self.current_style()).clone();
|
||||
copied_style.effects.insert(effect);
|
||||
self.current_style.replace(Rc::new(copied_style));
|
||||
}
|
||||
|
||||
fn unset_effect(&self, effect: theme::Effect) {
|
||||
let mut copied_style = (*self.current_style()).clone();
|
||||
copied_style.effects.remove(effect);
|
||||
self.current_style.replace(Rc::new(copied_style));
|
||||
}
|
||||
}
|
607
src/backend/puppet/observed.rs
Normal file
607
src/backend/puppet/observed.rs
Normal file
@ -0,0 +1,607 @@
|
||||
//! Structs representing output of puppet backend
|
||||
use enumset::EnumSet;
|
||||
use std::ops::Index;
|
||||
use std::ops::IndexMut;
|
||||
use std::rc::Rc;
|
||||
use std::string::ToString;
|
||||
use crate::theme::ColorPair;
|
||||
use crate::theme::Effect;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
use crate::Vec2;
|
||||
|
||||
/// Style of observed cell
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct ObservedStyle {
|
||||
/// Colors: front and back
|
||||
pub colors: ColorPair,
|
||||
/// Effects enabled on observed cell
|
||||
pub effects: EnumSet<Effect>,
|
||||
}
|
||||
|
||||
/// Contents of observed cell
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum GraphemePart {
|
||||
/// Represents begin of wide character
|
||||
Begin(String),
|
||||
/// Represents a cell that is filled with continuation of some character that begun in cell with lower x-index.
|
||||
Continuation,
|
||||
}
|
||||
|
||||
impl GraphemePart {
|
||||
/// Returns true iff GraphemePart is Continuation
|
||||
pub fn is_continuation(&self) -> bool {
|
||||
match self {
|
||||
&GraphemePart::Continuation => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns Some(String) if GraphemePart is Begin(String), else None.
|
||||
pub fn as_option(&self) -> Option<&String> {
|
||||
match self {
|
||||
&GraphemePart::Begin(ref string) => Some(string),
|
||||
&GraphemePart::Continuation => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns String if GraphemePart is Begin(String), panics otherwise.
|
||||
pub fn unwrap(&self) -> String {
|
||||
match self {
|
||||
&GraphemePart::Begin(ref s) => s.clone(),
|
||||
_ => panic!("unwrapping GraphemePart::Continuation"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a single cell of terminal.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct ObservedCell {
|
||||
/// Absolute position
|
||||
pub pos: Vec2,
|
||||
/// Style
|
||||
pub style: Rc<ObservedStyle>,
|
||||
/// Part of grapheme - either it's beginning or continuation when character is multi-cell long.
|
||||
pub letter: GraphemePart,
|
||||
}
|
||||
|
||||
impl ObservedCell {
|
||||
/// Constructor
|
||||
pub fn new(
|
||||
pos: Vec2,
|
||||
style: Rc<ObservedStyle>,
|
||||
letter: Option<String>,
|
||||
) -> Self {
|
||||
let letter: GraphemePart = match letter {
|
||||
Some(s) => GraphemePart::Begin(s),
|
||||
None => GraphemePart::Continuation,
|
||||
};
|
||||
|
||||
ObservedCell { pos, style, letter }
|
||||
}
|
||||
}
|
||||
|
||||
/// Puppet backend output
|
||||
///
|
||||
/// Represents single frame.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObservedScreen {
|
||||
/// Size
|
||||
size: Vec2,
|
||||
/// Contents. Each cell can be set or empty.
|
||||
contents: Vec<Option<ObservedCell>>,
|
||||
}
|
||||
|
||||
impl ObservedScreen {
|
||||
/// Creates empty ObservedScreen
|
||||
pub fn new(size: Vec2) -> Self {
|
||||
let contents: Vec<Option<ObservedCell>> = vec![None; size.x * size.y];
|
||||
|
||||
ObservedScreen { size, contents }
|
||||
}
|
||||
|
||||
fn flatten_index(&self, index: Vec2) -> usize {
|
||||
assert!(index.x < self.size.x);
|
||||
assert!(index.y < self.size.y);
|
||||
|
||||
index.y * self.size.x + index.x
|
||||
}
|
||||
|
||||
fn unflatten_index(&self, index: usize) -> Vec2 {
|
||||
assert!(index < self.contents.len());
|
||||
|
||||
Vec2::new(index / self.size.x, index % self.size.x)
|
||||
}
|
||||
|
||||
/// Sets all cells to empty cells with given style
|
||||
pub fn clear(&mut self, style: &Rc<ObservedStyle>) {
|
||||
for idx in 0..self.contents.len() {
|
||||
self.contents[idx] = Some(ObservedCell::new(
|
||||
self.unflatten_index(idx),
|
||||
style.clone(),
|
||||
None,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Size
|
||||
pub fn size(&self) -> Vec2 {
|
||||
self.size
|
||||
}
|
||||
|
||||
/// Returns a rectangular subset of observed screen.
|
||||
pub fn piece(&self, min: Vec2, max: Vec2) -> ObservedPiece {
|
||||
ObservedPiece::new(self, min, max)
|
||||
}
|
||||
|
||||
/// Prints the piece to stdout.
|
||||
pub fn print_stdout(&self) {
|
||||
println!("captured piece:");
|
||||
|
||||
print!("x");
|
||||
for x in 0..self.size().x {
|
||||
print!("{}", x % 10);
|
||||
}
|
||||
println!("x");
|
||||
|
||||
for y in 0..self.size().y {
|
||||
print!("{}", y % 10);
|
||||
|
||||
for x in 0..self.size().x {
|
||||
let pos = Vec2::new(x, y);
|
||||
let cell_op: &Option<ObservedCell> = &self[pos];
|
||||
if cell_op.is_some() {
|
||||
let cell = cell_op.as_ref().unwrap();
|
||||
|
||||
if cell.letter.is_continuation() {
|
||||
print!("c");
|
||||
continue;
|
||||
} else {
|
||||
let letter = cell.letter.unwrap();
|
||||
if letter == " " {
|
||||
print!(" ");
|
||||
} else {
|
||||
print!("{}", letter);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print!(".");
|
||||
}
|
||||
}
|
||||
print!("|");
|
||||
println!();
|
||||
}
|
||||
|
||||
print!("x");
|
||||
for _x in 0..self.size().x {
|
||||
print!("-");
|
||||
}
|
||||
println!("x");
|
||||
}
|
||||
|
||||
/// Returns occurences of given string pattern
|
||||
pub fn find_occurences(
|
||||
&self,
|
||||
pattern: &str,
|
||||
) -> Vec<ObservedLine> {
|
||||
// TODO(njskalski): test for two-cell letters.
|
||||
// TODO(njskalski): fails with whitespaces like "\t".
|
||||
|
||||
let mut hits: Vec<ObservedLine> = vec![];
|
||||
for y in self.min().y..self.max().y {
|
||||
'x: for x in self.min().x..self.max().x {
|
||||
// check candidate.
|
||||
|
||||
if pattern.len() > self.size().x - x {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut pattern_cursor: usize = 0;
|
||||
let mut pos_cursor: usize = 0;
|
||||
|
||||
loop {
|
||||
let pattern_symbol = pattern
|
||||
.graphemes(true)
|
||||
.skip(pattern_cursor)
|
||||
.next()
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Found no char at cursor {} in {}",
|
||||
pattern_cursor, &pattern
|
||||
)
|
||||
});
|
||||
|
||||
let pos_it = Vec2::new(x + pos_cursor, y);
|
||||
|
||||
let found_symbol: Option<&String> =
|
||||
if let Some(ref cell) = self[pos_it] {
|
||||
cell.letter.as_option()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match found_symbol {
|
||||
Some(screen_symbol) => {
|
||||
if pattern_symbol == screen_symbol {
|
||||
pattern_cursor += 1;
|
||||
pos_cursor += screen_symbol.width();
|
||||
} else {
|
||||
continue 'x;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if pattern_symbol == " " {
|
||||
pattern_cursor += 1;
|
||||
pos_cursor += 1;
|
||||
} else {
|
||||
continue 'x;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if pattern_cursor == pattern.graphemes(true).count() {
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
if pattern_cursor == pattern.graphemes(true).count() {
|
||||
hits.push(ObservedLine::new(
|
||||
self,
|
||||
Vec2::new(x, y),
|
||||
pos_cursor,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
hits
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents rectangular piece of observed screen (Puppet backend output)
|
||||
pub trait ObservedPieceInterface {
|
||||
|
||||
/// Minimums of coordinates
|
||||
fn min(&self) -> Vec2;
|
||||
/// Maximums of coordinates
|
||||
fn max(&self) -> Vec2;
|
||||
|
||||
/// Reference of ObservablePiece this one is a subsection of or Self
|
||||
fn parent(&self) -> &ObservedScreen;
|
||||
|
||||
/// Size of piece
|
||||
fn size(&self) -> Vec2 {
|
||||
self.max() - self.min()
|
||||
}
|
||||
|
||||
/// Returns a string representation of consecutive lines of this piece.
|
||||
fn as_strings(&self) -> Vec<String> {
|
||||
let mut v: Vec<String> = vec![];
|
||||
for y in self.min().y..self.max().y {
|
||||
let mut s = String::new();
|
||||
for x in self.min().x..self.max().x {
|
||||
match &self.parent()[Vec2::new(x, y)] {
|
||||
None => s.push(' '),
|
||||
Some(cell) => match &cell.letter {
|
||||
GraphemePart::Begin(lex) => s.push_str(&lex),
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
v.push(s);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// Returns expanded sibling of this piece
|
||||
///
|
||||
/// Asserts if request can be satisfied.
|
||||
fn expanded(&self, up_left: Vec2, down_right: Vec2) -> ObservedPiece {
|
||||
assert!(self.min().x >= up_left.x);
|
||||
assert!(self.min().y >= up_left.y);
|
||||
assert!(self.max().x + down_right.x <= self.parent().size.x);
|
||||
assert!(self.max().y + down_right.y <= self.parent().size.y);
|
||||
|
||||
ObservedPiece::new(
|
||||
self.parent(),
|
||||
self.min() - up_left,
|
||||
self.max() + down_right,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a piece or whole of observed screen.
|
||||
pub struct ObservedPiece<'a> {
|
||||
min: Vec2,
|
||||
max: Vec2,
|
||||
parent: &'a ObservedScreen,
|
||||
}
|
||||
|
||||
impl<'a> ObservedPiece<'a> {
|
||||
fn new(parent: &'a ObservedScreen, min: Vec2, max: Vec2) -> Self {
|
||||
ObservedPiece { min, max, parent }
|
||||
}
|
||||
}
|
||||
|
||||
impl ObservedPieceInterface for ObservedScreen {
|
||||
fn min(&self) -> Vec2 {
|
||||
Vec2::new(0, 0)
|
||||
}
|
||||
|
||||
fn max(&self) -> Vec2 {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn parent(&self) -> &ObservedScreen {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ObservedPieceInterface for ObservedPiece<'a> {
|
||||
fn min(&self) -> Vec2 {
|
||||
self.min
|
||||
}
|
||||
|
||||
fn max(&self) -> Vec2 {
|
||||
self.max
|
||||
}
|
||||
|
||||
fn parent(&self) -> &ObservedScreen {
|
||||
self.parent
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a single line of observed screen.
|
||||
pub struct ObservedLine<'a> {
|
||||
line_start: Vec2,
|
||||
line_len: usize,
|
||||
parent: &'a ObservedScreen,
|
||||
}
|
||||
|
||||
impl<'a> ObservedLine<'a> {
|
||||
fn new(
|
||||
parent: &'a ObservedScreen,
|
||||
line_start: Vec2,
|
||||
line_len: usize,
|
||||
) -> Self {
|
||||
ObservedLine {
|
||||
parent,
|
||||
line_start,
|
||||
line_len,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the same line, but expanded.
|
||||
///
|
||||
/// Asserts whether request can be satisfied
|
||||
pub fn expanded_line(&self, left: usize, right: usize) -> Self {
|
||||
assert!(left <= self.line_start.x);
|
||||
assert!(
|
||||
self.line_start.x + self.line_len + right <= self.parent.size.x
|
||||
);
|
||||
|
||||
ObservedLine {
|
||||
parent: self.parent,
|
||||
line_start: Vec2::new(self.line_start.x - left, self.line_start.y),
|
||||
line_len: self.line_len + left + right,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ObservedPieceInterface for ObservedLine<'a> {
|
||||
fn min(&self) -> Vec2 {
|
||||
self.line_start
|
||||
}
|
||||
|
||||
fn max(&self) -> Vec2 {
|
||||
self.line_start + Vec2::new(self.line_len, 1)
|
||||
}
|
||||
|
||||
fn parent(&self) -> &ObservedScreen {
|
||||
self.parent
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToString for ObservedLine<'a> {
|
||||
fn to_string(&self) -> String {
|
||||
self.as_strings().remove(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<Vec2> for ObservedPieceInterface {
|
||||
type Output = Option<ObservedCell>;
|
||||
|
||||
fn index(&self, index: Vec2) -> &Self::Output {
|
||||
assert!(self.max().x - self.min().x > index.x);
|
||||
assert!(self.max().y - self.min().y > index.y);
|
||||
|
||||
let parent_index = self.min() + index;
|
||||
|
||||
&self.parent()[parent_index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<Vec2> for ObservedScreen {
|
||||
type Output = Option<ObservedCell>;
|
||||
|
||||
fn index(&self, index: Vec2) -> &Self::Output {
|
||||
let idx = self.flatten_index(index);
|
||||
&self.contents[idx]
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<Vec2> for ObservedScreen {
|
||||
fn index_mut(&mut self, index: Vec2) -> &mut Option<ObservedCell> {
|
||||
let idx = self.flatten_index(index);
|
||||
&mut self.contents[idx]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::backend::puppet::DEFAULT_OBSERVED_STYLE;
|
||||
|
||||
/// Expecting fake_screen to be square, # will be replaced with blank.
|
||||
fn get_observed_screen(fake_screen: &Vec<&str>) -> ObservedScreen {
|
||||
let observed_style: Rc<ObservedStyle> =
|
||||
Rc::new(DEFAULT_OBSERVED_STYLE.clone());
|
||||
|
||||
let height = fake_screen.len();
|
||||
let width = fake_screen[0].width();
|
||||
let size = Vec2::new(width, height);
|
||||
|
||||
let mut os = ObservedScreen::new(size);
|
||||
|
||||
for y in 0..fake_screen.len() {
|
||||
let mut x: usize = 0;
|
||||
for letter in fake_screen[y].graphemes(true) {
|
||||
let idx = os.flatten_index(Vec2::new(x, y));
|
||||
os.contents[idx] = if letter == "#" {
|
||||
None
|
||||
} else {
|
||||
Some(ObservedCell::new(
|
||||
Vec2::new(x, y),
|
||||
observed_style.clone(),
|
||||
Some(letter.to_owned()),
|
||||
))
|
||||
};
|
||||
|
||||
x += letter.width();
|
||||
}
|
||||
}
|
||||
|
||||
os
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test() {
|
||||
let fake_screen: Vec<&'static str> =
|
||||
vec!["..hello***", "!!##$$$$$*", ".hello^^^^"];
|
||||
|
||||
let os = get_observed_screen(&fake_screen);
|
||||
|
||||
assert_eq!(
|
||||
os[Vec2::new(0, 0)].as_ref().unwrap().letter.as_option(),
|
||||
Some(&".".to_owned())
|
||||
);
|
||||
assert_eq!(os[Vec2::new(2, 1)], None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_occurrences_no_blanks() {
|
||||
let fake_screen: Vec<&'static str> =
|
||||
vec!["..hello***", "!!##$$$$$*", ".hello^^^^"];
|
||||
|
||||
let os = get_observed_screen(&fake_screen);
|
||||
|
||||
let hits = os.find_occurences("hello");
|
||||
|
||||
assert_eq!(hits.len(), 2);
|
||||
assert_eq!(hits[0].size(), Vec2::new(5, 1));
|
||||
assert_eq!(hits[1].size(), Vec2::new(5, 1));
|
||||
|
||||
assert_eq!(hits[0].to_string(), "hello");
|
||||
assert_eq!(hits[1].to_string(), "hello");
|
||||
|
||||
assert_eq!(hits[0].min(), Vec2::new(2, 0));
|
||||
assert_eq!(hits[0].max(), Vec2::new(7, 1));
|
||||
|
||||
assert_eq!(hits[1].min(), Vec2::new(1, 2));
|
||||
assert_eq!(hits[1].max(), Vec2::new(6, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_occurrences_some_blanks() {
|
||||
let fake_screen: Vec<&'static str> =
|
||||
vec!["__hello world_", "hello!world___", "___hello#world"];
|
||||
|
||||
let os = get_observed_screen(&fake_screen);
|
||||
|
||||
let hits = os.find_occurences("hello world");
|
||||
|
||||
assert_eq!(hits.len(), 2);
|
||||
assert_eq!(hits[0].size(), Vec2::new(11, 1));
|
||||
assert_eq!(hits[1].size(), Vec2::new(11, 1));
|
||||
|
||||
assert_eq!(hits[0].to_string(), "hello world");
|
||||
assert_eq!(hits[1].to_string(), "hello world");
|
||||
|
||||
assert_eq!(hits[0].min(), Vec2::new(2, 0));
|
||||
assert_eq!(hits[0].max(), Vec2::new(13, 1));
|
||||
|
||||
assert_eq!(hits[1].min(), Vec2::new(3, 2));
|
||||
assert_eq!(hits[1].max(), Vec2::new(14, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expand_lines() {
|
||||
let fake_screen: Vec<&'static str> = vec!["abc hello#efg"];
|
||||
|
||||
let os = get_observed_screen(&fake_screen);
|
||||
|
||||
let hits = os.find_occurences("hello");
|
||||
|
||||
assert_eq!(hits.len(), 1);
|
||||
let hit = hits.first().unwrap();
|
||||
assert_eq!(hit.size(), Vec2::new(5, 1));
|
||||
let expanded_left = hit.expanded_line(3, 0);
|
||||
assert_eq!(expanded_left.size(), Vec2::new(8, 1));
|
||||
assert_eq!(expanded_left.to_string(), "bc hello");
|
||||
|
||||
let expanded_left = hit.expanded_line(4, 0);
|
||||
assert_eq!(expanded_left.size(), Vec2::new(9, 1));
|
||||
assert_eq!(expanded_left.to_string(), "abc hello");
|
||||
|
||||
let expanded_right = hit.expanded_line(0, 2);
|
||||
assert_eq!(expanded_right.size(), Vec2::new(7, 1));
|
||||
assert_eq!(expanded_right.to_string(), "hello e");
|
||||
|
||||
let expanded_right = hit.expanded_line(0, 4);
|
||||
assert_eq!(expanded_right.size(), Vec2::new(9, 1));
|
||||
assert_eq!(expanded_right.to_string(), "hello efg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expand_lines_weird_symbol_1() {
|
||||
let fake_screen: Vec<&'static str> = vec!["abc ▸ <root>#efg"];
|
||||
|
||||
let os = get_observed_screen(&fake_screen);
|
||||
|
||||
let hits = os.find_occurences("root");
|
||||
|
||||
assert_eq!(hits.len(), 1);
|
||||
let hit = hits.first().unwrap();
|
||||
assert_eq!(hit.size(), Vec2::new(4, 1));
|
||||
let expanded_left = hit.expanded_line(3, 0);
|
||||
assert_eq!(expanded_left.size(), Vec2::new(7, 1));
|
||||
assert_eq!(expanded_left.to_string(), "▸ <root");
|
||||
|
||||
let expanded_left = hit.expanded_line(7, 0);
|
||||
assert_eq!(expanded_left.size(), Vec2::new(11, 1));
|
||||
assert_eq!(expanded_left.to_string(), "abc ▸ <root");
|
||||
|
||||
let expanded_right = hit.expanded_line(0, 5);
|
||||
assert_eq!(expanded_right.size(), Vec2::new(9, 1));
|
||||
assert_eq!(expanded_right.to_string(), "root> efg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expand_lines_weird_symbol_2() {
|
||||
let fake_screen: Vec<&'static str> = vec!["abc ▸ <root>#efg"];
|
||||
|
||||
let os = get_observed_screen(&fake_screen);
|
||||
|
||||
let hits = os.find_occurences("▸");
|
||||
|
||||
assert_eq!(hits.len(), 1);
|
||||
let hit = hits.first().unwrap();
|
||||
assert_eq!(hit.size(), Vec2::new(1, 1));
|
||||
let expanded_left = hit.expanded_line(3, 0);
|
||||
assert_eq!(expanded_left.size(), Vec2::new(4, 1));
|
||||
assert_eq!(expanded_left.to_string(), "bc ▸");
|
||||
|
||||
let expanded_right = hit.expanded_line(0, 9);
|
||||
assert_eq!(expanded_right.size(), Vec2::new(10, 1));
|
||||
assert_eq!(expanded_right.to_string(), "▸ <root> e");
|
||||
}
|
||||
}
|
55
src/backend/puppet/observed_screen_view.rs
Normal file
55
src/backend/puppet/observed_screen_view.rs
Normal file
@ -0,0 +1,55 @@
|
||||
//! View visualizing a captured PuppetBackend outputs
|
||||
use crate::backend::puppet::observed::ObservedCell;
|
||||
use crate::backend::puppet::observed::ObservedScreen;
|
||||
use crate::theme::ColorStyle;
|
||||
use crate::theme::ColorType;
|
||||
use crate::view::View;
|
||||
use crate::Printer;
|
||||
use crate::Vec2;
|
||||
|
||||
/// A view that visualize observed screen
|
||||
pub struct ObservedScreenView {
|
||||
screen: ObservedScreen,
|
||||
}
|
||||
|
||||
impl ObservedScreenView {
|
||||
/// Constructor
|
||||
pub fn new(obs: ObservedScreen) -> Self {
|
||||
ObservedScreenView { screen: obs }
|
||||
}
|
||||
}
|
||||
|
||||
impl View for ObservedScreenView {
|
||||
fn draw(&self, printer: &Printer) {
|
||||
for x in 0..self.screen.size().x {
|
||||
for y in 0..self.screen.size().y {
|
||||
let pos = Vec2::new(x, y);
|
||||
let cell_op: &Option<ObservedCell> = &self.screen[pos];
|
||||
if cell_op.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cell = cell_op.as_ref().unwrap();
|
||||
|
||||
if cell.letter.is_continuation() {
|
||||
continue;
|
||||
}
|
||||
|
||||
printer.with_effects(cell.style.effects, |printer| {
|
||||
let color_style = ColorStyle {
|
||||
front: ColorType::Color(cell.style.colors.front),
|
||||
back: ColorType::Color(cell.style.colors.back),
|
||||
};
|
||||
|
||||
printer.with_color(color_style, |printer| {
|
||||
printer.print(pos, &cell.letter.unwrap());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn required_size(&mut self, _: Vec2) -> Vec2 {
|
||||
self.screen.size()
|
||||
}
|
||||
}
|
23
src/backend/puppet/static_values.rs
Normal file
23
src/backend/puppet/static_values.rs
Normal file
@ -0,0 +1,23 @@
|
||||
/// Some default values to Puppet backend.
|
||||
#[allow(missing_docs)]
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use crate::theme::{Color, Effect};
|
||||
use crate::theme::ColorPair;
|
||||
use crate::vec::Vec2;
|
||||
use crate::XY;
|
||||
use enumset::EnumSet;
|
||||
|
||||
use crate::backend::puppet::observed::*;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref DEFAULT_SIZE: Vec2 = XY::<usize> { x: 120, y: 80 };
|
||||
pub static ref DEFAULT_OBSERVED_STYLE: ObservedStyle = ObservedStyle {
|
||||
colors: ColorPair {
|
||||
front: Color::TerminalDefault,
|
||||
back: Color::TerminalDefault,
|
||||
},
|
||||
effects: EnumSet::<Effect>::empty(),
|
||||
};
|
||||
}
|
@ -89,6 +89,7 @@ pub mod theme;
|
||||
pub mod vec;
|
||||
pub mod views;
|
||||
|
||||
|
||||
// This probably doesn't need to be public?
|
||||
mod cursive;
|
||||
mod printer;
|
||||
|
Loading…
Reference in New Issue
Block a user