//! Configuration definitions and file loading //! //! Alacritty reads from a config file at startup to determine various runtime //! parameters including font family and style, font size, etc. In the future, //! the config file will also hold user and platform specific keybindings. use std::env; use std::fs; use std::io::{self, Read}; use std::path::{Path, PathBuf}; use std::sync::mpsc; use ::Rgb; use font::Size; use serde_yaml; use serde::{self, Error as SerdeError}; use notify::{Watcher as WatcherApi, RecommendedWatcher as FileWatcher, op}; /// Function that returns true for serde default fn true_bool() -> bool { true } /// Top-level config type #[derive(Debug, Deserialize)] pub struct Config { /// Pixels per inch #[serde(default)] dpi: Dpi, /// Font configuration #[serde(default)] font: Font, /// Should show render timer #[serde(default)] render_timer: bool, /// Should show render timer #[serde(default="true_bool")] draw_bold_text_with_bright_colors: bool, /// The standard ANSI colors to use #[serde(default)] colors: Colors, } impl Default for Config { fn default() -> Config { Config { draw_bold_text_with_bright_colors: true, dpi: Default::default(), font: Default::default(), render_timer: Default::default(), colors: Default::default(), } } } /// Errors occurring during config loading #[derive(Debug)] pub enum Error { /// Config file not found NotFound, /// Couldn't read $HOME environment variable ReadingEnvHome(env::VarError), /// io error reading file Io(io::Error), /// Not valid yaml or missing parameters Yaml(serde_yaml::Error), } #[derive(Debug, Deserialize)] pub struct Colors { primary: PrimaryColors, normal: AnsiColors, bright: AnsiColors, } #[derive(Debug, Deserialize)] pub struct PrimaryColors { background: Rgb, foreground: Rgb, } impl Default for Colors { fn default() -> Colors { Colors { primary: PrimaryColors { background: Rgb { r: 0, g: 0, b: 0 }, foreground: Rgb { r: 0xea, g: 0xea, b: 0xea }, }, normal: AnsiColors { black: Rgb {r: 0x00, g: 0x00, b: 0x00}, red: Rgb {r: 0xd5, g: 0x4e, b: 0x53}, green: Rgb {r: 0xb9, g: 0xca, b: 0x4a}, yellow: Rgb {r: 0xe6, g: 0xc5, b: 0x47}, blue: Rgb {r: 0x7a, g: 0xa6, b: 0xda}, magenta: Rgb {r: 0xc3, g: 0x97, b: 0xd8}, cyan: Rgb {r: 0x70, g: 0xc0, b: 0xba}, white: Rgb {r: 0x42, g: 0x42, b: 0x42}, }, bright: AnsiColors { black: Rgb {r: 0x66, g: 0x66, b: 0x66}, red: Rgb {r: 0xff, g: 0x33, b: 0x34}, green: Rgb {r: 0x9e, g: 0xc4, b: 0x00}, yellow: Rgb {r: 0xe7, g: 0xc5, b: 0x47}, blue: Rgb {r: 0x7a, g: 0xa6, b: 0xda}, magenta: Rgb {r: 0xb7, g: 0x7e, b: 0xe0}, cyan: Rgb {r: 0x54, g: 0xce, b: 0xd6}, white: Rgb {r: 0x2a, g: 0x2a, b: 0x2a}, } } } } /// The normal or bright colors section of config #[derive(Debug, Deserialize)] pub struct AnsiColors { black: Rgb, red: Rgb, green: Rgb, yellow: Rgb, blue: Rgb, magenta: Rgb, cyan: Rgb, white: Rgb, } impl serde::de::Deserialize for Rgb { fn deserialize(deserializer: &mut D) -> ::std::result::Result where D: serde::de::Deserializer { use std::marker::PhantomData; struct StringVisitor<__D> { _marker: PhantomData<__D>, } impl<__D> ::serde::de::Visitor for StringVisitor<__D> where __D: ::serde::de::Deserializer { type Value = String; fn visit_str(&mut self, value: &str) -> ::std::result::Result where E: ::serde::de::Error { Ok(value.to_owned()) } } deserializer .deserialize_f64(StringVisitor::{ _marker: PhantomData }) .and_then(|v| { Rgb::from_str(&v[..]) .map_err(|_| D::Error::custom("failed to parse rgb; expect 0xrrggbb")) }) } } impl Rgb { fn from_str(s: &str) -> ::std::result::Result { let mut chars = s.chars(); let mut rgb = Rgb::default(); macro_rules! component { ($($c:ident),*) => { $( match chars.next().unwrap().to_digit(16) { Some(val) => rgb.$c = (val as u8) << 4, None => return Err(()) } match chars.next().unwrap().to_digit(16) { Some(val) => rgb.$c |= val as u8, None => return Err(()) } )* } } if chars.next().unwrap() != '0' { return Err(()); } if chars.next().unwrap() != 'x' { return Err(()); } component!(r, g, b); Ok(rgb) } } impl ::std::error::Error for Error { fn cause(&self) -> Option<&::std::error::Error> { match *self { Error::NotFound => None, Error::ReadingEnvHome(ref err) => Some(err), Error::Io(ref err) => Some(err), Error::Yaml(ref err) => Some(err), } } fn description(&self) -> &str { match *self { Error::NotFound => "could not locate config file", Error::ReadingEnvHome(ref err) => err.description(), Error::Io(ref err) => err.description(), Error::Yaml(ref err) => err.description(), } } } impl ::std::fmt::Display for Error { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { match *self { Error::NotFound => write!(f, "{}", ::std::error::Error::description(self)), Error::ReadingEnvHome(ref err) => { write!(f, "could not read $HOME environment variable: {}", err) }, Error::Io(ref err) => write!(f, "error reading config file: {}", err), Error::Yaml(ref err) => write!(f, "problem with config: {:?}", err), } } } impl From for Error { fn from(val: env::VarError) -> Error { Error::ReadingEnvHome(val) } } impl From for Error { fn from(val: io::Error) -> Error { if val.kind() == io::ErrorKind::NotFound { Error::NotFound } else { Error::Io(val) } } } impl From for Error { fn from(val: serde_yaml::Error) -> Error { Error::Yaml(val) } } /// Result from config loading pub type Result = ::std::result::Result; impl Config { /// Attempt to load the config file /// /// The config file is loaded from the first file it finds in this list of paths /// /// 1. `$HOME/.config/alacritty.yml` /// 2. `$HOME/.alacritty.yml` pub fn load() -> Result<(Config, PathBuf)> { let home = env::var("HOME")?; // First path let mut path = PathBuf::from(&home); path.push(".config"); path.push("alacritty.yml"); match Config::load_from(path) { Ok(c) => Ok(c), Err(e) => { match e { Error::NotFound => { // Fallback path let mut alt_path = PathBuf::from(&home); alt_path.push(".alacritty.yml"); Config::load_from(alt_path) }, _ => Err(e), } } } } /// Get list of colors /// /// The ordering returned here is expected by the terminal. Colors are simply indexed in this /// array for performance. pub fn color_list(&self) -> [Rgb; 18] { let colors = &self.colors; [ // Normals colors.normal.black, colors.normal.red, colors.normal.green, colors.normal.yellow, colors.normal.blue, colors.normal.magenta, colors.normal.cyan, colors.normal.white, // Brights colors.bright.black, colors.bright.red, colors.bright.green, colors.bright.yellow, colors.bright.blue, colors.bright.magenta, colors.bright.cyan, colors.bright.white, // Foreground and background colors.primary.foreground, colors.primary.background, ] } pub fn fg_color(&self) -> Rgb { self.colors.primary.foreground } pub fn bg_color(&self) -> Rgb { self.colors.primary.background } #[inline] pub fn draw_bold_text_with_bright_colors(&self) -> bool { self.draw_bold_text_with_bright_colors } /// Get font config #[inline] pub fn font(&self) -> &Font { &self.font } /// Get dpi config #[inline] pub fn dpi(&self) -> &Dpi { &self.dpi } /// Should show render timer #[inline] pub fn render_timer(&self) -> bool { self.render_timer } fn load_from>(path: P) -> Result<(Config, PathBuf)> { let path = path.into(); let raw = Config::read_file(path.as_path())?; Ok((serde_yaml::from_str(&raw[..])?, path)) } fn read_file>(path: P) -> Result { let mut f = fs::File::open(path)?; let mut contents = String::new(); f.read_to_string(&mut contents)?; Ok(contents) } } /// Pixels per inch /// /// This is only used on FreeType systems #[derive(Debug, Deserialize)] pub struct Dpi { /// Horizontal dpi x: f32, /// Vertical dpi y: f32, } impl Default for Dpi { fn default() -> Dpi { Dpi { x: 96.0, y: 96.0 } } } impl Dpi { /// Get horizontal dpi #[inline] pub fn x(&self) -> f32 { self.x } /// Get vertical dpi #[inline] pub fn y(&self) -> f32 { self.y } } /// Modifications to font spacing /// /// The way Alacritty calculates vertical and horizontal cell sizes may not be /// ideal for all fonts. This gives the user a way to tweak those values. #[derive(Debug, Deserialize)] pub struct FontOffset { /// Extra horizontal spacing between letters x: f32, /// Extra vertical spacing between lines y: f32, } impl FontOffset { /// Get letter spacing #[inline] pub fn x(&self) -> f32 { self.x } /// Get line spacing #[inline] pub fn y(&self) -> f32 { self.y } } trait DeserializeFromF32 : Sized { fn deserialize_from_f32(&mut D) -> ::std::result::Result where D: serde::de::Deserializer; } impl DeserializeFromF32 for Size { fn deserialize_from_f32(deserializer: &mut D) -> ::std::result::Result where D: serde::de::Deserializer { use std::marker::PhantomData; struct FloatVisitor<__D> { _marker: PhantomData<__D>, } impl<__D> ::serde::de::Visitor for FloatVisitor<__D> where __D: ::serde::de::Deserializer { type Value = f64; fn visit_f64(&mut self, value: f64) -> ::std::result::Result where E: ::serde::de::Error { Ok(value) } } deserializer .deserialize_f64(FloatVisitor::{ _marker: PhantomData }) .map(|v| Size::new(v as _)) } } /// Font config /// /// Defaults are provided at the level of this struct per platform, but not per /// field in this struct. It might be nice in the future to have defaults for /// each value independently. Alternatively, maybe erroring when the user /// doesn't provide complete config is Ok. #[derive(Debug, Deserialize)] pub struct Font { /// Font family family: String, /// Font style style: String, /// Bold font style bold_style: Option, /// Italic font style italic_style: Option, // Font size in points #[serde(deserialize_with="DeserializeFromF32::deserialize_from_f32")] size: Size, /// Extra spacing per character offset: FontOffset, } impl Font { /// Get the font family #[inline] pub fn family(&self) -> &str { &self.family[..] } /// Get the font style #[inline] pub fn style(&self) -> &str { &self.style[..] } /// Get italic font style; assumes same family #[inline] pub fn italic_style(&self) -> Option<&str> { self.italic_style .as_ref() .map(|s| s.as_str()) } /// Get bold font style; assumes same family #[inline] pub fn bold_style(&self) -> Option<&str> { self.bold_style .as_ref() .map(|s| s.as_str()) } /// Get the font size in points #[inline] pub fn size(&self) -> Size { self.size } /// Get offsets to font metrics #[inline] pub fn offset(&self) -> &FontOffset { &self.offset } } #[cfg(target_os = "macos")] impl Default for Font { fn default() -> Font { Font { family: String::from("Menlo"), style: String::from("Regular"), size: Size::new(11.0), bold_style: Some(String::from("Bold")), italic_style: Some(String::from("Italic")), offset: FontOffset { x: 0.0, y: 0.0 } } } } #[cfg(target_os = "linux")] impl Default for Font { fn default() -> Font { Font { family: String::from("DejaVu Sans Mono"), style: String::from("Book"), size: Size::new(11.0), bold_style: Some(String::from("Bold")), italic_style: Some(String::from("Italic")), offset: FontOffset { // TODO should improve freetype metrics... shouldn't need such // drastic offsets for the default! x: 2.0, y: -7.0 } } } } pub struct Watcher(::std::thread::JoinHandle<()>); pub trait OnConfigReload { fn on_config_reload(&mut self, Config); } impl Watcher { pub fn new(path: PathBuf, mut handler: H) -> Watcher { Watcher(::util::thread::spawn_named("config watcher", move || { let (tx, rx) = mpsc::channel(); let mut watcher = FileWatcher::new(tx).unwrap(); watcher.watch(&path).expect("watch alacritty yml"); let config_path = path.as_path(); loop { let event = rx.recv().expect("watcher event"); let ::notify::Event { path, op } = event; if let Ok(op) = op { if op.contains(op::RENAME) { continue; } if op.contains(op::IGNORED) { if let Some(path) = path.as_ref() { if let Err(err) = watcher.watch(&path) { err_println!("failed to establish watch on {:?}: {:?}", path, err); } if path == config_path { if let Ok((config, _)) = Config::load() { handler.on_config_reload(config); }; } } } } } })) } }