Add support for recording/running ref tests

Ref tests use a recording of the terminal protocol and a serialization
of the grid state to check that the parsing and action handling systems
produce the correct result. Ref tests may be recorded by running
alacritty with `--ref-test` and closing the terminal by using the window
"X" button. At that point, the recording is fully written to disk, and a
serialization of important state is recorded. Those files should be
moved to an appropriate folder in the `tests/ref/` tree, and the
`ref_test!` macro invocation should be updated accordingly.

A couple of changes were necessary to make this work:

* Ref tests shouldn't create a pty; the pty was refactored out of the
  `Term` type.
* Repeatable lines/cols were needed; on startup, the terminal is resized
* by default to 80x24 though that may be changed by passing
  `--dimensions w h`.
* Calculating window size based on desired rows/columns and font metrics
  required making load_font callable multiple times.
* Refactor types into library crate so they may be imported in an
  integration test.
* A whole bunch of types needed symmetric serialization and
  deserialization. Mostly this was just adding derives, but the custom
  deserialization of Rgb had to change to a deserialize_with function.

This initially adds one ref test as a sanity check, and more will be
added in subsequent commits. This initial ref tests just starts the
terminal and runs `ll`.
This commit is contained in:
Joe Wilm 2016-11-19 16:16:20 -08:00
parent d97996e19d
commit 66dbd29cd1
21 changed files with 448 additions and 163 deletions

1
Cargo.lock generated
View File

@ -15,6 +15,7 @@ dependencies = [
"parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 0.8.17 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.8.17 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 0.8.17 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 0.8.17 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_yaml 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde_yaml 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"vte 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "vte 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
] ]

View File

@ -5,6 +5,11 @@ authors = ["Joe Wilm <joe@jwilm.com>"]
license = "Apache-2.0" license = "Apache-2.0"
build = "build.rs" build = "build.rs"
[[bin]]
doc = false
path = "src/main.rs"
name = "alacritty"
[dependencies] [dependencies]
libc = "*" libc = "*"
cgmath = "0.7" cgmath = "0.7"
@ -18,6 +23,7 @@ serde_yaml = "0.5"
serde_derive = "0.8" serde_derive = "0.8"
vte = "0.1.2" vte = "0.1.2"
mio = "0.6" mio = "0.6"
serde_json = "*"
copypasta = { path = "./copypasta" } copypasta = { path = "./copypasta" }
[features] [features]
@ -32,5 +38,6 @@ gl_generator = "0.5"
git = "https://github.com/jwilm/glutin" git = "https://github.com/jwilm/glutin"
rev = "78838c1e1497dc8a1b1c8f69da7a6f3cd7da15c1" rev = "78838c1e1497dc8a1b1c8f69da7a6f3cd7da15c1"
[profile.release] [profile.release]
debug = true debug = true

View File

@ -75,6 +75,7 @@ pub struct Descriptor {
/// Given a fontdesc, can rasterize fonts. /// Given a fontdesc, can rasterize fonts.
pub struct Rasterizer { pub struct Rasterizer {
fonts: HashMap<FontKey, Font>, fonts: HashMap<FontKey, Font>,
keys: HashMap<(FontDesc, Size), FontKey>,
device_pixel_ratio: f32, device_pixel_ratio: f32,
} }
@ -83,6 +84,7 @@ impl Rasterizer {
println!("device_pixel_ratio: {}", device_pixel_ratio); println!("device_pixel_ratio: {}", device_pixel_ratio);
Rasterizer { Rasterizer {
fonts: HashMap::new(), fonts: HashMap::new(),
keys: HashMap::new(),
device_pixel_ratio: device_pixel_ratio, device_pixel_ratio: device_pixel_ratio,
} }
} }
@ -100,12 +102,19 @@ impl Rasterizer {
} }
pub fn load_font(&mut self, desc: &FontDesc, size: Size) -> Option<FontKey> { pub fn load_font(&mut self, desc: &FontDesc, size: Size) -> Option<FontKey> {
self.get_font(desc, size) self.keys
.map(|font| { .get(&(desc.to_owned(), size))
let key = FontKey::next(); .map(|k| *k)
self.fonts.insert(key, font); .or_else(|| {
self.get_font(desc, size)
.map(|font| {
let key = FontKey::next();
key self.fonts.insert(key, font);
self.keys.insert((desc.clone(), size), key);
key
})
}) })
} }

View File

@ -29,6 +29,7 @@ pub struct Rasterizer {
faces: HashMap<FontKey, Face<'static>>, faces: HashMap<FontKey, Face<'static>>,
library: Library, library: Library,
system_fonts: HashMap<String, Family>, system_fonts: HashMap<String, Family>,
keys: HashMap<FontDesc, FontKey>,
dpi_x: u32, dpi_x: u32,
dpi_y: u32, dpi_y: u32,
dpr: f32, dpr: f32,
@ -51,6 +52,7 @@ impl Rasterizer {
Rasterizer { Rasterizer {
system_fonts: get_font_families(), system_fonts: get_font_families(),
faces: HashMap::new(), faces: HashMap::new(),
keys: HashMap::new(),
library: library, library: library,
dpi_x: dpi_x as u32, dpi_x: dpi_x as u32,
dpi_y: dpi_y as u32, dpi_y: dpi_y as u32,
@ -77,11 +79,16 @@ impl Rasterizer {
} }
pub fn load_font(&mut self, desc: &FontDesc, _size: Size) -> Option<FontKey> { pub fn load_font(&mut self, desc: &FontDesc, _size: Size) -> Option<FontKey> {
self.get_face(desc) self.keys
.map(|face| { .get(&desc.to_owned())
let key = FontKey::next(); .map(|k| *k)
self.faces.insert(key, face); .or_else(|| {
key self.get_face(desc)
.map(|face| {
let key = FontKey::next();
self.faces.insert(key, face);
key
})
}) })
} }

View File

@ -303,7 +303,7 @@ pub enum TabulationClearMode {
/// ///
/// The order here matters since the enum should be castable to a `usize` for /// The order here matters since the enum should be castable to a `usize` for
/// indexing a color list. /// indexing a color list.
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] #[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Color { pub enum Color {
/// Black /// Black
Black = 0, Black = 0,

View File

@ -463,7 +463,9 @@ pub struct Colors {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct PrimaryColors { pub struct PrimaryColors {
#[serde(deserialize_with = "rgb_from_hex")]
background: Rgb, background: Rgb,
#[serde(deserialize_with = "rgb_from_hex")]
foreground: Rgb, foreground: Rgb,
} }
@ -501,45 +503,45 @@ impl Default for Colors {
/// The normal or bright colors section of config /// The normal or bright colors section of config
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct AnsiColors { pub struct AnsiColors {
#[serde(deserialize_with = "rgb_from_hex")]
black: Rgb, black: Rgb,
#[serde(deserialize_with = "rgb_from_hex")]
red: Rgb, red: Rgb,
#[serde(deserialize_with = "rgb_from_hex")]
green: Rgb, green: Rgb,
#[serde(deserialize_with = "rgb_from_hex")]
yellow: Rgb, yellow: Rgb,
#[serde(deserialize_with = "rgb_from_hex")]
blue: Rgb, blue: Rgb,
#[serde(deserialize_with = "rgb_from_hex")]
magenta: Rgb, magenta: Rgb,
#[serde(deserialize_with = "rgb_from_hex")]
cyan: Rgb, cyan: Rgb,
#[serde(deserialize_with = "rgb_from_hex")]
white: Rgb, white: Rgb,
} }
impl serde::de::Deserialize for Rgb { /// Deserialize an Rgb from a hex string
fn deserialize<D>(deserializer: &mut D) -> ::std::result::Result<Self, D::Error> ///
where D: serde::de::Deserializer /// This is *not* the deserialize impl for Rgb since we want a symmetric
{ /// serialize/deserialize impl for ref tests.
use std::marker::PhantomData; fn rgb_from_hex<D>(deserializer: &mut D) -> ::std::result::Result<Rgb, D::Error>
where D: de::Deserializer
{
struct RgbVisitor;
struct StringVisitor<__D> { impl ::serde::de::Visitor for RgbVisitor {
_marker: PhantomData<__D>, type Value = Rgb;
}
impl<__D> ::serde::de::Visitor for StringVisitor<__D> fn visit_str<E>(&mut self, value: &str) -> ::std::result::Result<Rgb, E>
where __D: ::serde::de::Deserializer where E: ::serde::de::Error
{ {
type Value = String; Rgb::from_str(&value[..])
.map_err(|_| E::custom("failed to parse rgb; expect 0xrrggbb"))
fn visit_str<E>(&mut self, value: &str) -> ::std::result::Result<Self::Value, E>
where E: ::serde::de::Error
{
Ok(value.to_owned())
}
} }
deserializer
.deserialize_f64(StringVisitor::<D>{ _marker: PhantomData })
.and_then(|v| {
Rgb::from_str(&v[..])
.map_err(|_| D::Error::custom("failed to parse rgb; expect 0xrrggbb"))
})
} }
deserializer.deserialize_str(RgbVisitor)
} }
impl Rgb { impl Rgb {

View File

@ -1,12 +1,14 @@
//! Process window events //! Process window events
use std::fs::File;
use std::io::Write;
use std::sync::{Arc, mpsc}; use std::sync::{Arc, mpsc};
use serde_json as json;
use glutin; use glutin;
use input; use input;
use sync::FairMutex; use sync::FairMutex;
use term::Term; use term::Term;
use util::encode_char;
use config::Config; use config::Config;
/// The event processor /// The event processor
@ -15,6 +17,7 @@ pub struct Processor<N> {
input_processor: input::Processor, input_processor: input::Processor,
terminal: Arc<FairMutex<Term>>, terminal: Arc<FairMutex<Term>>,
resize_tx: mpsc::Sender<(u32, u32)>, resize_tx: mpsc::Sender<(u32, u32)>,
ref_test: bool,
} }
impl<N: input::Notify> Processor<N> { impl<N: input::Notify> Processor<N> {
@ -27,18 +30,43 @@ impl<N: input::Notify> Processor<N> {
terminal: Arc<FairMutex<Term>>, terminal: Arc<FairMutex<Term>>,
resize_tx: mpsc::Sender<(u32, u32)>, resize_tx: mpsc::Sender<(u32, u32)>,
config: &Config, config: &Config,
ref_test: bool,
) -> Processor<N> { ) -> Processor<N> {
Processor { Processor {
notifier: notifier, notifier: notifier,
terminal: terminal, terminal: terminal,
input_processor: input::Processor::new(config), input_processor: input::Processor::new(config),
resize_tx: resize_tx, resize_tx: resize_tx,
ref_test: ref_test,
} }
} }
fn handle_event(&mut self, event: glutin::Event) { fn handle_event(&mut self, event: glutin::Event) {
match event { match event {
glutin::Event::Closed => panic!("window closed"), // TODO ... glutin::Event::Closed => {
if self.ref_test {
// dump grid state
let terminal = self.terminal.lock();
let grid = terminal.grid();
let serialized_grid = json::to_string(&grid)
.expect("serialize grid");
let serialized_size = json::to_string(terminal.size_info())
.expect("serialize size");
File::create("./grid.json")
.and_then(|mut f| f.write_all(serialized_grid.as_bytes()))
.expect("write grid.json");
File::create("./size.json")
.and_then(|mut f| f.write_all(serialized_size.as_bytes()))
.expect("write size.json");
}
// FIXME
panic!("window closed");
},
glutin::Event::Resized(w, h) => { glutin::Event::Resized(w, h) => {
self.resize_tx.send((w, h)).expect("send new size"); self.resize_tx.send((w, h)).expect("send new size");
// Acquire term lock // Acquire term lock

View File

@ -1,7 +1,8 @@
//! The main event loop which performs I/O on the pseudoterminal //! The main event loop which performs I/O on the pseudoterminal
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::io::{self, ErrorKind}; use std::io::{self, ErrorKind, Write};
use std::fs::File;
use std::os::unix::io::AsRawFd; use std::os::unix::io::AsRawFd;
use std::sync::Arc; use std::sync::Arc;
@ -34,6 +35,7 @@ pub struct EventLoop<Io> {
terminal: Arc<FairMutex<Term>>, terminal: Arc<FairMutex<Term>>,
proxy: ::glutin::WindowProxy, proxy: ::glutin::WindowProxy,
signal_flag: Flag, signal_flag: Flag,
ref_test: bool,
} }
/// Helper type which tracks how much of a buffer has been written. /// Helper type which tracks how much of a buffer has been written.
@ -130,6 +132,7 @@ impl<Io> EventLoop<Io>
proxy: ::glutin::WindowProxy, proxy: ::glutin::WindowProxy,
signal_flag: Flag, signal_flag: Flag,
pty: Io, pty: Io,
ref_test: bool,
) -> EventLoop<Io> { ) -> EventLoop<Io> {
let (tx, rx) = ::mio::channel::channel(); let (tx, rx) = ::mio::channel::channel();
EventLoop { EventLoop {
@ -139,7 +142,8 @@ impl<Io> EventLoop<Io>
rx: rx, rx: rx,
terminal: terminal, terminal: terminal,
proxy: proxy, proxy: proxy,
signal_flag: signal_flag signal_flag: signal_flag,
ref_test: ref_test,
} }
} }
@ -174,11 +178,15 @@ impl<Io> EventLoop<Io>
} }
#[inline] #[inline]
fn pty_read(&mut self, state: &mut State, buf: &mut [u8]) { fn pty_read<W: Write>(&mut self, state: &mut State, buf: &mut [u8], mut writer: Option<&mut W>) {
loop { loop {
match self.pty.read(&mut buf[..]) { match self.pty.read(&mut buf[..]) {
Ok(0) => break, Ok(0) => break,
Ok(got) => { Ok(got) => {
writer = writer.map(|w| {
w.write_all(&buf[..got]).unwrap(); w
});
let mut terminal = self.terminal.lock(); let mut terminal = self.terminal.lock();
for byte in &buf[..got] { for byte in &buf[..got] {
state.parser.advance(&mut *terminal, *byte); state.parser.advance(&mut *terminal, *byte);
@ -252,6 +260,14 @@ impl<Io> EventLoop<Io>
let mut events = Events::with_capacity(1024); let mut events = Events::with_capacity(1024);
let mut pipe = if self.ref_test {
let file = File::create("./alacritty.recording")
.expect("create alacritty recording");
Some(file)
} else {
None
};
'event_loop: loop { 'event_loop: loop {
self.poll.poll(&mut events, None).expect("poll ok"); self.poll.poll(&mut events, None).expect("poll ok");
@ -262,7 +278,7 @@ impl<Io> EventLoop<Io>
let kind = event.kind(); let kind = event.kind();
if kind.is_readable() { if kind.is_readable() {
self.pty_read(&mut state, &mut buf); self.pty_read(&mut state, &mut buf, pipe.as_mut());
if ::tty::process_should_exit() { if ::tty::process_should_exit() {
break 'event_loop; break 'event_loop;
} }

View File

@ -29,7 +29,7 @@ use std::slice::{self, Iter, IterMut};
use index::{self, Cursor}; use index::{self, Cursor};
/// Represents the terminal display contents /// Represents the terminal display contents
#[derive(Clone, Debug)] #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct Grid<T> { pub struct Grid<T> {
/// Lines in the grid. Each row holds a list of cells corresponding to the /// Lines in the grid. Each row holds a list of cells corresponding to the
/// columns in that row. /// columns in that row.
@ -221,7 +221,7 @@ impl<'cursor, T> IndexMut<&'cursor Cursor> for Grid<T> {
} }
/// A row in the grid /// A row in the grid
#[derive(Clone, Debug)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct Row<T>(Vec<T>); pub struct Row<T>(Vec<T>);
impl<T: Clone> Row<T> { impl<T: Clone> Row<T> {

View File

@ -21,7 +21,7 @@ use std::mem;
use std::ops::{self, Deref, Add}; use std::ops::{self, Deref, Add};
/// Index in the grid using row, column notation /// Index in the grid using row, column notation
#[derive(Debug, Clone, Default, Eq, PartialEq)] #[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct Cursor { pub struct Cursor {
pub line: Line, pub line: Line,
pub col: Column, pub col: Column,
@ -30,7 +30,7 @@ pub struct Cursor {
/// A line /// A line
/// ///
/// Newtype to avoid passing values incorrectly /// Newtype to avoid passing values incorrectly
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Ord, PartialOrd)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Ord, PartialOrd, Serialize, Deserialize)]
pub struct Line(pub usize); pub struct Line(pub usize);
impl fmt::Display for Line { impl fmt::Display for Line {
@ -42,7 +42,7 @@ impl fmt::Display for Line {
/// A column /// A column
/// ///
/// Newtype to avoid passing values incorrectly /// Newtype to avoid passing values incorrectly
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Ord, PartialOrd)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Ord, PartialOrd, Serialize, Deserialize)]
pub struct Column(pub usize); pub struct Column(pub usize);
impl fmt::Display for Column { impl fmt::Display for Column {

View File

@ -31,8 +31,7 @@ use glutin::{Mods, mods};
use config::Config; use config::Config;
use event_loop; use event_loop;
use term::mode::{self, TermMode}; use term::mode::{TermMode};
use util::encode_char;
/// Processes input from glutin. /// Processes input from glutin.
/// ///

98
src/lib.rs Normal file
View File

@ -0,0 +1,98 @@
// Copyright 2016 Joe Wilm, The Alacritty Project Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//! Alacritty - The GPU Enhanced Terminal
#![feature(question_mark)]
#![feature(range_contains)]
#![feature(inclusive_range_syntax)]
#![feature(drop_types_in_const)]
#![feature(unicode)]
#![feature(step_trait)]
#![feature(core_intrinsics)]
#![allow(stable_features)] // lying about question_mark because 1.14.0 isn't released!
#![feature(proc_macro)]
#[macro_use]
extern crate serde_derive;
extern crate cgmath;
extern crate copypasta;
extern crate errno;
extern crate font;
extern crate glutin;
extern crate libc;
extern crate mio;
extern crate notify;
extern crate parking_lot;
extern crate serde;
extern crate serde_json;
extern crate serde_yaml;
extern crate vte;
#[macro_use]
extern crate bitflags;
#[macro_use]
pub mod macros;
pub mod event;
pub mod event_loop;
pub mod index;
pub mod input;
pub mod meter;
pub mod renderer;
pub mod sync;
pub mod term;
pub mod tty;
pub mod util;
pub mod ansi;
pub mod config;
pub mod grid;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
pub use grid::Grid;
pub use term::Term;
#[derive(Debug, Eq, PartialEq, Copy, Clone, Default, Serialize, Deserialize)]
pub struct Rgb {
pub r: u8,
pub g: u8,
pub b: u8,
}
pub mod gl {
#![allow(non_upper_case_globals)]
include!(concat!(env!("OUT_DIR"), "/gl_bindings.rs"));
}
#[derive(Clone)]
pub struct Flag(pub Arc<AtomicBool>);
impl Flag {
pub fn new(initial_value: bool) -> Flag {
Flag(Arc::new(AtomicBool::new(initial_value)))
}
#[inline]
pub fn get(&self) -> bool {
self.0.load(Ordering::Acquire)
}
#[inline]
pub fn set(&self, value: bool) {
self.0.store(value, Ordering::Release)
}
}

View File

@ -14,12 +14,8 @@
// //
//! Alacritty - The GPU Enhanced Terminal //! Alacritty - The GPU Enhanced Terminal
#![feature(question_mark)] #![feature(question_mark)]
#![feature(range_contains)]
#![feature(inclusive_range_syntax)] #![feature(inclusive_range_syntax)]
#![feature(drop_types_in_const)] #![feature(drop_types_in_const)]
#![feature(unicode)]
#![feature(step_trait)]
#![feature(core_intrinsics)]
#![allow(stable_features)] // lying about question_mark because 1.14.0 isn't released! #![allow(stable_features)] // lying about question_mark because 1.14.0 isn't released!
#![feature(proc_macro)] #![feature(proc_macro)]
@ -27,6 +23,8 @@
#[macro_use] #[macro_use]
extern crate serde_derive; extern crate serde_derive;
#[macro_use]
extern crate alacritty;
extern crate cgmath; extern crate cgmath;
extern crate copypasta; extern crate copypasta;
extern crate errno; extern crate errno;
@ -37,64 +35,34 @@ extern crate mio;
extern crate notify; extern crate notify;
extern crate parking_lot; extern crate parking_lot;
extern crate serde; extern crate serde;
extern crate serde_json;
extern crate serde_yaml; extern crate serde_yaml;
extern crate vte; extern crate vte;
#[macro_use] #[macro_use]
extern crate bitflags; extern crate bitflags;
#[macro_use]
mod macros;
mod event;
mod event_loop;
mod index;
mod input;
mod meter;
mod renderer;
mod sync;
mod term;
mod tty;
mod util;
pub mod ansi;
pub mod config;
pub mod grid;
use std::sync::{mpsc, Arc}; use std::sync::{mpsc, Arc};
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::Ordering;
use parking_lot::{MutexGuard}; use parking_lot::{MutexGuard};
use event_loop::EventLoop; use alacritty::Flag;
use alacritty::Rgb;
use config::Config; use alacritty::config::{self, Config};
use meter::Meter; use alacritty::event;
use renderer::{QuadRenderer, GlyphCache}; use alacritty::gl;
use sync::FairMutex; use alacritty::input;
use term::Term; use alacritty::meter::Meter;
use tty::process_should_exit; use alacritty::renderer::{QuadRenderer, GlyphCache};
use alacritty::sync::FairMutex;
use alacritty::term::{self, Term};
use alacritty::tty::{self, Pty, process_should_exit};
use alacritty::event_loop::EventLoop;
/// Channel used by resize handling on mac /// Channel used by resize handling on mac
static mut RESIZE_CALLBACK: Option<Box<Fn(u32, u32)>> = None; static mut RESIZE_CALLBACK: Option<Box<Fn(u32, u32)>> = None;
#[derive(Clone)]
pub struct Flag(Arc<AtomicBool>);
impl Flag {
pub fn new(initial_value: bool) -> Flag {
Flag(Arc::new(AtomicBool::new(initial_value)))
}
#[inline]
pub fn get(&self) -> bool {
self.0.load(Ordering::Acquire)
}
#[inline]
pub fn set(&self, value: bool) {
self.0.store(value, Ordering::Release)
}
}
/// Resize handling for Mac /// Resize handling for Mac
fn window_resize_handler(width: u32, height: u32) { fn window_resize_handler(width: u32, height: u32) {
unsafe { unsafe {
@ -102,18 +70,6 @@ fn window_resize_handler(width: u32, height: u32) {
} }
} }
#[derive(Debug, Eq, PartialEq, Copy, Clone, Default)]
pub struct Rgb {
r: u8,
g: u8,
b: u8,
}
mod gl {
#![allow(non_upper_case_globals)]
include!(concat!(env!("OUT_DIR"), "/gl_bindings.rs"));
}
fn main() { fn main() {
// Load configuration // Load configuration
let (config, config_path) = match Config::load() { let (config, config_path) = match Config::load() {
@ -126,6 +82,27 @@ fn main() {
Ok((config, path)) => (config, Some(path)), Ok((config, path)) => (config, Some(path)),
}; };
let mut ref_test = false;
let mut columns = 80;
let mut lines = 24;
let mut args_iter = ::std::env::args();
while let Some(arg) = args_iter.next() {
match &arg[..] {
// Generate ref test
"--ref-test" => ref_test = true,
// Set dimensions
"-d" | "--dimensions" => {
args_iter.next()
.map(|w| w.parse().map(|w| columns = w));
args_iter.next()
.map(|h| h.parse().map(|h| lines = h));
},
// ignore unexpected
_ => (),
}
}
let font = config.font(); let font = config.font();
let dpi = config.dpi(); let dpi = config.dpi();
let render_timer = config.render_timer(); let render_timer = config.render_timer();
@ -145,7 +122,7 @@ fn main() {
let _ = unsafe { window.make_current() }; let _ = unsafe { window.make_current() };
unsafe { unsafe {
gl::Viewport(0, 0, width as i32, height as i32); // gl::Viewport(0, 0, width as i32, height as i32);
gl::Enable(gl::BLEND); gl::Enable(gl::BLEND);
gl::BlendFunc(gl::SRC1_COLOR, gl::ONE_MINUS_SRC1_COLOR); gl::BlendFunc(gl::SRC1_COLOR, gl::ONE_MINUS_SRC1_COLOR);
gl::Enable(gl::MULTISAMPLE); gl::Enable(gl::MULTISAMPLE);
@ -176,15 +153,30 @@ fn main() {
let cell_width = (metrics.average_advance + font.offset().x() as f64) as u32; let cell_width = (metrics.average_advance + font.offset().x() as f64) as u32;
let cell_height = (metrics.line_height + font.offset().y() as f64) as u32; let cell_height = (metrics.line_height + font.offset().y() as f64) as u32;
// Resize window to be 80 col x 24 lines
let width = cell_width * columns + 4;
let height = cell_height * lines + 4;
println!("set_inner_size: {} x {}", width, height);
// Is this in points?
let width_pts = (width as f32 / dpr) as u32;
let height_pts = (height as f32 / dpr) as u32;
println!("set_inner_size: {} x {}; pts: {} x {}", width, height, width_pts, height_pts);
window.set_inner_size(width_pts, height_pts);
renderer.resize(width as _, height as _);
println!("Cell Size: ({} x {})", cell_width, cell_height); println!("Cell Size: ({} x {})", cell_width, cell_height);
let terminal = Term::new( let size = term::SizeInfo {
width as f32, width: width as f32,
height as f32, height: height as f32,
cell_width as f32, cell_width: cell_width as f32,
cell_height as f32 cell_height: cell_height as f32
); };
let pty_io = terminal.tty().reader();
let terminal = Term::new(size);
let pty = tty::new(size.lines(), size.cols());
pty.resize(size.lines(), size.cols(), size.width as usize, size.height as usize);
let pty_io = pty.reader();
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
@ -215,6 +207,7 @@ fn main() {
window.create_window_proxy(), window.create_window_proxy(),
signal_flag.clone(), signal_flag.clone(),
pty_io, pty_io,
ref_test,
); );
let loop_tx = event_loop.channel(); let loop_tx = event_loop.channel();
@ -226,7 +219,8 @@ fn main() {
renderer, renderer,
glyph_cache, glyph_cache,
render_timer, render_timer,
rx rx,
pty
); );
// Event processor // Event processor
@ -234,7 +228,8 @@ fn main() {
input::LoopNotifier(loop_tx), input::LoopNotifier(loop_tx),
terminal.clone(), terminal.clone(),
tx, tx,
&config &config,
ref_test,
); );
let (config_tx, config_rx) = mpsc::channel(); let (config_tx, config_rx) = mpsc::channel();
@ -302,6 +297,7 @@ struct Display {
render_timer: bool, render_timer: bool,
rx: mpsc::Receiver<(u32, u32)>, rx: mpsc::Receiver<(u32, u32)>,
meter: Meter, meter: Meter,
pty: Pty,
} }
impl Display { impl Display {
@ -314,7 +310,8 @@ impl Display {
renderer: QuadRenderer, renderer: QuadRenderer,
glyph_cache: GlyphCache, glyph_cache: GlyphCache,
render_timer: bool, render_timer: bool,
rx: mpsc::Receiver<(u32, u32)>) rx: mpsc::Receiver<(u32, u32)>,
pty: Pty)
-> Display -> Display
{ {
Display { Display {
@ -324,6 +321,7 @@ impl Display {
render_timer: render_timer, render_timer: render_timer,
rx: rx, rx: rx,
meter: Meter::new(), meter: Meter::new(),
pty: pty,
} }
} }
@ -350,6 +348,8 @@ impl Display {
// available // available
if let Some((w, h)) = new_size.take() { if let Some((w, h)) = new_size.take() {
terminal.resize(w as f32, h as f32); terminal.resize(w as f32, h as f32);
let size = terminal.size_info();
self.pty.resize(size.lines(), size.cols(), w as _, h as _);
self.renderer.resize(w as i32, h as i32); self.renderer.resize(w as i32, h as i32);
} }
@ -369,7 +369,7 @@ impl Display {
// Draw render timer // Draw render timer
if self.render_timer { if self.render_timer {
let timing = format!("{:.3} usec", self.meter.average()); let timing = format!("{:.3} usec", self.meter.average());
let color = ::term::cell::Color::Rgb(Rgb { r: 0xd5, g: 0x4e, b: 0x53 }); let color = alacritty::term::cell::Color::Rgb(Rgb { r: 0xd5, g: 0x4e, b: 0x53 });
self.renderer.with_api(terminal.size_info(), |mut api| { self.renderer.with_api(terminal.size_info(), |mut api| {
api.render_string(&timing[..], glyph_cache, &color); api.render_string(&timing[..], glyph_cache, &color);
}); });

View File

@ -20,7 +20,7 @@
//! //!
//! ```rust //! ```rust
//! // create a meter //! // create a meter
//! let mut meter = Meter::new(); //! let mut meter = alacritty::meter::Meter::new();
//! //!
//! // Sample something. //! // Sample something.
//! { //! {
@ -29,7 +29,7 @@
//! //!
//! // Get the moving average. The meter tracks a fixed number of samles, and the average won't mean //! // Get the moving average. The meter tracks a fixed number of samles, and the average won't mean
//! // much until it's filled up at least once. //! // much until it's filled up at least once.
//! printf!("Average time: {}", meter.average()); //! println!("Average time: {}", meter.average());
use std::time::{Instant, Duration}; use std::time::{Instant, Duration};

View File

@ -1100,6 +1100,7 @@ impl From<io::Error> for ShaderCreationError {
/// ///
/// The strategy for filling an atlas looks roughly like this: /// The strategy for filling an atlas looks roughly like this:
/// ///
/// ```ignore
/// (width, height) /// (width, height)
/// ┌─────┬─────┬─────┬─────┬─────┐ /// ┌─────┬─────┬─────┬─────┬─────┐
/// │ 10 │ │ │ │ │ <- Empty spaces; can be filled while /// │ 10 │ │ │ │ │ <- Empty spaces; can be filled while
@ -1112,6 +1113,7 @@ impl From<io::Error> for ShaderCreationError {
/// │ │ │ │ │ <- Row considered full when next glyph doesn't /// │ │ │ │ │ <- Row considered full when next glyph doesn't
/// └─────┴─────┴─────┴───────────┘ fit in the row. /// └─────┴─────┴─────┴───────────┘ fit in the row.
/// (0, 0) x-> /// (0, 0) x->
/// ```
#[derive(Debug)] #[derive(Debug)]
struct Atlas { struct Atlas {
/// Texture id for this atlas /// Texture id for this atlas

View File

@ -20,7 +20,6 @@ use std::ptr;
use ansi::{self, Attr, Handler}; use ansi::{self, Attr, Handler};
use grid::{Grid, ClearRegion}; use grid::{Grid, ClearRegion};
use index::{Cursor, Column, Line}; use index::{Cursor, Column, Line};
use tty;
use ansi::Color; use ansi::Color;
/// RAII type which manages grid state for render /// RAII type which manages grid state for render
@ -80,6 +79,7 @@ pub mod cell {
use ::Rgb; use ::Rgb;
bitflags! { bitflags! {
#[derive(Serialize, Deserialize)]
pub flags Flags: u32 { pub flags Flags: u32 {
const INVERSE = 0b00000001, const INVERSE = 0b00000001,
const BOLD = 0b00000010, const BOLD = 0b00000010,
@ -88,13 +88,13 @@ pub mod cell {
} }
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Color { pub enum Color {
Rgb(Rgb), Rgb(Rgb),
Ansi(::ansi::Color), Ansi(::ansi::Color),
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct Cell { pub struct Cell {
pub c: char, pub c: char,
pub fg: Color, pub fg: Color,
@ -191,9 +191,6 @@ pub struct Term {
/// Alt is active /// Alt is active
alt: bool, alt: bool,
/// Reference to the underlying tty
tty: tty::Tty,
/// The cursor /// The cursor
cursor: Cursor, cursor: Cursor,
@ -222,7 +219,7 @@ pub struct Term {
} }
/// Terminal size info /// Terminal size info
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct SizeInfo { pub struct SizeInfo {
/// Terminal window width /// Terminal window width
pub width: f32, pub width: f32,
@ -251,18 +248,8 @@ impl SizeInfo {
impl Term { impl Term {
pub fn new( pub fn new(
width: f32, size: SizeInfo
height: f32,
cell_width: f32,
cell_height: f32
) -> Term { ) -> Term {
let size = SizeInfo {
width: width as f32,
height: height as f32,
cell_width: cell_width as f32,
cell_height: cell_height as f32,
};
let template = Cell::new( let template = Cell::new(
' ', ' ',
cell::Color::Ansi(Color::Foreground), cell::Color::Ansi(Color::Foreground),
@ -276,9 +263,6 @@ impl Term {
let grid = Grid::new(num_lines, num_cols, &template); let grid = Grid::new(num_lines, num_cols, &template);
let tty = tty::new(*num_lines as u8, *num_cols as u8);
tty.resize(*num_lines as usize, *num_cols as usize, size.width as usize, size.height as usize);
let mut tabs = (Column(0)..grid.num_cols()) let mut tabs = (Column(0)..grid.num_cols())
.map(|i| (*i as usize) % TAB_SPACES == 0) .map(|i| (*i as usize) % TAB_SPACES == 0)
.collect::<Vec<bool>>(); .collect::<Vec<bool>>();
@ -295,7 +279,6 @@ impl Term {
alt: false, alt: false,
cursor: Cursor::default(), cursor: Cursor::default(),
alt_cursor: Cursor::default(), alt_cursor: Cursor::default(),
tty: tty,
tabs: tabs, tabs: tabs,
mode: Default::default(), mode: Default::default(),
scroll_region: scroll_region, scroll_region: scroll_region,
@ -305,6 +288,10 @@ impl Term {
} }
} }
pub fn grid(&self) -> &Grid<Cell> {
&self.grid
}
pub fn render_grid<'a>(&'a mut self) -> RenderGrid<'a> { pub fn render_grid<'a>(&'a mut self) -> RenderGrid<'a> {
RenderGrid::new(&mut self.grid, &self.cursor, self.mode) RenderGrid::new(&mut self.grid, &self.cursor, self.mode)
} }
@ -364,18 +351,6 @@ impl Term {
// Reset scrolling region to new size // Reset scrolling region to new size
self.scroll_region = Line(0)..self.grid.num_lines(); self.scroll_region = Line(0)..self.grid.num_lines();
// Inform tty of new dimensions
self.tty.resize(*num_lines as _,
*num_cols as _,
self.size_info.width as usize,
self.size_info.height as usize);
}
#[inline]
pub fn tty(&self) -> &tty::Tty {
&self.tty
} }
#[inline] #[inline]
@ -876,3 +851,33 @@ impl ansi::Handler for Term {
self.mode.remove(mode::APP_KEYPAD); self.mode.remove(mode::APP_KEYPAD);
} }
} }
#[cfg(test)]
mod tests {
extern crate serde_json;
use ansi::Color;
use grid::Grid;
use index::{Line, Column};
use term::{cell, Cell};
/// Check that the grid can be serialized back and forth losslessly
///
/// This test is in the term module as opposed to the grid since we want to
/// test this property with a T=Cell.
#[test]
fn grid_serde() {
let template = Cell::new(
' ',
cell::Color::Ansi(Color::Foreground),
cell::Color::Ansi(Color::Background)
);
let grid = Grid::new(Line(24), Column(80), &template);
let serialized = serde_json::to_string(&grid).expect("ser");
let deserialized = serde_json::from_str::<Grid<Cell>>(&serialized)
.expect("de");
assert_eq!(deserialized, grid);
}
}

View File

@ -23,6 +23,8 @@ use std::ptr;
use libc::{self, winsize, c_int, pid_t, WNOHANG, WIFEXITED, WEXITSTATUS, SIGCHLD}; use libc::{self, winsize, c_int, pid_t, WNOHANG, WIFEXITED, WEXITSTATUS, SIGCHLD};
use index::{Line, Column};
/// Process ID of child process /// Process ID of child process
/// ///
/// Necessary to put this in static storage for `sigchld` to have access /// Necessary to put this in static storage for `sigchld` to have access
@ -236,8 +238,8 @@ fn execsh() -> ! {
} }
/// Create a new tty and return a handle to interact with it. /// Create a new tty and return a handle to interact with it.
pub fn new(rows: u8, cols: u8) -> Tty { pub fn new(lines: Line, cols: Column) -> Pty {
let (master, slave) = openpty(rows, cols); let (master, slave) = openpty(lines.0 as _, cols.0 as _);
match fork() { match fork() {
Relation::Child => { Relation::Child => {
@ -280,16 +282,16 @@ pub fn new(rows: u8, cols: u8) -> Tty {
set_nonblocking(master); set_nonblocking(master);
} }
Tty { fd: master } Pty { fd: master }
} }
} }
} }
pub struct Tty { pub struct Pty {
fd: c_int, fd: c_int,
} }
impl Tty { impl Pty {
/// Get reader for the TTY /// Get reader for the TTY
/// ///
/// XXX File is a bad abstraction here; it closes the fd on drop /// XXX File is a bad abstraction here; it closes the fd on drop
@ -299,9 +301,11 @@ impl Tty {
} }
} }
pub fn resize(&self, rows: usize, cols: usize, px_x: usize, px_y: usize) { pub fn resize(&self, lines: Line, cols: Column, px_x: usize, px_y: usize) {
let lines = lines.0;
let cols = cols.0;
let win = winsize { let win = winsize {
ws_row: rows as libc::c_ushort, ws_row: lines as libc::c_ushort,
ws_col: cols as libc::c_ushort, ws_col: cols as libc::c_ushort,
ws_xpixel: px_x as libc::c_ushort, ws_xpixel: px_x as libc::c_ushort,
ws_ypixel: px_y as libc::c_ushort, ws_ypixel: px_y as libc::c_ushort,

78
tests/ref.rs Normal file
View File

@ -0,0 +1,78 @@
extern crate alacritty;
extern crate serde_json;
/// ref tests
mod reference {
use std::fs::File;
use std::io::Read;
use std::path::Path;
use serde_json as json;
use alacritty::Grid;
use alacritty::Term;
use alacritty::term::Cell;
use alacritty::term::SizeInfo;
use alacritty::ansi;
macro_rules! ref_file {
($ref_name:ident, $file:expr) => {
concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/ref/", stringify!($ref_name), "/", $file
)
}
}
fn read_u8<P>(path: P) -> Vec<u8>
where P: AsRef<Path>
{
let mut res = Vec::new();
File::open(path.as_ref()).unwrap()
.read_to_end(&mut res).unwrap();
res
}
fn read_string<P>(path: P) -> String
where P: AsRef<Path>
{
let mut res = String::new();
File::open(path.as_ref()).unwrap()
.read_to_string(&mut res).unwrap();
res
}
macro_rules! ref_test {
($name:ident) => {
#[test]
fn $name() {
let recording = read_u8(ref_file!($name, "alacritty.recording"));
let serialized_size = read_string(ref_file!($name, "size.json"));
let serialized_grid = read_string(ref_file!($name, "grid.json"));
let size: SizeInfo = json::from_str(&serialized_size[..]).unwrap();
let grid: Grid<Cell> = json::from_str(&serialized_grid[..]).unwrap();
let mut terminal = Term::new(size);
let mut parser = ansi::Processor::new();
for byte in recording {
parser.advance(&mut terminal, byte);
}
assert_eq!(grid, *terminal.grid());
}
};
($( $name:ident ),*) => {
$(
ref_test! { $name }
),*
}
}
// Ref tests!
ref_test! { ll }
}

View File

@ -0,0 +1,27 @@
% jwilm@kurast.local ➜  ~/code/alacritty  [?1h=[?2004hlll[?1l>[?2004l
total 16440
drwxr-xr-x 3 jwilm staff 102B Nov 2 10:54 Alacritty.app
-rw-r--r-- 1 jwilm staff 53K Nov 19 14:27 Cargo.lock
-rw-r--r-- 1 jwilm staff 746B Nov 19 14:24 Cargo.toml
-rw-r--r-- 1 jwilm staff 11K Jun 30 10:44 LICENSE-APACHE
-rw-r--r-- 1 jwilm staff 1.6K Nov 2 10:52 Makefile
-rw-r--r-- 1 jwilm staff 49B Jun 9 18:56 TASKS.md
-rwxr-xr-x 1 jwilm staff 1.7M Sep 26 10:49 alacritty-pre-eloop
-rw-r--r-- 1 jwilm staff 255B Nov 19 14:31 alacritty.recording
-rw-r--r-- 1 jwilm staff 6.5K Nov 17 17:18 alacritty.yml
-rw-r--r-- 1 jwilm staff 1.1K Jun 30 10:44 build.rs
drwxr-xr-x 6 jwilm staff 204B Oct 10 10:46 copypasta
drwxr-xr-x 3 jwilm staff 102B Jun 9 18:56 docs
-rwxr-xr-x 1 jwilm staff 2.2M Nov 11 17:53 exitter
drwxr-xr-x 5 jwilm staff 170B Jun 28 14:50 font
-rwxr-xr-x 1 jwilm staff 2.2M Nov 14 13:27 hardcoded_bindings_alacritty
drwxr-xr-x 6 jwilm staff 204B Nov 2 10:54 macos
drwxr-xr-x 4 jwilm staff 136B Oct 27 17:59 res
-rw-r--r-- 1 jwilm staff 19B Nov 11 16:55 rustc-version
drwxr-xr-x 5 jwilm staff 170B Oct 10 10:46 scripts
drwxr-xr-x 17 jwilm staff 578B Nov 19 14:30 src
drwxr-xr-x 5 jwilm staff 170B Jun 28 15:49 target
-rw-r--r-- 1 jwilm staff 8.1K Nov 17 11:13 thing.log
-rw-r--r-- 1 jwilm staff 3.5K Sep 1 11:27 tmux-client-23038.log
-rwxr-xr-x 1 jwilm staff 1.8M Sep 22 12:03 with_parallel
% jwilm@kurast.local ➜  ~/code/alacritty  [?1h=[?2004h

1
tests/ref/ll/grid.json Normal file

File diff suppressed because one or more lines are too long

1
tests/ref/ll/size.json Normal file
View File

@ -0,0 +1 @@
{"width":1124.0,"height":628.0,"cell_width":14.0,"cell_height":26.0}