mirror of
https://github.com/wez/wezterm.git
synced 2024-11-10 15:04:32 +03:00
start building out the terminal model
This is influenced by my code in wezterm
This commit is contained in:
commit
e547f8c504
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
Cargo.lock
|
||||
.*.sw*
|
||||
/target
|
||||
**/*.rs.bk
|
3
.rustfmt.toml
Normal file
3
.rustfmt.toml
Normal file
@ -0,0 +1,3 @@
|
||||
# Please keep these in alphabetical order.
|
||||
tab_spaces = 4
|
||||
wrap_comments = true
|
10
Cargo.toml
Normal file
10
Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "termwiz"
|
||||
version = "0.1.0"
|
||||
authors = ["Wez Furlong"]
|
||||
|
||||
[dependencies]
|
||||
terminfo = "~0.5"
|
||||
palette = "~0.4"
|
||||
serde = "~1.0"
|
||||
serde_derive = "~1.0"
|
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Wez Furlong
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
178
src/cell.rs
Normal file
178
src/cell.rs
Normal file
@ -0,0 +1,178 @@
|
||||
use color;
|
||||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Hyperlink {
|
||||
pub id: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct CellAttributes {
|
||||
attributes: u16,
|
||||
pub foreground: color::ColorAttribute,
|
||||
pub background: color::ColorAttribute,
|
||||
pub hyperlink: Option<Rc<Hyperlink>>,
|
||||
}
|
||||
|
||||
/// Define getter and setter for the attributes bitfield.
|
||||
/// The first form is for a simple boolean value stored in
|
||||
/// a single bit. The $bitnum parameter specifies which bit.
|
||||
/// The second form is for an integer value that occupies a range
|
||||
/// of bits. The $bitmask and $bitshift parameters define how
|
||||
/// to transform from the stored bit value to the consumable
|
||||
/// value.
|
||||
macro_rules! bitfield {
|
||||
($getter:ident, $setter:ident, $bitnum:expr) => {
|
||||
#[inline]
|
||||
pub fn $getter(&self) -> bool {
|
||||
(self.attributes & (1 << $bitnum)) == (1 << $bitnum)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn $setter(&mut self, value: bool) {
|
||||
let attr_value = if value { 1 << $bitnum } else { 0 };
|
||||
self.attributes = (self.attributes & !(1 << $bitnum)) | attr_value;
|
||||
}
|
||||
};
|
||||
|
||||
($getter:ident, $setter:ident, $bitmask:expr, $bitshift:expr) => {
|
||||
#[inline]
|
||||
pub fn $getter(&self) -> u16 {
|
||||
(self.attributes >> $bitshift) & $bitmask
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn $setter(&mut self, value: u16) {
|
||||
let clear = !($bitmask << $bitshift);
|
||||
let attr_value = (value & $bitmask) << $bitshift;
|
||||
self.attributes = (self.attributes & clear) | attr_value;
|
||||
}
|
||||
};
|
||||
|
||||
($getter:ident, $setter:ident, $enum:ident, $bitmask:expr, $bitshift:expr) => {
|
||||
#[inline]
|
||||
pub fn $getter(&self) -> $enum {
|
||||
unsafe { mem::transmute(((self.attributes >> $bitshift) & $bitmask) as u16)}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn $setter(&mut self, value: $enum) {
|
||||
let value = value as u16;
|
||||
let clear = !($bitmask << $bitshift);
|
||||
let attr_value = (value & $bitmask) << $bitshift;
|
||||
self.attributes = (self.attributes & clear) | attr_value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
|
||||
#[repr(u16)]
|
||||
pub enum Intensity {
|
||||
Normal = 0,
|
||||
Bold = 1,
|
||||
Half = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
|
||||
#[repr(u16)]
|
||||
pub enum Underline {
|
||||
None = 0,
|
||||
Single = 1,
|
||||
Double = 2,
|
||||
}
|
||||
|
||||
impl Default for CellAttributes {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
attributes: 0,
|
||||
foreground: color::ColorAttribute {
|
||||
ansi: color::ColorSpec::Foreground,
|
||||
full: None,
|
||||
},
|
||||
background: color::ColorAttribute {
|
||||
ansi: color::ColorSpec::Background,
|
||||
full: None,
|
||||
},
|
||||
hyperlink: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CellAttributes {
|
||||
bitfield!(intensity, set_intensity, Intensity, 0b11, 0);
|
||||
bitfield!(underline, set_underline, Underline, 0b11, 2);
|
||||
bitfield!(italic, set_italic, 4);
|
||||
bitfield!(blink, set_blink, 5);
|
||||
bitfield!(reverse, set_reverse, 6);
|
||||
bitfield!(strikethrough, set_strikethrough, 7);
|
||||
bitfield!(invisible, set_invisible, 8);
|
||||
|
||||
/// Clone the attributes, but exclude fancy extras such
|
||||
/// as hyperlinks or future sprite things
|
||||
pub fn clone_sgr_only(&self) -> Self {
|
||||
Self {
|
||||
attributes: self.attributes,
|
||||
foreground: self.foreground,
|
||||
background: self.background,
|
||||
hyperlink: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Models the contents of a cell on the terminal display
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Cell {
|
||||
text: char,
|
||||
attrs: CellAttributes,
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
fn default() -> Self {
|
||||
Cell::new(' ', CellAttributes::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
/// De-fang the input character such that it has no special meaning
|
||||
/// to a terminal. All control and movement characters are rewritten
|
||||
/// as a space.
|
||||
pub fn nerf_control_char(text: char) -> char {
|
||||
if text < 0x20 as char || text == 0x7f as char {
|
||||
' '
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(text: char, attrs: CellAttributes) -> Self {
|
||||
Self {
|
||||
text: Self::nerf_control_char(text),
|
||||
attrs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn char(&self) -> char {
|
||||
self.text
|
||||
}
|
||||
|
||||
pub fn attrs(&self) -> &CellAttributes {
|
||||
&self.attrs
|
||||
}
|
||||
}
|
||||
|
||||
/// Models a change in the attributes of a cell in a stream of changes
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum AttributeChange {
|
||||
Intensity(Intensity),
|
||||
Underline(Underline),
|
||||
Italic(bool),
|
||||
Blink(bool),
|
||||
Reverse(bool),
|
||||
StrikeThrough(bool),
|
||||
Invisible(bool),
|
||||
Foreground(color::ColorAttribute),
|
||||
Background(color::ColorAttribute),
|
||||
Hyperlink(Option<Rc<Hyperlink>>),
|
||||
}
|
122
src/color.rs
Normal file
122
src/color.rs
Normal file
@ -0,0 +1,122 @@
|
||||
//! Colors for attributes
|
||||
|
||||
use palette;
|
||||
use palette::Srgb;
|
||||
use serde::{self, Deserialize, Deserializer};
|
||||
use std::result::Result;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[repr(u8)]
|
||||
/// These correspond to the classic ANSI color indices and are
|
||||
/// used for convenience/readability in code
|
||||
pub enum AnsiColor {
|
||||
Black = 0,
|
||||
Maroon,
|
||||
Green,
|
||||
Olive,
|
||||
Navy,
|
||||
Purple,
|
||||
Teal,
|
||||
Silver,
|
||||
Grey,
|
||||
Red,
|
||||
Lime,
|
||||
Yellow,
|
||||
Blue,
|
||||
Fuschia,
|
||||
Aqua,
|
||||
White,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash)]
|
||||
pub struct RgbColor {
|
||||
pub red: u8,
|
||||
pub green: u8,
|
||||
pub blue: u8,
|
||||
}
|
||||
|
||||
impl RgbColor {
|
||||
/// Construct a color from discrete red, green, blue values
|
||||
/// in the range 0-255.
|
||||
pub fn new(red: u8, green: u8, blue: u8) -> Self {
|
||||
Self { red, green, blue }
|
||||
}
|
||||
|
||||
/// Construct a color from an SVG/CSS3 color name.
|
||||
/// Returns None if the supplied name is not recognized.
|
||||
/// The list of names can be found here:
|
||||
/// https://ogeon.github.io/docs/palette/master/palette/named/index.html
|
||||
pub fn from_named(name: &str) -> Option<RgbColor> {
|
||||
palette::named::from_str(&name.to_ascii_lowercase()).map(|color| {
|
||||
let color = Srgb::<u8>::from_format(color);
|
||||
Self::new(color.red, color.green, color.blue)
|
||||
})
|
||||
}
|
||||
|
||||
/// Construct a color from a string of the form `#RRGGBB` where
|
||||
/// R, G and B are all hex digits.
|
||||
pub fn from_rgb_str(s: &str) -> Option<RgbColor> {
|
||||
if s.as_bytes()[0] == b'#' && s.len() == 7 {
|
||||
let mut chars = s.chars().skip(1);
|
||||
|
||||
macro_rules! digit {
|
||||
() => {{
|
||||
let hi = match chars.next().unwrap().to_digit(16) {
|
||||
Some(v) => (v as u8) << 4,
|
||||
None => return None,
|
||||
};
|
||||
let lo = match chars.next().unwrap().to_digit(16) {
|
||||
Some(v) => v as u8,
|
||||
None => return None,
|
||||
};
|
||||
hi | lo
|
||||
}};
|
||||
}
|
||||
Some(Self::new(digit!(), digit!(), digit!()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for RgbColor {
|
||||
fn deserialize<D>(deserializer: D) -> Result<RgbColor, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
RgbColor::from_rgb_str(&s)
|
||||
.or_else(|| RgbColor::from_named(&s))
|
||||
.ok_or_else(|| format!("unknown color name: {}", s))
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum ColorSpec {
|
||||
Foreground,
|
||||
Background,
|
||||
/// Use either a raw number, or use values from the `AnsiColor` enum
|
||||
PaletteIndex(u8),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct ColorAttribute {
|
||||
/// Used if the terminal supports full color
|
||||
pub full: Option<RgbColor>,
|
||||
/// If the terminal doesn't support full color, or the full color
|
||||
/// spec is_none, use old school ansi color number.
|
||||
pub ansi: ColorSpec,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn named_rgb() {
|
||||
let dark_green = RgbColor::from_named("DarkGreen").unwrap();
|
||||
assert_eq!(dark_green.red, 0);
|
||||
assert_eq!(dark_green.green, 0x64);
|
||||
assert_eq!(dark_green.blue, 0);
|
||||
}
|
||||
}
|
9
src/lib.rs
Normal file
9
src/lib.rs
Normal file
@ -0,0 +1,9 @@
|
||||
extern crate palette;
|
||||
extern crate serde;
|
||||
extern crate terminfo;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
pub mod cell;
|
||||
pub mod color;
|
||||
pub mod screen;
|
313
src/screen.rs
Normal file
313
src/screen.rs
Normal file
@ -0,0 +1,313 @@
|
||||
use cell::{AttributeChange, Cell, CellAttributes};
|
||||
use std::cmp::min;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum Position {
|
||||
NoChange,
|
||||
/// Negative values move up, positive values down
|
||||
Relative(isize),
|
||||
Absolute(usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum Change {
|
||||
Attribute(AttributeChange),
|
||||
AllAttributes(CellAttributes),
|
||||
Text(String),
|
||||
// ClearScreen,
|
||||
// ClearToStartOfLine,
|
||||
// ClearToEndOfLine,
|
||||
// ClearToEndOfScreen,
|
||||
CursorPosition { x: Position, y: Position },
|
||||
/* CursorVisibility(bool),
|
||||
* ChangeScrollRegion{top: usize, bottom: usize}, */
|
||||
}
|
||||
|
||||
impl<S: Into<String>> From<S> for Change {
|
||||
fn from(s: S) -> Self {
|
||||
Change::Text(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AttributeChange> for Change {
|
||||
fn from(c: AttributeChange) -> Self {
|
||||
Change::Attribute(c)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Line {
|
||||
cells: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl Line {
|
||||
fn with_width(width: usize) -> Self {
|
||||
let mut cells = Vec::with_capacity(width);
|
||||
cells.resize(width, Cell::default());
|
||||
Self { cells }
|
||||
}
|
||||
|
||||
fn resize(&mut self, width: usize) {
|
||||
self.cells.resize(width, Cell::default());
|
||||
}
|
||||
}
|
||||
|
||||
pub type SequenceNo = usize;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Screen {
|
||||
width: usize,
|
||||
height: usize,
|
||||
lines: Vec<Line>,
|
||||
attributes: CellAttributes,
|
||||
xpos: usize,
|
||||
ypos: usize,
|
||||
seqno: SequenceNo,
|
||||
changes: Vec<Change>,
|
||||
}
|
||||
|
||||
impl Screen {
|
||||
pub fn new(width: usize, height: usize) -> Self {
|
||||
let mut scr = Screen {
|
||||
width,
|
||||
height,
|
||||
..Default::default()
|
||||
};
|
||||
scr.resize(width, height);
|
||||
scr
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: usize, height: usize) {
|
||||
self.lines.resize(height, Line::with_width(width));
|
||||
for line in &mut self.lines {
|
||||
line.resize(width);
|
||||
}
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
|
||||
// FIXME: cursor position is now undefined
|
||||
}
|
||||
|
||||
pub fn add_change<C: Into<Change>>(&mut self, change: C) -> SequenceNo {
|
||||
let seq = self.seqno;
|
||||
self.seqno += 1;
|
||||
let change = change.into();
|
||||
self.apply_change(&change);
|
||||
self.changes.push(change);
|
||||
seq
|
||||
}
|
||||
|
||||
fn apply_change(&mut self, change: &Change) {
|
||||
match change {
|
||||
Change::AllAttributes(attr) => self.attributes = attr.clone(),
|
||||
Change::Text(text) => self.print_text(text),
|
||||
Change::Attribute(change) => self.change_attribute(change),
|
||||
Change::CursorPosition { x, y } => self.set_cursor_pos(x, y),
|
||||
}
|
||||
}
|
||||
|
||||
fn scroll_screen_up(&mut self) {
|
||||
self.lines.remove(0);
|
||||
self.lines.push(Line::with_width(self.width));
|
||||
}
|
||||
|
||||
fn print_text(&mut self, text: &str) {
|
||||
for c in text.chars() {
|
||||
if self.xpos >= self.width {
|
||||
let new_y = self.ypos + 1;
|
||||
if new_y >= self.height {
|
||||
self.scroll_screen_up();
|
||||
} else {
|
||||
self.ypos = new_y;
|
||||
}
|
||||
self.xpos = 0;
|
||||
}
|
||||
|
||||
self.lines[self.ypos].cells[self.xpos] = Cell::new(c, self.attributes.clone());
|
||||
|
||||
// Increment the position now; we'll defer processing
|
||||
// wrapping until the next printed character, otherwise
|
||||
// we'll eagerly scroll when we reach the right margin.
|
||||
self.xpos += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn change_attribute(&mut self, change: &AttributeChange) {
|
||||
use cell::AttributeChange::*;
|
||||
match change {
|
||||
Intensity(value) => self.attributes.set_intensity(*value),
|
||||
Underline(value) => self.attributes.set_underline(*value),
|
||||
Italic(value) => self.attributes.set_italic(*value),
|
||||
Blink(value) => self.attributes.set_blink(*value),
|
||||
Reverse(value) => self.attributes.set_reverse(*value),
|
||||
StrikeThrough(value) => self.attributes.set_strikethrough(*value),
|
||||
Invisible(value) => self.attributes.set_invisible(*value),
|
||||
Foreground(value) => self.attributes.foreground = *value,
|
||||
Background(value) => self.attributes.background = *value,
|
||||
Hyperlink(value) => self.attributes.hyperlink = value.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_cursor_pos(&mut self, x: &Position, y: &Position) {
|
||||
self.xpos = compute_position_change(self.xpos, x, self.width);
|
||||
self.ypos = compute_position_change(self.ypos, y, self.height);
|
||||
}
|
||||
|
||||
/// Returns the entire contents of the screen as a string.
|
||||
/// Only the character data is returned. The end of each line is
|
||||
/// returned as a \n character.
|
||||
/// This function exists primarily for testing purposes.
|
||||
pub fn screen_chars_to_string(&self) -> String {
|
||||
let mut s = String::new();
|
||||
|
||||
for line in &self.lines {
|
||||
for cell in &line.cells {
|
||||
s.push(cell.char());
|
||||
}
|
||||
s.push('\n');
|
||||
}
|
||||
|
||||
s
|
||||
}
|
||||
|
||||
/// Returns the cell data for the screen.
|
||||
/// This is intended to be used for testing purposes.
|
||||
pub fn screen_cells(&self) -> Vec<&[Cell]> {
|
||||
let mut lines = Vec::new();
|
||||
for line in &self.lines {
|
||||
lines.push(line.cells.as_slice());
|
||||
}
|
||||
lines
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a Position update to either the x or y position.
|
||||
/// The value is clamped to be in the range: 0..limit
|
||||
fn compute_position_change(current: usize, pos: &Position, limit: usize) -> usize {
|
||||
use self::Position::*;
|
||||
match pos {
|
||||
NoChange => current,
|
||||
Relative(delta) => {
|
||||
if *delta > 0 {
|
||||
min(current.saturating_add(*delta as usize), limit - 1)
|
||||
} else {
|
||||
current.saturating_sub((*delta).abs() as usize)
|
||||
}
|
||||
}
|
||||
Absolute(abs) => min(*abs, limit - 1),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
// The \x20's look a little awkward, but we can't use a plain
|
||||
// space in the first chararcter of a multi-line continuation;
|
||||
// it gets eaten up and ignored.
|
||||
|
||||
#[test]
|
||||
fn test_basic_print() {
|
||||
let mut s = Screen::new(4, 3);
|
||||
assert_eq!(
|
||||
s.screen_chars_to_string(),
|
||||
"\x20\x20\x20\x20\n\
|
||||
\x20\x20\x20\x20\n\
|
||||
\x20\x20\x20\x20\n"
|
||||
);
|
||||
|
||||
s.add_change("w00t");
|
||||
assert_eq!(
|
||||
s.screen_chars_to_string(),
|
||||
"w00t\n\
|
||||
\x20\x20\x20\x20\n\
|
||||
\x20\x20\x20\x20\n"
|
||||
);
|
||||
|
||||
s.add_change("foo");
|
||||
assert_eq!(
|
||||
s.screen_chars_to_string(),
|
||||
"w00t\n\
|
||||
foo\x20\n\
|
||||
\x20\x20\x20\x20\n"
|
||||
);
|
||||
|
||||
s.add_change("baar");
|
||||
assert_eq!(
|
||||
s.screen_chars_to_string(),
|
||||
"w00t\n\
|
||||
foob\n\
|
||||
aar\x20\n"
|
||||
);
|
||||
|
||||
s.add_change("baz");
|
||||
assert_eq!(
|
||||
s.screen_chars_to_string(),
|
||||
"foob\n\
|
||||
aarb\n\
|
||||
az\x20\x20\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cursor_movement() {
|
||||
let mut s = Screen::new(4, 3);
|
||||
s.add_change(Change::CursorPosition {
|
||||
x: Position::Absolute(3),
|
||||
y: Position::Absolute(2),
|
||||
});
|
||||
s.add_change("X");
|
||||
assert_eq!(
|
||||
s.screen_chars_to_string(),
|
||||
"\x20\x20\x20\x20\n\
|
||||
\x20\x20\x20\x20\n\
|
||||
\x20\x20\x20X\n"
|
||||
);
|
||||
|
||||
s.add_change(Change::CursorPosition {
|
||||
x: Position::Relative(-2),
|
||||
y: Position::Relative(-1),
|
||||
});
|
||||
s.add_change("-");
|
||||
assert_eq!(
|
||||
s.screen_chars_to_string(),
|
||||
"\x20\x20\x20\x20\n\
|
||||
\x20\x20-\x20\n\
|
||||
\x20\x20\x20X\n"
|
||||
);
|
||||
|
||||
s.add_change(Change::CursorPosition {
|
||||
x: Position::Relative(1),
|
||||
y: Position::Relative(-1),
|
||||
});
|
||||
s.add_change("-");
|
||||
assert_eq!(
|
||||
s.screen_chars_to_string(),
|
||||
"\x20\x20\x20-\n\
|
||||
\x20\x20-\x20\n\
|
||||
\x20\x20\x20X\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attribute_setting() {
|
||||
use cell::Intensity;
|
||||
|
||||
let mut s = Screen::new(3, 1);
|
||||
s.add_change("n");
|
||||
s.add_change(AttributeChange::Intensity(Intensity::Bold));
|
||||
s.add_change("b");
|
||||
|
||||
let mut bold = CellAttributes::default();
|
||||
bold.set_intensity(Intensity::Bold);
|
||||
|
||||
assert_eq!(
|
||||
s.screen_cells(),
|
||||
[[
|
||||
Cell::new('n', CellAttributes::default()),
|
||||
Cell::new('b', bold),
|
||||
Cell::default(),
|
||||
]]
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user