diff --git a/examples/select_test.rs b/examples/select_test.rs new file mode 100644 index 0000000..9084fe6 --- /dev/null +++ b/examples/select_test.rs @@ -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, + input: crossbeam_channel::Sender>, + last_screen: RefCell>, + } + + 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 { + 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); + } +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 74a0262..9583124 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -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. /// diff --git a/src/backend/puppet/mod.rs b/src/backend/puppet/mod.rs new file mode 100644 index 0000000..207784a --- /dev/null +++ b/src/backend/puppet/mod.rs @@ -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>, + inner_receiver: Receiver>, + prev_frame: RefCell>, + current_frame: RefCell, + size: RefCell, + current_style: RefCell>, + screen_channel: (Sender, Receiver), +} + +impl Backend { + /// Creates new Puppet backend of given or default size. + pub fn init(size_op: Option) -> Box + 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 { + self.current_style.borrow().clone() + } + + /// Ouput stream of consecutive frames rendered by Puppet backend + pub fn stream(&self) -> Receiver { + self.screen_channel.1.clone() + } + + /// Input stream to inject artificial input to Puppet backend. + pub fn input(&self) -> Sender> { + self.inner_sender.clone() + } +} + +impl backend::Backend for Backend { + + fn poll_event(&mut self) -> Option { + 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)); + } +} diff --git a/src/backend/puppet/observed.rs b/src/backend/puppet/observed.rs new file mode 100644 index 0000000..f8852fc --- /dev/null +++ b/src/backend/puppet/observed.rs @@ -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, +} + +/// 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, + /// 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, + letter: Option, + ) -> 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>, +} + +impl ObservedScreen { + /// Creates empty ObservedScreen + pub fn new(size: Vec2) -> Self { + let contents: Vec> = 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) { + 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 = &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 { + // TODO(njskalski): test for two-cell letters. + // TODO(njskalski): fails with whitespaces like "\t". + + let mut hits: Vec = 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 { + let mut v: Vec = 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 for ObservedPieceInterface { + type Output = Option; + + 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 for ObservedScreen { + type Output = Option; + + fn index(&self, index: Vec2) -> &Self::Output { + let idx = self.flatten_index(index); + &self.contents[idx] + } +} + +impl IndexMut for ObservedScreen { + fn index_mut(&mut self, index: Vec2) -> &mut Option { + 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 = + 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 ▸ #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(), "▸ efg"); + } + + #[test] + fn test_expand_lines_weird_symbol_2() { + let fake_screen: Vec<&'static str> = vec!["abc ▸ #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(), "▸ e"); + } +} diff --git a/src/backend/puppet/observed_screen_view.rs b/src/backend/puppet/observed_screen_view.rs new file mode 100644 index 0000000..92ca685 --- /dev/null +++ b/src/backend/puppet/observed_screen_view.rs @@ -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 = &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() + } +} diff --git a/src/backend/puppet/static_values.rs b/src/backend/puppet/static_values.rs new file mode 100644 index 0000000..3bbea17 --- /dev/null +++ b/src/backend/puppet/static_values.rs @@ -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:: { x: 120, y: 80 }; + pub static ref DEFAULT_OBSERVED_STYLE: ObservedStyle = ObservedStyle { + colors: ColorPair { + front: Color::TerminalDefault, + back: Color::TerminalDefault, + }, + effects: EnumSet::::empty(), + }; +} diff --git a/src/lib.rs b/src/lib.rs index 700ab1e..7496d30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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;