1
1
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:
Wez Furlong 2018-07-12 07:26:37 -07:00
commit e547f8c504
8 changed files with 660 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
Cargo.lock
.*.sw*
/target
**/*.rs.bk

3
.rustfmt.toml Normal file
View File

@ -0,0 +1,3 @@
# Please keep these in alphabetical order.
tab_spaces = 4
wrap_comments = true

10
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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(),
]]
);
}
}