mirror of
https://github.com/alacritty/alacritty.git
synced 2024-11-11 13:51:01 -05:00
31271c726e
The rusttype backend did not properly support manually specifying font styles, but instead chose to panic when they are specified. The rusttype implementation now provides a proper implementation for handling `bold`, `italic` and `regular` font styles. This fixes #2020.
2630 lines
76 KiB
Rust
2630 lines
76 KiB
Rust
//! 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::borrow::Cow;
|
|
use std::{env, fmt};
|
|
use std::fs::{self, File};
|
|
use std::io::{self, Read, Write};
|
|
use std::path::{Path, PathBuf};
|
|
use std::str::FromStr;
|
|
use std::sync::mpsc;
|
|
use std::time::Duration;
|
|
use std::collections::HashMap;
|
|
|
|
use crate::Rgb;
|
|
use font::Size;
|
|
use serde_yaml;
|
|
use serde::{self, de, Deserialize};
|
|
use serde::de::Error as SerdeError;
|
|
use serde::de::{Visitor, MapAccess, Unexpected};
|
|
use notify::{Watcher, watcher, DebouncedEvent, RecursiveMode};
|
|
use glutin::ModifiersState;
|
|
|
|
use crate::cli::Options;
|
|
use crate::input::{Action, Binding, MouseBinding, KeyBinding};
|
|
use crate::index::{Line, Column};
|
|
use crate::ansi::{CursorStyle, NamedColor, Color};
|
|
|
|
mod bindings;
|
|
|
|
const MAX_SCROLLBACK_LINES: u32 = 100_000;
|
|
static DEFAULT_ALACRITTY_CONFIG: &'static str =
|
|
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/alacritty.yml"));
|
|
|
|
#[serde(default)]
|
|
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
|
pub struct Selection {
|
|
#[serde(deserialize_with = "deserialize_escape_chars")]
|
|
pub semantic_escape_chars: String,
|
|
#[serde(deserialize_with = "failure_default")]
|
|
pub save_to_clipboard: bool,
|
|
}
|
|
|
|
impl Default for Selection {
|
|
fn default() -> Selection {
|
|
Selection {
|
|
semantic_escape_chars: default_escape_chars(),
|
|
save_to_clipboard: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn deserialize_escape_chars<'a, D>(deserializer: D) -> ::std::result::Result<String, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
match String::deserialize(deserializer) {
|
|
Ok(escape_chars) => Ok(escape_chars),
|
|
Err(err) => {
|
|
error!("Problem with config: {}; using default value", err);
|
|
Ok(default_escape_chars())
|
|
},
|
|
}
|
|
}
|
|
|
|
fn default_escape_chars() -> String {
|
|
String::from(",│`|:\"' ()[]{}<>")
|
|
}
|
|
|
|
#[serde(default)]
|
|
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
|
pub struct ClickHandler {
|
|
#[serde(deserialize_with = "deserialize_duration_ms")]
|
|
pub threshold: Duration,
|
|
}
|
|
|
|
impl Default for ClickHandler {
|
|
fn default() -> Self {
|
|
ClickHandler { threshold: default_threshold_ms() }
|
|
}
|
|
}
|
|
|
|
fn default_threshold_ms() -> Duration {
|
|
Duration::from_millis(300)
|
|
}
|
|
|
|
fn deserialize_duration_ms<'a, D>(deserializer: D) -> ::std::result::Result<Duration, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
match u64::deserialize(deserializer) {
|
|
Ok(threshold_ms) => Ok(Duration::from_millis(threshold_ms)),
|
|
Err(err) => {
|
|
error!("Problem with config: {}; using default value", err);
|
|
Ok(default_threshold_ms())
|
|
},
|
|
}
|
|
}
|
|
|
|
#[serde(default)]
|
|
#[derive(Default, Clone, Debug, Deserialize, PartialEq, Eq)]
|
|
pub struct Mouse {
|
|
#[serde(deserialize_with = "failure_default")]
|
|
pub double_click: ClickHandler,
|
|
#[serde(deserialize_with = "failure_default")]
|
|
pub triple_click: ClickHandler,
|
|
#[serde(deserialize_with = "failure_default")]
|
|
pub hide_when_typing: bool,
|
|
#[serde(deserialize_with = "failure_default")]
|
|
pub url: Url,
|
|
|
|
// TODO: DEPRECATED
|
|
pub faux_scrollback_lines: Option<usize>,
|
|
}
|
|
|
|
#[serde(default)]
|
|
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
|
pub struct Url {
|
|
// Program for opening links
|
|
#[serde(deserialize_with = "failure_default")]
|
|
pub launcher: Option<CommandWrapper>,
|
|
|
|
// Modifier used to open links
|
|
#[serde(deserialize_with = "deserialize_modifiers")]
|
|
pub modifiers: ModifiersState,
|
|
}
|
|
|
|
impl Default for Url {
|
|
fn default() -> Url {
|
|
Url {
|
|
#[cfg(not(any(target_os = "macos", windows)))]
|
|
launcher: Some(CommandWrapper::Just(String::from("xdg-open"))),
|
|
#[cfg(target_os = "macos")]
|
|
launcher: Some(CommandWrapper::Just(String::from("open"))),
|
|
#[cfg(windows)]
|
|
launcher: Some(CommandWrapper::Just(String::from("explorer"))),
|
|
modifiers: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn deserialize_modifiers<'a, D>(deserializer: D) -> ::std::result::Result<ModifiersState, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
ModsWrapper::deserialize(deserializer).map(|wrapper| wrapper.into_inner())
|
|
}
|
|
|
|
/// `VisualBellAnimations` are modeled after a subset of CSS transitions and Robert
|
|
/// Penner's Easing Functions.
|
|
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
|
pub enum VisualBellAnimation {
|
|
Ease, // CSS
|
|
EaseOut, // CSS
|
|
EaseOutSine, // Penner
|
|
EaseOutQuad, // Penner
|
|
EaseOutCubic, // Penner
|
|
EaseOutQuart, // Penner
|
|
EaseOutQuint, // Penner
|
|
EaseOutExpo, // Penner
|
|
EaseOutCirc, // Penner
|
|
Linear,
|
|
}
|
|
|
|
impl Default for VisualBellAnimation {
|
|
fn default() -> Self {
|
|
VisualBellAnimation::EaseOutExpo
|
|
}
|
|
}
|
|
|
|
#[serde(default)]
|
|
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
|
pub struct VisualBellConfig {
|
|
/// Visual bell animation function
|
|
#[serde(deserialize_with = "failure_default")]
|
|
animation: VisualBellAnimation,
|
|
|
|
/// Visual bell duration in milliseconds
|
|
#[serde(deserialize_with = "failure_default")]
|
|
duration: u16,
|
|
|
|
/// Visual bell flash color
|
|
#[serde(deserialize_with = "rgb_from_hex")]
|
|
color: Rgb,
|
|
}
|
|
|
|
impl Default for VisualBellConfig {
|
|
fn default() -> VisualBellConfig {
|
|
VisualBellConfig {
|
|
animation: Default::default(),
|
|
duration: Default::default(),
|
|
color: default_visual_bell_color(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn default_visual_bell_color() -> Rgb {
|
|
Rgb { r: 255, g: 255, b: 255 }
|
|
}
|
|
|
|
impl VisualBellConfig {
|
|
/// Visual bell animation
|
|
#[inline]
|
|
pub fn animation(&self) -> VisualBellAnimation {
|
|
self.animation
|
|
}
|
|
|
|
/// Visual bell duration in milliseconds
|
|
#[inline]
|
|
pub fn duration(&self) -> Duration {
|
|
Duration::from_millis(u64::from(self.duration))
|
|
}
|
|
|
|
/// Visual bell flash color
|
|
#[inline]
|
|
pub fn color(&self) -> Rgb {
|
|
self.color
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
|
pub struct Shell<'a> {
|
|
program: Cow<'a, str>,
|
|
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
args: Vec<String>,
|
|
}
|
|
|
|
impl<'a> Shell<'a> {
|
|
pub fn new<S>(program: S) -> Shell<'a>
|
|
where S: Into<Cow<'a, str>>
|
|
{
|
|
Shell {
|
|
program: program.into(),
|
|
args: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn new_with_args<S>(program: S, args: Vec<String>) -> Shell<'a>
|
|
where S: Into<Cow<'a, str>>
|
|
{
|
|
Shell {
|
|
program: program.into(),
|
|
args,
|
|
}
|
|
}
|
|
|
|
pub fn program(&self) -> &str {
|
|
&*self.program
|
|
}
|
|
|
|
pub fn args(&self) -> &[String] {
|
|
self.args.as_slice()
|
|
}
|
|
}
|
|
|
|
/// Wrapper around f32 that represents an alpha value between 0.0 and 1.0
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct Alpha(f32);
|
|
|
|
impl Alpha {
|
|
pub fn new(value: f32) -> Self {
|
|
Alpha(Self::clamp_to_valid_range(value))
|
|
}
|
|
|
|
pub fn set(&mut self, value: f32) {
|
|
self.0 = Self::clamp_to_valid_range(value);
|
|
}
|
|
|
|
#[inline]
|
|
pub fn get(self) -> f32 {
|
|
self.0
|
|
}
|
|
|
|
fn clamp_to_valid_range(value: f32) -> f32 {
|
|
if value < 0.0 {
|
|
0.0
|
|
} else if value > 1.0 {
|
|
1.0
|
|
} else {
|
|
value
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Alpha {
|
|
fn default() -> Self {
|
|
Alpha(1.0)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
pub enum Decorations {
|
|
Full,
|
|
Transparent,
|
|
Buttonless,
|
|
None,
|
|
}
|
|
|
|
impl Default for Decorations {
|
|
fn default() -> Decorations {
|
|
Decorations::Full
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for Decorations {
|
|
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Decorations, D::Error>
|
|
where D: de::Deserializer<'de>
|
|
{
|
|
|
|
struct DecorationsVisitor;
|
|
|
|
impl<'de> Visitor<'de> for DecorationsVisitor {
|
|
type Value = Decorations;
|
|
|
|
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str("Some subset of full|transparent|buttonless|none")
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
fn visit_str<E>(self, value: &str) -> ::std::result::Result<Decorations, E>
|
|
where E: de::Error
|
|
{
|
|
match value.to_lowercase().as_str() {
|
|
"transparent" => Ok(Decorations::Transparent),
|
|
"buttonless" => Ok(Decorations::Buttonless),
|
|
"none" => Ok(Decorations::None),
|
|
"full" => Ok(Decorations::Full),
|
|
"true" => {
|
|
error!("Deprecated decorations boolean value, \
|
|
use one of transparent|buttonless|none|full instead; \
|
|
falling back to \"full\"");
|
|
Ok(Decorations::Full)
|
|
},
|
|
"false" => {
|
|
error!("Deprecated decorations boolean value, \
|
|
use one of transparent|buttonless|none|full instead; \
|
|
falling back to \"none\"");
|
|
Ok(Decorations::None)
|
|
},
|
|
_ => {
|
|
error!("Invalid decorations value: {}; using default value", value);
|
|
Ok(Decorations::Full)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
fn visit_str<E>(self, value: &str) -> ::std::result::Result<Decorations, E>
|
|
where E: de::Error
|
|
{
|
|
match value.to_lowercase().as_str() {
|
|
"none" => Ok(Decorations::None),
|
|
"full" => Ok(Decorations::Full),
|
|
"true" => {
|
|
error!("Deprecated decorations boolean value, \
|
|
use one of none|full instead; \
|
|
falling back to \"full\"");
|
|
Ok(Decorations::Full)
|
|
},
|
|
"false" => {
|
|
error!("Deprecated decorations boolean value, \
|
|
use one of none|full instead; \
|
|
falling back to \"none\"");
|
|
Ok(Decorations::None)
|
|
},
|
|
"transparent" | "buttonless" => {
|
|
error!("macOS-only decorations value: {}; using default value", value);
|
|
Ok(Decorations::Full)
|
|
},
|
|
_ => {
|
|
error!("Invalid decorations value: {}; using default value", value);
|
|
Ok(Decorations::Full)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
deserializer.deserialize_str(DecorationsVisitor)
|
|
}
|
|
}
|
|
|
|
#[serde(default)]
|
|
#[derive(Debug, Copy, Clone, Deserialize, PartialEq, Eq)]
|
|
pub struct WindowConfig {
|
|
/// Initial dimensions
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
dimensions: Dimensions,
|
|
|
|
/// Pixel padding
|
|
#[serde(deserialize_with = "deserialize_padding")]
|
|
padding: Delta<u8>,
|
|
|
|
/// Draw the window with title bar / borders
|
|
#[serde(deserialize_with = "failure_default")]
|
|
decorations: Decorations,
|
|
|
|
/// Spread out additional padding evenly
|
|
#[serde(deserialize_with = "failure_default")]
|
|
dynamic_padding: bool,
|
|
|
|
/// Start maximized
|
|
#[serde(deserialize_with = "failure_default")]
|
|
start_maximized: bool,
|
|
}
|
|
|
|
impl Default for WindowConfig {
|
|
fn default() -> Self {
|
|
WindowConfig{
|
|
dimensions: Default::default(),
|
|
padding: default_padding(),
|
|
decorations: Default::default(),
|
|
dynamic_padding: Default::default(),
|
|
start_maximized: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn default_padding() -> Delta<u8> {
|
|
Delta { x: 2, y: 2 }
|
|
}
|
|
|
|
fn deserialize_padding<'a, D>(deserializer: D) -> ::std::result::Result<Delta<u8>, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
match Delta::deserialize(deserializer) {
|
|
Ok(delta) => Ok(delta),
|
|
Err(err) => {
|
|
error!("Problem with config: {}; using default value", err);
|
|
Ok(default_padding())
|
|
},
|
|
}
|
|
}
|
|
|
|
impl WindowConfig {
|
|
pub fn decorations(&self) -> Decorations {
|
|
self.decorations
|
|
}
|
|
|
|
pub fn dynamic_padding(&self) -> bool {
|
|
self.dynamic_padding
|
|
}
|
|
|
|
pub fn start_maximized(&self) -> bool {
|
|
self.start_maximized
|
|
}
|
|
}
|
|
|
|
/// Top-level config type
|
|
#[derive(Debug, PartialEq, Deserialize)]
|
|
pub struct Config {
|
|
/// Initial dimensions
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
dimensions: Option<Dimensions>,
|
|
|
|
/// Pixel padding
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
padding: Option<Delta<u8>>,
|
|
|
|
/// TERM env variable
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
env: HashMap<String, String>,
|
|
|
|
/// Font configuration
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
font: Font,
|
|
|
|
/// Should show render timer
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
render_timer: bool,
|
|
|
|
/// Should draw bold text with brighter colors instead of bold font
|
|
#[serde(default = "default_true_bool", deserialize_with = "deserialize_true_bool")]
|
|
draw_bold_text_with_bright_colors: bool,
|
|
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
colors: Colors,
|
|
|
|
/// Background opacity from 0.0 to 1.0
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
background_opacity: Alpha,
|
|
|
|
/// Window configuration
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
window: WindowConfig,
|
|
|
|
/// Keybindings
|
|
#[serde(default="default_key_bindings", deserialize_with = "deserialize_key_bindings")]
|
|
key_bindings: Vec<KeyBinding>,
|
|
|
|
/// Bindings for the mouse
|
|
#[serde(default="default_mouse_bindings", deserialize_with = "deserialize_mouse_bindings")]
|
|
mouse_bindings: Vec<MouseBinding>,
|
|
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
selection: Selection,
|
|
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
mouse: Mouse,
|
|
|
|
/// Path to a shell program to run on startup
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
shell: Option<Shell<'static>>,
|
|
|
|
/// Path where config was loaded from
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
config_path: Option<PathBuf>,
|
|
|
|
/// Visual bell configuration
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
visual_bell: VisualBellConfig,
|
|
|
|
/// Use dynamic title
|
|
#[serde(default = "default_true_bool", deserialize_with = "deserialize_true_bool")]
|
|
dynamic_title: bool,
|
|
|
|
/// Live config reload
|
|
#[serde(default = "default_true_bool", deserialize_with = "deserialize_true_bool")]
|
|
live_config_reload: bool,
|
|
|
|
/// Number of spaces in one tab
|
|
#[serde(default = "default_tabspaces", deserialize_with = "deserialize_tabspaces")]
|
|
tabspaces: usize,
|
|
|
|
/// How much scrolling history to keep
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
scrolling: Scrolling,
|
|
|
|
/// Cursor configuration
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
cursor: Cursor,
|
|
|
|
/// Keep the log file after quitting
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
persistent_logging: bool,
|
|
|
|
/// Enable experimental conpty backend instead of using winpty.
|
|
/// Will only take effect on Windows 10 Oct 2018 and later.
|
|
#[cfg(windows)]
|
|
#[serde(default, deserialize_with = "failure_default")]
|
|
enable_experimental_conpty_backend: bool,
|
|
|
|
/// Send escape sequences using the alt key.
|
|
#[serde(default = "default_true_bool", deserialize_with = "deserialize_true_bool")]
|
|
alt_send_esc: bool,
|
|
|
|
// TODO: DEPRECATED
|
|
custom_cursor_colors: Option<bool>,
|
|
|
|
// TODO: DEPRECATED
|
|
hide_cursor_when_typing: Option<bool>,
|
|
|
|
// TODO: DEPRECATED
|
|
cursor_style: Option<CursorStyle>,
|
|
|
|
// TODO: DEPRECATED
|
|
unfocused_hollow_cursor: Option<bool>,
|
|
}
|
|
|
|
impl Default for Config {
|
|
fn default() -> Self {
|
|
serde_yaml::from_str(DEFAULT_ALACRITTY_CONFIG)
|
|
.expect("default config is invalid")
|
|
}
|
|
}
|
|
|
|
fn default_key_bindings() -> Vec<KeyBinding> {
|
|
bindings::default_key_bindings()
|
|
}
|
|
|
|
fn default_mouse_bindings() -> Vec<MouseBinding> {
|
|
bindings::default_mouse_bindings()
|
|
}
|
|
|
|
fn deserialize_key_bindings<'a, D>(deserializer: D)
|
|
-> ::std::result::Result<Vec<KeyBinding>, D::Error>
|
|
where
|
|
D: de::Deserializer<'a>,
|
|
{
|
|
deserialize_bindings(deserializer, bindings::default_key_bindings())
|
|
}
|
|
|
|
fn deserialize_mouse_bindings<'a, D>(deserializer: D)
|
|
-> ::std::result::Result<Vec<MouseBinding>, D::Error>
|
|
where
|
|
D: de::Deserializer<'a>,
|
|
{
|
|
deserialize_bindings(deserializer, bindings::default_mouse_bindings())
|
|
}
|
|
|
|
fn deserialize_bindings<'a, D, T>(deserializer: D, mut default: Vec<Binding<T>>)
|
|
-> ::std::result::Result<Vec<Binding<T>>, D::Error>
|
|
where
|
|
D: de::Deserializer<'a>,
|
|
T: Copy + Eq + std::hash::Hash + std::fmt::Debug,
|
|
Binding<T>: de::Deserialize<'a>,
|
|
{
|
|
let mut bindings: Vec<Binding<T>> = failure_default_vec(deserializer)?;
|
|
|
|
for binding in bindings.iter() {
|
|
default.retain(|b| !b.triggers_match(binding));
|
|
}
|
|
|
|
bindings.extend(default);
|
|
|
|
Ok(bindings)
|
|
}
|
|
|
|
fn failure_default_vec<'a, D, T>(deserializer: D) -> ::std::result::Result<Vec<T>, D::Error>
|
|
where D: de::Deserializer<'a>,
|
|
T: Deserialize<'a>
|
|
{
|
|
// Deserialize as generic vector
|
|
let vec = match Vec::<serde_yaml::Value>::deserialize(deserializer) {
|
|
Ok(vec) => vec,
|
|
Err(err) => {
|
|
error!("Problem with config: {}; using empty vector", err);
|
|
return Ok(Vec::new());
|
|
},
|
|
};
|
|
|
|
// Move to lossy vector
|
|
let mut bindings: Vec<T> = Vec::new();
|
|
for value in vec {
|
|
match T::deserialize(value) {
|
|
Ok(binding) => bindings.push(binding),
|
|
Err(err) => {
|
|
error!("Problem with config: {}; skipping value", err);
|
|
},
|
|
}
|
|
}
|
|
|
|
Ok(bindings)
|
|
}
|
|
|
|
fn default_tabspaces() -> usize {
|
|
8
|
|
}
|
|
|
|
fn deserialize_tabspaces<'a, D>(deserializer: D) -> ::std::result::Result<usize, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
match usize::deserialize(deserializer) {
|
|
Ok(value) => Ok(value),
|
|
Err(err) => {
|
|
error!("Problem with config: {}; using 8", err);
|
|
Ok(default_tabspaces())
|
|
},
|
|
}
|
|
}
|
|
|
|
fn deserialize_true_bool<'a, D>(deserializer: D) -> ::std::result::Result<bool, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
match bool::deserialize(deserializer) {
|
|
Ok(value) => Ok(value),
|
|
Err(err) => {
|
|
error!("Problem with config: {}; using true", err);
|
|
Ok(true)
|
|
},
|
|
}
|
|
}
|
|
|
|
fn default_true_bool() -> bool {
|
|
true
|
|
}
|
|
|
|
fn failure_default<'a, D, T>(deserializer: D)
|
|
-> ::std::result::Result<T, D::Error>
|
|
where D: de::Deserializer<'a>,
|
|
T: Deserialize<'a> + Default
|
|
{
|
|
match T::deserialize(deserializer) {
|
|
Ok(value) => Ok(value),
|
|
Err(err) => {
|
|
error!("Problem with config: {}; using default value", err);
|
|
Ok(T::default())
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Struct for scrolling related settings
|
|
#[serde(default)]
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
|
|
pub struct Scrolling {
|
|
#[serde(deserialize_with = "deserialize_scrolling_history")]
|
|
pub history: u32,
|
|
#[serde(deserialize_with = "deserialize_scrolling_multiplier")]
|
|
pub multiplier: u8,
|
|
#[serde(deserialize_with = "deserialize_scrolling_multiplier")]
|
|
pub faux_multiplier: u8,
|
|
#[serde(deserialize_with = "failure_default")]
|
|
pub auto_scroll: bool,
|
|
}
|
|
|
|
impl Default for Scrolling {
|
|
fn default() -> Self {
|
|
Self {
|
|
history: default_scrolling_history(),
|
|
multiplier: default_scrolling_multiplier(),
|
|
faux_multiplier: default_scrolling_multiplier(),
|
|
auto_scroll: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn default_scrolling_history() -> u32 {
|
|
10_000
|
|
}
|
|
|
|
// Default for normal and faux scrolling
|
|
fn default_scrolling_multiplier() -> u8 {
|
|
3
|
|
}
|
|
|
|
fn deserialize_scrolling_history<'a, D>(deserializer: D) -> ::std::result::Result<u32, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
match u32::deserialize(deserializer) {
|
|
Ok(lines) => {
|
|
if lines > MAX_SCROLLBACK_LINES {
|
|
error!(
|
|
"Problem with config: scrollback size is {}, but expected a maximum of {}; \
|
|
using {1} instead",
|
|
lines, MAX_SCROLLBACK_LINES,
|
|
);
|
|
Ok(MAX_SCROLLBACK_LINES)
|
|
} else {
|
|
Ok(lines)
|
|
}
|
|
},
|
|
Err(err) => {
|
|
error!("Problem with config: {}; using default value", err);
|
|
Ok(default_scrolling_history())
|
|
},
|
|
}
|
|
}
|
|
|
|
fn deserialize_scrolling_multiplier<'a, D>(deserializer: D) -> ::std::result::Result<u8, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
match u8::deserialize(deserializer) {
|
|
Ok(lines) => Ok(lines),
|
|
Err(err) => {
|
|
error!("Problem with config: {}; using default value", err);
|
|
Ok(default_scrolling_multiplier())
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Newtype for implementing deserialize on glutin Mods
|
|
///
|
|
/// Our deserialize impl wouldn't be covered by a derive(Deserialize); see the
|
|
/// impl below.
|
|
#[derive(Debug, Copy, Clone, Hash, Default, Eq, PartialEq)]
|
|
struct ModsWrapper(ModifiersState);
|
|
|
|
impl ModsWrapper {
|
|
fn into_inner(self) -> ModifiersState {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl<'a> de::Deserialize<'a> for ModsWrapper {
|
|
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
struct ModsVisitor;
|
|
|
|
impl<'a> Visitor<'a> for ModsVisitor {
|
|
type Value = ModsWrapper;
|
|
|
|
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str("Some subset of Command|Shift|Super|Alt|Option|Control")
|
|
}
|
|
|
|
fn visit_str<E>(self, value: &str) -> ::std::result::Result<ModsWrapper, E>
|
|
where E: de::Error,
|
|
{
|
|
let mut res = ModifiersState::default();
|
|
for modifier in value.split('|') {
|
|
match modifier.trim() {
|
|
"Command" | "Super" => res.logo = true,
|
|
"Shift" => res.shift = true,
|
|
"Alt" | "Option" => res.alt = true,
|
|
"Control" => res.ctrl = true,
|
|
"None" => (),
|
|
_ => error!("Unknown modifier {:?}", modifier),
|
|
}
|
|
}
|
|
|
|
Ok(ModsWrapper(res))
|
|
}
|
|
}
|
|
|
|
deserializer.deserialize_str(ModsVisitor)
|
|
}
|
|
}
|
|
|
|
struct ActionWrapper(crate::input::Action);
|
|
|
|
impl ActionWrapper {
|
|
fn into_inner(self) -> crate::input::Action {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl<'a> de::Deserialize<'a> for ActionWrapper {
|
|
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
struct ActionVisitor;
|
|
|
|
impl<'a> Visitor<'a> for ActionVisitor {
|
|
type Value = ActionWrapper;
|
|
|
|
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str("Paste, Copy, PasteSelection, IncreaseFontSize, DecreaseFontSize, \
|
|
ResetFontSize, ScrollPageUp, ScrollPageDown, ScrollToTop, \
|
|
ScrollToBottom, ClearHistory, Hide, ClearLogNotice, SpawnNewInstance, \
|
|
None or Quit")
|
|
}
|
|
|
|
fn visit_str<E>(self, value: &str) -> ::std::result::Result<ActionWrapper, E>
|
|
where E: de::Error,
|
|
{
|
|
Ok(ActionWrapper(match value {
|
|
"Paste" => Action::Paste,
|
|
"Copy" => Action::Copy,
|
|
"PasteSelection" => Action::PasteSelection,
|
|
"IncreaseFontSize" => Action::IncreaseFontSize,
|
|
"DecreaseFontSize" => Action::DecreaseFontSize,
|
|
"ResetFontSize" => Action::ResetFontSize,
|
|
"ScrollPageUp" => Action::ScrollPageUp,
|
|
"ScrollPageDown" => Action::ScrollPageDown,
|
|
"ScrollToTop" => Action::ScrollToTop,
|
|
"ScrollToBottom" => Action::ScrollToBottom,
|
|
"ClearHistory" => Action::ClearHistory,
|
|
"Hide" => Action::Hide,
|
|
"Quit" => Action::Quit,
|
|
"ClearLogNotice" => Action::ClearLogNotice,
|
|
"SpawnNewInstance" => Action::SpawnNewInstance,
|
|
"None" => Action::None,
|
|
_ => return Err(E::invalid_value(Unexpected::Str(value), &self)),
|
|
}))
|
|
}
|
|
}
|
|
deserializer.deserialize_str(ActionVisitor)
|
|
}
|
|
}
|
|
|
|
#[serde(untagged)]
|
|
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
|
|
pub enum CommandWrapper {
|
|
Just(String),
|
|
WithArgs {
|
|
program: String,
|
|
#[serde(default)]
|
|
args: Vec<String>,
|
|
},
|
|
}
|
|
|
|
impl CommandWrapper {
|
|
pub fn program(&self) -> &str {
|
|
match self {
|
|
CommandWrapper::Just(program) => program,
|
|
CommandWrapper::WithArgs { program, .. } => program,
|
|
}
|
|
}
|
|
|
|
pub fn args(&self) -> &[String] {
|
|
match self {
|
|
CommandWrapper::Just(_) => &[],
|
|
CommandWrapper::WithArgs { args, .. } => args,
|
|
}
|
|
}
|
|
}
|
|
|
|
use crate::term::{mode, TermMode};
|
|
|
|
struct ModeWrapper {
|
|
pub mode: TermMode,
|
|
pub not_mode: TermMode,
|
|
}
|
|
|
|
impl<'a> de::Deserialize<'a> for ModeWrapper {
|
|
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
struct ModeVisitor;
|
|
|
|
impl<'a> Visitor<'a> for ModeVisitor {
|
|
type Value = ModeWrapper;
|
|
|
|
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str("Combination of AppCursor | AppKeypad, possibly with negation (~)")
|
|
}
|
|
|
|
fn visit_str<E>(self, value: &str) -> ::std::result::Result<ModeWrapper, E>
|
|
where E: de::Error,
|
|
{
|
|
let mut res = ModeWrapper {
|
|
mode: TermMode::empty(),
|
|
not_mode: TermMode::empty()
|
|
};
|
|
|
|
for modifier in value.split('|') {
|
|
match modifier.trim() {
|
|
"AppCursor" => res.mode |= mode::TermMode::APP_CURSOR,
|
|
"~AppCursor" => res.not_mode |= mode::TermMode::APP_CURSOR,
|
|
"AppKeypad" => res.mode |= mode::TermMode::APP_KEYPAD,
|
|
"~AppKeypad" => res.not_mode |= mode::TermMode::APP_KEYPAD,
|
|
_ => error!("Unknown mode {:?}", modifier),
|
|
}
|
|
}
|
|
|
|
Ok(res)
|
|
}
|
|
}
|
|
deserializer.deserialize_str(ModeVisitor)
|
|
}
|
|
}
|
|
|
|
struct MouseButton(::glutin::MouseButton);
|
|
|
|
impl MouseButton {
|
|
fn into_inner(self) -> ::glutin::MouseButton {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl<'a> de::Deserialize<'a> for MouseButton {
|
|
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
struct MouseButtonVisitor;
|
|
|
|
impl<'a> Visitor<'a> for MouseButtonVisitor {
|
|
type Value = MouseButton;
|
|
|
|
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str("Left, Right, Middle, or a number")
|
|
}
|
|
|
|
fn visit_str<E>(self, value: &str) -> ::std::result::Result<MouseButton, E>
|
|
where E: de::Error,
|
|
{
|
|
match value {
|
|
"Left" => Ok(MouseButton(::glutin::MouseButton::Left)),
|
|
"Right" => Ok(MouseButton(::glutin::MouseButton::Right)),
|
|
"Middle" => Ok(MouseButton(::glutin::MouseButton::Middle)),
|
|
_ => {
|
|
if let Ok(index) = u8::from_str(value) {
|
|
Ok(MouseButton(::glutin::MouseButton::Other(index)))
|
|
} else {
|
|
Err(E::invalid_value(Unexpected::Str(value), &self))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
deserializer.deserialize_str(MouseButtonVisitor)
|
|
}
|
|
}
|
|
|
|
/// Bindings are deserialized into a `RawBinding` before being parsed as a
|
|
/// `KeyBinding` or `MouseBinding`.
|
|
#[derive(PartialEq, Eq)]
|
|
struct RawBinding {
|
|
key: Option<Key>,
|
|
mouse: Option<::glutin::MouseButton>,
|
|
mods: ModifiersState,
|
|
mode: TermMode,
|
|
notmode: TermMode,
|
|
action: Action,
|
|
}
|
|
|
|
impl RawBinding {
|
|
fn into_mouse_binding(self) -> ::std::result::Result<MouseBinding, Self> {
|
|
if let Some(mouse) = self.mouse {
|
|
Ok(Binding {
|
|
trigger: mouse,
|
|
mods: self.mods,
|
|
action: self.action,
|
|
mode: self.mode,
|
|
notmode: self.notmode,
|
|
})
|
|
} else {
|
|
Err(self)
|
|
}
|
|
}
|
|
|
|
fn into_key_binding(self) -> ::std::result::Result<KeyBinding, Self> {
|
|
if let Some(key) = self.key {
|
|
Ok(KeyBinding {
|
|
trigger: key,
|
|
mods: self.mods,
|
|
action: self.action,
|
|
mode: self.mode,
|
|
notmode: self.notmode,
|
|
})
|
|
} else {
|
|
Err(self)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> de::Deserialize<'a> for RawBinding {
|
|
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
enum Field {
|
|
Key,
|
|
Mods,
|
|
Mode,
|
|
Action,
|
|
Chars,
|
|
Mouse,
|
|
Command,
|
|
}
|
|
|
|
impl<'a> de::Deserialize<'a> for Field {
|
|
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Field, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
struct FieldVisitor;
|
|
|
|
static FIELDS: &'static [&'static str] = &[
|
|
"key", "mods", "mode", "action", "chars", "mouse", "command",
|
|
];
|
|
|
|
impl<'a> Visitor<'a> for FieldVisitor {
|
|
type Value = Field;
|
|
|
|
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str("binding fields")
|
|
}
|
|
|
|
fn visit_str<E>(self, value: &str) -> ::std::result::Result<Field, E>
|
|
where E: de::Error,
|
|
{
|
|
match value {
|
|
"key" => Ok(Field::Key),
|
|
"mods" => Ok(Field::Mods),
|
|
"mode" => Ok(Field::Mode),
|
|
"action" => Ok(Field::Action),
|
|
"chars" => Ok(Field::Chars),
|
|
"mouse" => Ok(Field::Mouse),
|
|
"command" => Ok(Field::Command),
|
|
_ => Err(E::unknown_field(value, FIELDS)),
|
|
}
|
|
}
|
|
}
|
|
|
|
deserializer.deserialize_str(FieldVisitor)
|
|
}
|
|
}
|
|
|
|
struct RawBindingVisitor;
|
|
impl<'a> Visitor<'a> for RawBindingVisitor {
|
|
type Value = RawBinding;
|
|
|
|
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str("binding specification")
|
|
}
|
|
|
|
fn visit_map<V>(
|
|
self,
|
|
mut map: V
|
|
) -> ::std::result::Result<RawBinding, V::Error>
|
|
where V: MapAccess<'a>,
|
|
{
|
|
let mut mods: Option<ModifiersState> = None;
|
|
let mut key: Option<Key> = None;
|
|
let mut chars: Option<String> = None;
|
|
let mut action: Option<crate::input::Action> = None;
|
|
let mut mode: Option<TermMode> = None;
|
|
let mut not_mode: Option<TermMode> = None;
|
|
let mut mouse: Option<::glutin::MouseButton> = None;
|
|
let mut command: Option<CommandWrapper> = None;
|
|
|
|
use ::serde::de::Error;
|
|
|
|
while let Some(struct_key) = map.next_key::<Field>()? {
|
|
match struct_key {
|
|
Field::Key => {
|
|
if key.is_some() {
|
|
return Err(<V::Error as Error>::duplicate_field("key"));
|
|
}
|
|
|
|
let val = map.next_value::<serde_yaml::Value>()?;
|
|
if val.is_u64() {
|
|
let scancode = val.as_u64().unwrap();
|
|
if scancode > u64::from(::std::u32::MAX) {
|
|
return Err(<V::Error as Error>::custom(format!(
|
|
"Invalid key binding, scancode too big: {}",
|
|
scancode
|
|
)));
|
|
}
|
|
key = Some(Key::Scancode(scancode as u32));
|
|
} else {
|
|
let k = Key::deserialize(val)
|
|
.map_err(V::Error::custom)?;
|
|
key = Some(k);
|
|
}
|
|
},
|
|
Field::Mods => {
|
|
if mods.is_some() {
|
|
return Err(<V::Error as Error>::duplicate_field("mods"));
|
|
}
|
|
|
|
mods = Some(map.next_value::<ModsWrapper>()?.into_inner());
|
|
},
|
|
Field::Mode => {
|
|
if mode.is_some() {
|
|
return Err(<V::Error as Error>::duplicate_field("mode"));
|
|
}
|
|
|
|
let mode_deserializer = map.next_value::<ModeWrapper>()?;
|
|
mode = Some(mode_deserializer.mode);
|
|
not_mode = Some(mode_deserializer.not_mode);
|
|
},
|
|
Field::Action => {
|
|
if action.is_some() {
|
|
return Err(<V::Error as Error>::duplicate_field("action"));
|
|
}
|
|
|
|
action = Some(map.next_value::<ActionWrapper>()?.into_inner());
|
|
},
|
|
Field::Chars => {
|
|
if chars.is_some() {
|
|
return Err(<V::Error as Error>::duplicate_field("chars"));
|
|
}
|
|
|
|
chars = Some(map.next_value()?);
|
|
},
|
|
Field::Mouse => {
|
|
if chars.is_some() {
|
|
return Err(<V::Error as Error>::duplicate_field("mouse"));
|
|
}
|
|
|
|
mouse = Some(map.next_value::<MouseButton>()?.into_inner());
|
|
},
|
|
Field::Command => {
|
|
if command.is_some() {
|
|
return Err(<V::Error as Error>::duplicate_field("command"));
|
|
}
|
|
|
|
command = Some(map.next_value::<CommandWrapper>()?);
|
|
},
|
|
}
|
|
}
|
|
|
|
let action = match (action, chars, command) {
|
|
(Some(action), None, None) => action,
|
|
(None, Some(chars), None) => Action::Esc(chars),
|
|
(None, None, Some(cmd)) => {
|
|
match cmd {
|
|
CommandWrapper::Just(program) => {
|
|
Action::Command(program, vec![])
|
|
},
|
|
CommandWrapper::WithArgs { program, args } => {
|
|
Action::Command(program, args)
|
|
},
|
|
}
|
|
},
|
|
(None, None, None) => return Err(V::Error::custom("must specify chars, action or command")),
|
|
_ => return Err(V::Error::custom("must specify only chars, action or command")),
|
|
};
|
|
|
|
let mode = mode.unwrap_or_else(TermMode::empty);
|
|
let not_mode = not_mode.unwrap_or_else(TermMode::empty);
|
|
let mods = mods.unwrap_or_else(ModifiersState::default);
|
|
|
|
if mouse.is_none() && key.is_none() {
|
|
return Err(V::Error::custom("bindings require mouse button or key"));
|
|
}
|
|
|
|
Ok(RawBinding {
|
|
mode,
|
|
notmode: not_mode,
|
|
action,
|
|
key,
|
|
mouse,
|
|
mods,
|
|
})
|
|
}
|
|
}
|
|
|
|
const FIELDS: &[&str] = &[
|
|
"key", "mods", "mode", "action", "chars", "mouse", "command",
|
|
];
|
|
|
|
deserializer.deserialize_struct("RawBinding", FIELDS, RawBindingVisitor)
|
|
}
|
|
}
|
|
|
|
|
|
impl<'a> de::Deserialize<'a> for Alpha {
|
|
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
let value = f32::deserialize(deserializer)?;
|
|
Ok(Alpha::new(value))
|
|
}
|
|
}
|
|
|
|
impl<'a> de::Deserialize<'a> for MouseBinding {
|
|
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
let raw = RawBinding::deserialize(deserializer)?;
|
|
raw.into_mouse_binding()
|
|
.map_err(|_| D::Error::custom("expected mouse binding"))
|
|
}
|
|
}
|
|
|
|
impl<'a> de::Deserialize<'a> for KeyBinding {
|
|
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
let raw = RawBinding::deserialize(deserializer)?;
|
|
raw.into_key_binding()
|
|
.map_err(|_| D::Error::custom("expected key binding"))
|
|
}
|
|
}
|
|
|
|
/// Errors occurring during config loading
|
|
#[derive(Debug)]
|
|
pub enum Error {
|
|
/// Config file not found
|
|
NotFound,
|
|
|
|
/// Config file empty
|
|
Empty,
|
|
|
|
/// 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),
|
|
}
|
|
|
|
#[serde(default)]
|
|
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
|
pub struct Colors {
|
|
#[serde(deserialize_with = "failure_default")]
|
|
pub primary: PrimaryColors,
|
|
#[serde(deserialize_with = "failure_default")]
|
|
pub cursor: CursorColors,
|
|
#[serde(deserialize_with = "deserialize_normal_colors")]
|
|
pub normal: AnsiColors,
|
|
#[serde(deserialize_with = "deserialize_bright_colors")]
|
|
pub bright: AnsiColors,
|
|
#[serde(deserialize_with = "failure_default")]
|
|
pub dim: Option<AnsiColors>,
|
|
#[serde(deserialize_with = "failure_default_vec")]
|
|
pub indexed_colors: Vec<IndexedColor>,
|
|
}
|
|
|
|
impl Default for Colors {
|
|
fn default() -> Colors {
|
|
Colors {
|
|
primary: Default::default(),
|
|
cursor: Default::default(),
|
|
normal: default_normal_colors(),
|
|
bright: default_bright_colors(),
|
|
dim: Default::default(),
|
|
indexed_colors: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn default_normal_colors() -> AnsiColors {
|
|
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: 0xea, g: 0xea, b: 0xea},
|
|
}
|
|
}
|
|
|
|
fn default_bright_colors() -> AnsiColors {
|
|
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: 0xff, g: 0xff, b: 0xff},
|
|
}
|
|
}
|
|
|
|
fn deserialize_normal_colors<'a, D>(deserializer: D) -> ::std::result::Result<AnsiColors, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
match AnsiColors::deserialize(deserializer) {
|
|
Ok(escape_chars) => Ok(escape_chars),
|
|
Err(err) => {
|
|
error!("Problem with config: {}; using default value", err);
|
|
Ok(default_normal_colors())
|
|
},
|
|
}
|
|
}
|
|
|
|
fn deserialize_bright_colors<'a, D>(deserializer: D) -> ::std::result::Result<AnsiColors, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
match AnsiColors::deserialize(deserializer) {
|
|
Ok(escape_chars) => Ok(escape_chars),
|
|
Err(err) => {
|
|
error!("Problem with config: {}; using default value", err);
|
|
Ok(default_bright_colors())
|
|
},
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
|
pub struct IndexedColor {
|
|
#[serde(deserialize_with = "deserialize_color_index")]
|
|
pub index: u8,
|
|
#[serde(deserialize_with = "rgb_from_hex")]
|
|
pub color: Rgb,
|
|
}
|
|
|
|
fn deserialize_color_index<'a, D>(deserializer: D) -> ::std::result::Result<u8, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
match u8::deserialize(deserializer) {
|
|
Ok(index) => {
|
|
if index < 16 {
|
|
error!(
|
|
"Problem with config: indexed_color's index is {}, \
|
|
but a value bigger than 15 was expected; \
|
|
ignoring setting",
|
|
index
|
|
);
|
|
|
|
// Return value out of range to ignore this color
|
|
Ok(0)
|
|
} else {
|
|
Ok(index)
|
|
}
|
|
},
|
|
Err(err) => {
|
|
error!("Problem with config: {}; ignoring setting", err);
|
|
|
|
// Return value out of range to ignore this color
|
|
Ok(0)
|
|
},
|
|
}
|
|
}
|
|
|
|
#[serde(default)]
|
|
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq)]
|
|
pub struct Cursor {
|
|
#[serde(deserialize_with = "failure_default")]
|
|
pub style: CursorStyle,
|
|
#[serde(deserialize_with = "deserialize_true_bool")]
|
|
pub unfocused_hollow: bool,
|
|
}
|
|
|
|
impl Default for Cursor {
|
|
fn default() -> Self {
|
|
Self {
|
|
style: Default::default(),
|
|
unfocused_hollow: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[serde(default)]
|
|
#[derive(Debug, Copy, Clone, Default, Deserialize, PartialEq, Eq)]
|
|
pub struct CursorColors {
|
|
#[serde(deserialize_with = "deserialize_optional_color")]
|
|
pub text: Option<Rgb>,
|
|
#[serde(deserialize_with = "deserialize_optional_color")]
|
|
pub cursor: Option<Rgb>,
|
|
}
|
|
|
|
#[serde(default)]
|
|
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
|
pub struct PrimaryColors {
|
|
#[serde(deserialize_with = "rgb_from_hex")]
|
|
pub background: Rgb,
|
|
#[serde(deserialize_with = "rgb_from_hex")]
|
|
pub foreground: Rgb,
|
|
#[serde(deserialize_with = "deserialize_optional_color")]
|
|
pub bright_foreground: Option<Rgb>,
|
|
#[serde(deserialize_with = "deserialize_optional_color")]
|
|
pub dim_foreground: Option<Rgb>,
|
|
}
|
|
|
|
impl Default for PrimaryColors {
|
|
fn default() -> Self {
|
|
PrimaryColors {
|
|
background: default_background(),
|
|
foreground: default_foreground(),
|
|
bright_foreground: Default::default(),
|
|
dim_foreground: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn deserialize_optional_color<'a, D>(deserializer: D) -> ::std::result::Result<Option<Rgb>, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
match Option::deserialize(deserializer) {
|
|
Ok(Some(color)) => {
|
|
let color: serde_yaml::Value = color;
|
|
Ok(Some(rgb_from_hex(color).unwrap()))
|
|
},
|
|
Ok(None) => Ok(None),
|
|
Err(err) => {
|
|
error!("Problem with config: {}; using standard foreground color", err);
|
|
Ok(None)
|
|
},
|
|
}
|
|
}
|
|
|
|
fn default_background() -> Rgb {
|
|
Rgb { r: 0, g: 0, b: 0 }
|
|
}
|
|
|
|
fn default_foreground() -> Rgb {
|
|
Rgb { r: 0xea, g: 0xea, b: 0xea }
|
|
}
|
|
|
|
/// The 8-colors sections of config
|
|
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
|
pub struct AnsiColors {
|
|
#[serde(deserialize_with = "rgb_from_hex")]
|
|
pub black: Rgb,
|
|
#[serde(deserialize_with = "rgb_from_hex")]
|
|
pub red: Rgb,
|
|
#[serde(deserialize_with = "rgb_from_hex")]
|
|
pub green: Rgb,
|
|
#[serde(deserialize_with = "rgb_from_hex")]
|
|
pub yellow: Rgb,
|
|
#[serde(deserialize_with = "rgb_from_hex")]
|
|
pub blue: Rgb,
|
|
#[serde(deserialize_with = "rgb_from_hex")]
|
|
pub magenta: Rgb,
|
|
#[serde(deserialize_with = "rgb_from_hex")]
|
|
pub cyan: Rgb,
|
|
#[serde(deserialize_with = "rgb_from_hex")]
|
|
pub white: Rgb,
|
|
}
|
|
|
|
/// Deserialize an Rgb from a hex string
|
|
///
|
|
/// This is *not* the deserialize impl for Rgb since we want a symmetric
|
|
/// serialize/deserialize impl for ref tests.
|
|
fn rgb_from_hex<'a, D>(deserializer: D) -> ::std::result::Result<Rgb, D::Error>
|
|
where D: de::Deserializer<'a>
|
|
{
|
|
struct RgbVisitor;
|
|
|
|
impl<'a> Visitor<'a> for RgbVisitor {
|
|
type Value = Rgb;
|
|
|
|
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str("hex color like 0xff00ff")
|
|
}
|
|
|
|
fn visit_str<E>(self, value: &str) -> ::std::result::Result<Rgb, E>
|
|
where E: ::serde::de::Error
|
|
{
|
|
Rgb::from_str(&value[..])
|
|
.map_err(|_| E::custom("failed to parse rgb; expected hex color like 0xff00ff"))
|
|
}
|
|
}
|
|
|
|
let rgb = deserializer.deserialize_str(RgbVisitor);
|
|
|
|
// Use #ff00ff as fallback color
|
|
match rgb {
|
|
Ok(rgb) => Ok(rgb),
|
|
Err(err) => {
|
|
error!("Problem with config: {}; using color #ff00ff", err);
|
|
Ok(Rgb { r: 255, g: 0, b: 255 })
|
|
},
|
|
}
|
|
}
|
|
|
|
impl FromStr for Rgb {
|
|
type Err = ();
|
|
fn from_str(s: &str) -> ::std::result::Result<Rgb, ()> {
|
|
let mut chars = s.chars();
|
|
let mut rgb = Rgb::default();
|
|
|
|
macro_rules! component {
|
|
($($c:ident),*) => {
|
|
$(
|
|
match chars.next().and_then(|c| c.to_digit(16)) {
|
|
Some(val) => rgb.$c = (val as u8) << 4,
|
|
None => return Err(())
|
|
}
|
|
|
|
match chars.next().and_then(|c| c.to_digit(16)) {
|
|
Some(val) => rgb.$c |= val as u8,
|
|
None => return Err(())
|
|
}
|
|
)*
|
|
}
|
|
}
|
|
|
|
match chars.next() {
|
|
Some('0') => if chars.next() != Some('x') { return Err(()); },
|
|
Some('#') => (),
|
|
_ => return Err(()),
|
|
}
|
|
|
|
component!(r, g, b);
|
|
|
|
Ok(rgb)
|
|
}
|
|
}
|
|
|
|
impl ::std::error::Error for Error {
|
|
fn cause(&self) -> Option<&dyn (::std::error::Error)> {
|
|
match *self {
|
|
Error::NotFound | Error::Empty => 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 => "Couldn't locate config file",
|
|
Error::Empty => "Empty 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 | Error::Empty => write!(f, "{}", ::std::error::Error::description(self)),
|
|
Error::ReadingEnvHome(ref err) => {
|
|
write!(f, "Couldn't 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<env::VarError> for Error {
|
|
fn from(val: env::VarError) -> Error {
|
|
Error::ReadingEnvHome(val)
|
|
}
|
|
}
|
|
|
|
impl From<io::Error> for Error {
|
|
fn from(val: io::Error) -> Error {
|
|
if val.kind() == io::ErrorKind::NotFound {
|
|
Error::NotFound
|
|
} else {
|
|
Error::Io(val)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<serde_yaml::Error> for Error {
|
|
fn from(val: serde_yaml::Error) -> Error {
|
|
Error::Yaml(val)
|
|
}
|
|
}
|
|
|
|
/// Result from config loading
|
|
pub type Result<T> = ::std::result::Result<T, Error>;
|
|
|
|
impl Config {
|
|
/// Get the location of the first found default config file paths
|
|
/// according to the following order:
|
|
///
|
|
/// 1. $XDG_CONFIG_HOME/alacritty/alacritty.yml
|
|
/// 2. $XDG_CONFIG_HOME/alacritty.yml
|
|
/// 3. $HOME/.config/alacritty/alacritty.yml
|
|
/// 4. $HOME/.alacritty.yml
|
|
#[cfg(not(windows))]
|
|
pub fn installed_config<'a>() -> Option<Cow<'a, Path>> {
|
|
// Try using XDG location by default
|
|
::xdg::BaseDirectories::with_prefix("alacritty")
|
|
.ok()
|
|
.and_then(|xdg| xdg.find_config_file("alacritty.yml"))
|
|
.or_else(|| {
|
|
::xdg::BaseDirectories::new().ok().and_then(|fallback| {
|
|
fallback.find_config_file("alacritty.yml")
|
|
})
|
|
})
|
|
.or_else(|| {
|
|
if let Ok(home) = env::var("HOME") {
|
|
// Fallback path: $HOME/.config/alacritty/alacritty.yml
|
|
let fallback = PathBuf::from(&home).join(".config/alacritty/alacritty.yml");
|
|
if fallback.exists() {
|
|
return Some(fallback);
|
|
}
|
|
// Fallback path: $HOME/.alacritty.yml
|
|
let fallback = PathBuf::from(&home).join(".alacritty.yml");
|
|
if fallback.exists() {
|
|
return Some(fallback);
|
|
}
|
|
}
|
|
None
|
|
})
|
|
.map(|path| path.into())
|
|
}
|
|
|
|
// TODO: Remove old configuration location warning (Deprecated 03/12/2018)
|
|
#[cfg(windows)]
|
|
pub fn installed_config<'a>() -> Option<Cow<'a, Path>> {
|
|
let old = dirs::home_dir()
|
|
.map(|path| path.join("alacritty.yml"));
|
|
let new = dirs::config_dir()
|
|
.map(|path| path.join("alacritty\\alacritty.yml"));
|
|
|
|
if let Some(old_path) = old.as_ref().filter(|old| old.exists()) {
|
|
warn!(
|
|
"Found configuration at: {}; this file should be moved to the new location: {}",
|
|
old_path.to_string_lossy(),
|
|
new.as_ref().map(|new| new.to_string_lossy()).unwrap(),
|
|
);
|
|
|
|
old.map(Cow::from)
|
|
} else {
|
|
new.filter(|new| new.exists()).map(Cow::from)
|
|
}
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn write_defaults() -> io::Result<Cow<'static, Path>> {
|
|
let path = xdg::BaseDirectories::with_prefix("alacritty")
|
|
.map_err(|err| io::Error::new(io::ErrorKind::NotFound, err.to_string().as_str()))
|
|
.and_then(|p| p.place_config_file("alacritty.yml"))?;
|
|
|
|
File::create(&path)?.write_all(DEFAULT_ALACRITTY_CONFIG.as_bytes())?;
|
|
|
|
Ok(path.into())
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
pub fn write_defaults() -> io::Result<Cow<'static, Path>> {
|
|
let mut path = dirs::config_dir()
|
|
.ok_or_else(|| {
|
|
io::Error::new(io::ErrorKind::NotFound, "Couldn't find profile directory")
|
|
}
|
|
)?;
|
|
|
|
path = path.join("alacritty/alacritty.yml");
|
|
|
|
fs::create_dir_all(path.parent().unwrap())?;
|
|
|
|
File::create(&path)?.write_all(DEFAULT_ALACRITTY_CONFIG.as_bytes())?;
|
|
|
|
Ok(path.into())
|
|
}
|
|
|
|
/// Get list of colors
|
|
///
|
|
/// The ordering returned here is expected by the terminal. Colors are simply indexed in this
|
|
/// array for performance.
|
|
pub fn colors(&self) -> &Colors {
|
|
&self.colors
|
|
}
|
|
|
|
#[inline]
|
|
pub fn background_opacity(&self) -> Alpha {
|
|
self.background_opacity
|
|
}
|
|
|
|
pub fn key_bindings(&self) -> &[KeyBinding] {
|
|
&self.key_bindings[..]
|
|
}
|
|
|
|
pub fn mouse_bindings(&self) -> &[MouseBinding] {
|
|
&self.mouse_bindings[..]
|
|
}
|
|
|
|
pub fn mouse(&self) -> &Mouse {
|
|
&self.mouse
|
|
}
|
|
|
|
pub fn selection(&self) -> &Selection {
|
|
&self.selection
|
|
}
|
|
|
|
pub fn tabspaces(&self) -> usize {
|
|
self.tabspaces
|
|
}
|
|
|
|
pub fn padding(&self) -> &Delta<u8> {
|
|
self.padding.as_ref()
|
|
.unwrap_or(&self.window.padding)
|
|
}
|
|
|
|
#[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 window dimensions
|
|
#[inline]
|
|
pub fn dimensions(&self) -> Dimensions {
|
|
self.dimensions.unwrap_or(self.window.dimensions)
|
|
}
|
|
|
|
/// Get window config
|
|
#[inline]
|
|
pub fn window(&self) -> &WindowConfig {
|
|
&self.window
|
|
}
|
|
|
|
/// Get visual bell config
|
|
#[inline]
|
|
pub fn visual_bell(&self) -> &VisualBellConfig {
|
|
&self.visual_bell
|
|
}
|
|
|
|
/// Should show render timer
|
|
#[inline]
|
|
pub fn render_timer(&self) -> bool {
|
|
self.render_timer
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
#[inline]
|
|
pub fn use_thin_strokes(&self) -> bool {
|
|
self.font.use_thin_strokes
|
|
}
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
#[inline]
|
|
pub fn use_thin_strokes(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
pub fn path(&self) -> Option<&Path> {
|
|
self.config_path
|
|
.as_ref()
|
|
.map(|p| p.as_path())
|
|
}
|
|
|
|
pub fn shell(&self) -> Option<&Shell<'_>> {
|
|
self.shell.as_ref()
|
|
}
|
|
|
|
pub fn env(&self) -> &HashMap<String, String> {
|
|
&self.env
|
|
}
|
|
|
|
/// Should hide mouse cursor when typing
|
|
#[inline]
|
|
pub fn hide_mouse_when_typing(&self) -> bool {
|
|
self.hide_cursor_when_typing.unwrap_or(self.mouse.hide_when_typing)
|
|
}
|
|
|
|
/// Style of the cursor
|
|
#[inline]
|
|
pub fn cursor_style(&self) -> CursorStyle {
|
|
self.cursor_style.unwrap_or(self.cursor.style)
|
|
}
|
|
|
|
/// Use hollow block cursor when unfocused
|
|
#[inline]
|
|
pub fn unfocused_hollow_cursor(&self) -> bool {
|
|
self.unfocused_hollow_cursor.unwrap_or(self.cursor.unfocused_hollow)
|
|
}
|
|
|
|
/// Live config reload
|
|
#[inline]
|
|
pub fn live_config_reload(&self) -> bool {
|
|
self.live_config_reload
|
|
}
|
|
|
|
#[inline]
|
|
pub fn dynamic_title(&self) -> bool {
|
|
self.dynamic_title
|
|
}
|
|
|
|
/// Scrolling settings
|
|
#[inline]
|
|
pub fn scrolling(&self) -> Scrolling {
|
|
self.scrolling
|
|
}
|
|
|
|
/// Cursor foreground color
|
|
#[inline]
|
|
pub fn cursor_text_color(&self) -> Option<Color> {
|
|
self.colors.cursor.text.map(|_| Color::Named(NamedColor::CursorText))
|
|
}
|
|
|
|
/// Cursor background color
|
|
#[inline]
|
|
pub fn cursor_cursor_color(&self) -> Option<Color> {
|
|
self.colors.cursor.cursor.map(|_| Color::Named(NamedColor::Cursor))
|
|
}
|
|
|
|
/// Enable experimental conpty backend (Windows only)
|
|
#[cfg(windows)]
|
|
#[inline]
|
|
pub fn enable_experimental_conpty_backend(&self) -> bool {
|
|
self.enable_experimental_conpty_backend
|
|
}
|
|
|
|
/// Send escape sequences using the alt key
|
|
#[inline]
|
|
pub fn alt_send_esc(&self) -> bool {
|
|
self.alt_send_esc
|
|
}
|
|
|
|
// Update the history size, used in ref tests
|
|
pub fn set_history(&mut self, history: u32) {
|
|
self.scrolling.history = history;
|
|
}
|
|
|
|
/// Keep the log file after quitting Alacritty
|
|
#[inline]
|
|
pub fn persistent_logging(&self) -> bool {
|
|
self.persistent_logging
|
|
}
|
|
|
|
pub fn load_from<P: Into<PathBuf>>(path: P) -> Result<Config> {
|
|
let path = path.into();
|
|
let raw = Config::read_file(path.as_path())?;
|
|
let mut config: Config = serde_yaml::from_str(&raw)?;
|
|
config.config_path = Some(path);
|
|
config.print_deprecation_warnings();
|
|
|
|
Ok(config)
|
|
}
|
|
|
|
/// Overrides the `dynamic_title` configuration based on `--title`.
|
|
pub fn update_dynamic_title(mut self, options: &Options) -> Self {
|
|
if options.title.is_some() {
|
|
self.dynamic_title = false;
|
|
}
|
|
self
|
|
}
|
|
|
|
fn read_file<P: AsRef<Path>>(path: P) -> Result<String> {
|
|
let mut f = fs::File::open(path)?;
|
|
let mut contents = String::new();
|
|
f.read_to_string(&mut contents)?;
|
|
if contents.is_empty() {
|
|
return Err(Error::Empty);
|
|
}
|
|
|
|
Ok(contents)
|
|
}
|
|
|
|
fn print_deprecation_warnings(&mut self) {
|
|
if self.dimensions.is_some() {
|
|
warn!("Config dimensions is deprecated; \
|
|
please use window.dimensions instead");
|
|
}
|
|
|
|
if self.padding.is_some() {
|
|
warn!("Config padding is deprecated; \
|
|
please use window.padding instead");
|
|
}
|
|
|
|
if self.mouse.faux_scrollback_lines.is_some() {
|
|
warn!("Config mouse.faux_scrollback_lines is deprecated; \
|
|
please use mouse.faux_scrolling_lines instead");
|
|
}
|
|
|
|
if let Some(custom_cursor_colors) = self.custom_cursor_colors {
|
|
warn!("Config custom_cursor_colors is deprecated");
|
|
|
|
if !custom_cursor_colors {
|
|
self.colors.cursor.cursor = None;
|
|
self.colors.cursor.text = None;
|
|
}
|
|
}
|
|
|
|
if self.cursor_style.is_some() {
|
|
warn!("Config cursor_style is deprecated; \
|
|
please use cursor.style instead");
|
|
}
|
|
|
|
if self.hide_cursor_when_typing.is_some() {
|
|
warn!("Config hide_cursor_when_typing is deprecated; \
|
|
please use mouse.hide_when_typing instead");
|
|
}
|
|
|
|
if self.unfocused_hollow_cursor.is_some() {
|
|
warn!("Config unfocused_hollow_cursor is deprecated; \
|
|
please use cursor.unfocused_hollow instead");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Window Dimensions
|
|
///
|
|
/// Newtype to avoid passing values incorrectly
|
|
#[serde(default)]
|
|
#[derive(Default, Debug, Copy, Clone, Deserialize, PartialEq, Eq)]
|
|
pub struct Dimensions {
|
|
/// Window width in character columns
|
|
#[serde(deserialize_with = "failure_default")]
|
|
columns: Column,
|
|
|
|
/// Window Height in character lines
|
|
#[serde(deserialize_with = "failure_default")]
|
|
lines: Line,
|
|
}
|
|
|
|
impl Dimensions {
|
|
pub fn new(columns: Column, lines: Line) -> Self {
|
|
Dimensions {
|
|
columns,
|
|
lines,
|
|
}
|
|
}
|
|
|
|
/// Get lines
|
|
#[inline]
|
|
pub fn lines_u32(&self) -> u32 {
|
|
self.lines.0 as u32
|
|
}
|
|
|
|
/// Get columns
|
|
#[inline]
|
|
pub fn columns_u32(&self) -> u32 {
|
|
self.columns.0 as u32
|
|
}
|
|
}
|
|
|
|
/// A delta for a point in a 2 dimensional plane
|
|
#[serde(default, bound(deserialize = "T: Deserialize<'de> + Default"))]
|
|
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
|
|
pub struct Delta<T: Default + PartialEq + Eq> {
|
|
/// Horizontal change
|
|
#[serde(deserialize_with = "failure_default")]
|
|
pub x: T,
|
|
/// Vertical change
|
|
#[serde(deserialize_with = "failure_default")]
|
|
pub y: T,
|
|
}
|
|
|
|
trait DeserializeSize : Sized {
|
|
fn deserialize<'a, D>(_: D) -> ::std::result::Result<Self, D::Error>
|
|
where D: serde::de::Deserializer<'a>;
|
|
}
|
|
|
|
impl DeserializeSize for Size {
|
|
fn deserialize<'a, D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
|
|
where D: serde::de::Deserializer<'a>
|
|
{
|
|
use std::marker::PhantomData;
|
|
|
|
struct NumVisitor<__D> {
|
|
_marker: PhantomData<__D>,
|
|
}
|
|
|
|
impl<'a, __D> Visitor<'a> for NumVisitor<__D>
|
|
where __D: serde::de::Deserializer<'a>
|
|
{
|
|
type Value = f64;
|
|
|
|
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str("f64 or u64")
|
|
}
|
|
|
|
fn visit_f64<E>(self, value: f64) -> ::std::result::Result<Self::Value, E>
|
|
where E: ::serde::de::Error
|
|
{
|
|
Ok(value)
|
|
}
|
|
|
|
fn visit_u64<E>(self, value: u64) -> ::std::result::Result<Self::Value, E>
|
|
where E: ::serde::de::Error
|
|
{
|
|
Ok(value as f64)
|
|
}
|
|
}
|
|
|
|
let size = deserializer
|
|
.deserialize_any(NumVisitor::<D>{ _marker: PhantomData })
|
|
.map(|v| Size::new(v as _));
|
|
|
|
// Use default font size as fallback
|
|
match size {
|
|
Ok(size) => Ok(size),
|
|
Err(err) => {
|
|
let size = default_font_size();
|
|
error!("Problem with config: {}; using size {}", err, size.as_f32_pts());
|
|
Ok(size)
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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.
|
|
#[serde(default)]
|
|
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
|
|
pub struct Font {
|
|
/// Normal font face
|
|
#[serde(deserialize_with = "failure_default")]
|
|
normal: FontDescription,
|
|
|
|
/// Bold font face
|
|
#[serde(deserialize_with = "failure_default")]
|
|
italic: SecondaryFontDescription,
|
|
|
|
/// Italic font face
|
|
#[serde(deserialize_with = "failure_default")]
|
|
bold: SecondaryFontDescription,
|
|
|
|
/// Font size in points
|
|
#[serde(deserialize_with = "DeserializeSize::deserialize")]
|
|
pub size: Size,
|
|
|
|
/// Extra spacing per character
|
|
#[serde(deserialize_with = "failure_default")]
|
|
offset: Delta<i8>,
|
|
|
|
/// Glyph offset within character cell
|
|
#[serde(deserialize_with = "failure_default")]
|
|
glyph_offset: Delta<i8>,
|
|
|
|
#[cfg(target_os = "macos")]
|
|
#[serde(deserialize_with = "deserialize_true_bool")]
|
|
use_thin_strokes: bool,
|
|
|
|
// TODO: Deprecated
|
|
#[serde(deserialize_with = "deserialize_scale_with_dpi")]
|
|
scale_with_dpi: Option<()>,
|
|
}
|
|
|
|
impl Default for Font {
|
|
fn default() -> Font {
|
|
Font {
|
|
#[cfg(target_os = "macos")]
|
|
use_thin_strokes: true,
|
|
size: default_font_size(),
|
|
normal: Default::default(),
|
|
bold: Default::default(),
|
|
italic: Default::default(),
|
|
scale_with_dpi: Default::default(),
|
|
glyph_offset: Default::default(),
|
|
offset: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Font {
|
|
/// Get the font size in points
|
|
#[inline]
|
|
pub fn size(&self) -> Size {
|
|
self.size
|
|
}
|
|
|
|
/// Get offsets to font metrics
|
|
#[inline]
|
|
pub fn offset(&self) -> &Delta<i8> {
|
|
&self.offset
|
|
}
|
|
|
|
/// Get cell offsets for glyphs
|
|
#[inline]
|
|
pub fn glyph_offset(&self) -> &Delta<i8> {
|
|
&self.glyph_offset
|
|
}
|
|
|
|
/// Get a font clone with a size modification
|
|
pub fn with_size(self, size: Size) -> Font {
|
|
Font {
|
|
size,
|
|
.. self
|
|
}
|
|
}
|
|
|
|
// Get normal font description
|
|
pub fn normal(&self) -> &FontDescription {
|
|
&self.normal
|
|
}
|
|
|
|
// Get italic font description
|
|
pub fn italic(&self) -> FontDescription {
|
|
self.italic.desc(&self.normal)
|
|
}
|
|
|
|
// Get bold font description
|
|
pub fn bold(&self) -> FontDescription {
|
|
self.bold.desc(&self.normal)
|
|
}
|
|
}
|
|
|
|
fn default_font_size() -> Size {
|
|
Size::new(11.)
|
|
}
|
|
|
|
fn deserialize_scale_with_dpi<'a, D>(deserializer: D) -> ::std::result::Result<Option<()>, D::Error>
|
|
where
|
|
D: de::Deserializer<'a>,
|
|
{
|
|
// This is necessary in order to get serde to complete deserialization of the configuration
|
|
let _ignored = bool::deserialize(deserializer);
|
|
error!("The scale_with_dpi setting has been removed, \
|
|
on X11 the WINIT_HIDPI_FACTOR environment variable can be used instead.");
|
|
Ok(None)
|
|
}
|
|
|
|
/// Description of the normal font
|
|
#[serde(default)]
|
|
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
|
|
pub struct FontDescription {
|
|
#[serde(deserialize_with = "failure_default")]
|
|
pub family: String,
|
|
#[serde(deserialize_with = "failure_default")]
|
|
pub style: Option<String>,
|
|
}
|
|
|
|
impl Default for FontDescription {
|
|
fn default() -> FontDescription {
|
|
FontDescription {
|
|
#[cfg(not(any(target_os = "macos", windows)))]
|
|
family: "monospace".into(),
|
|
#[cfg(target_os = "macos")]
|
|
family: "Menlo".into(),
|
|
#[cfg(windows)]
|
|
family: "Consolas".into(),
|
|
style: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Description of the italic and bold font
|
|
#[serde(default)]
|
|
#[derive(Debug, Default, Deserialize, Clone, PartialEq, Eq)]
|
|
pub struct SecondaryFontDescription {
|
|
#[serde(deserialize_with = "failure_default")]
|
|
family: Option<String>,
|
|
#[serde(deserialize_with = "failure_default")]
|
|
style: Option<String>,
|
|
}
|
|
|
|
impl SecondaryFontDescription {
|
|
pub fn desc(&self, fallback: &FontDescription) -> FontDescription {
|
|
FontDescription {
|
|
family: self.family.clone().unwrap_or_else(|| fallback.family.clone()),
|
|
style: self.style.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Monitor {
|
|
_thread: ::std::thread::JoinHandle<()>,
|
|
rx: mpsc::Receiver<Config>,
|
|
}
|
|
|
|
pub trait OnConfigReload {
|
|
fn on_config_reload(&mut self);
|
|
}
|
|
|
|
impl OnConfigReload for crate::display::Notifier {
|
|
fn on_config_reload(&mut self) {
|
|
self.notify();
|
|
}
|
|
}
|
|
|
|
impl Monitor {
|
|
/// Get pending config changes
|
|
pub fn pending_config(&self) -> Option<Config> {
|
|
let mut config = None;
|
|
while let Ok(new) = self.rx.try_recv() {
|
|
config = Some(new);
|
|
}
|
|
|
|
config
|
|
}
|
|
pub fn new<H, P>(path: P, mut handler: H) -> Monitor
|
|
where H: OnConfigReload + Send + 'static,
|
|
P: Into<PathBuf>
|
|
{
|
|
let path = path.into();
|
|
|
|
let (config_tx, config_rx) = mpsc::channel();
|
|
|
|
Monitor {
|
|
_thread: crate::util::thread::spawn_named("config watcher", move || {
|
|
let (tx, rx) = mpsc::channel();
|
|
// The Duration argument is a debouncing period.
|
|
let mut watcher = watcher(tx, Duration::from_millis(10))
|
|
.expect("Unable to spawn file watcher");
|
|
let config_path = ::std::fs::canonicalize(path)
|
|
.expect("canonicalize config path");
|
|
|
|
// Get directory of config
|
|
let mut parent = config_path.clone();
|
|
parent.pop();
|
|
|
|
// Watch directory
|
|
watcher.watch(&parent, RecursiveMode::NonRecursive)
|
|
.expect("watch alacritty.yml dir");
|
|
|
|
loop {
|
|
match rx.recv().expect("watcher event") {
|
|
DebouncedEvent::Rename(_, _) => continue,
|
|
DebouncedEvent::Write(path) | DebouncedEvent::Create(path)
|
|
| DebouncedEvent::Chmod(path) => {
|
|
// Reload file
|
|
if path == config_path {
|
|
match Config::load_from(path) {
|
|
Ok(config) => {
|
|
let _ = config_tx.send(config);
|
|
handler.on_config_reload();
|
|
},
|
|
Err(err) => error!("Ignoring invalid config: {}", err),
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}),
|
|
rx: config_rx,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
|
pub enum Key {
|
|
Scancode(u32),
|
|
Key1,
|
|
Key2,
|
|
Key3,
|
|
Key4,
|
|
Key5,
|
|
Key6,
|
|
Key7,
|
|
Key8,
|
|
Key9,
|
|
Key0,
|
|
A,
|
|
B,
|
|
C,
|
|
D,
|
|
E,
|
|
F,
|
|
G,
|
|
H,
|
|
I,
|
|
J,
|
|
K,
|
|
L,
|
|
M,
|
|
N,
|
|
O,
|
|
P,
|
|
Q,
|
|
R,
|
|
S,
|
|
T,
|
|
U,
|
|
V,
|
|
W,
|
|
X,
|
|
Y,
|
|
Z,
|
|
Escape,
|
|
F1,
|
|
F2,
|
|
F3,
|
|
F4,
|
|
F5,
|
|
F6,
|
|
F7,
|
|
F8,
|
|
F9,
|
|
F10,
|
|
F11,
|
|
F12,
|
|
F13,
|
|
F14,
|
|
F15,
|
|
F16,
|
|
F17,
|
|
F18,
|
|
F19,
|
|
F20,
|
|
F21,
|
|
F22,
|
|
F23,
|
|
F24,
|
|
Snapshot,
|
|
Scroll,
|
|
Pause,
|
|
Insert,
|
|
Home,
|
|
Delete,
|
|
End,
|
|
PageDown,
|
|
PageUp,
|
|
Left,
|
|
Up,
|
|
Right,
|
|
Down,
|
|
Back,
|
|
Return,
|
|
Space,
|
|
Compose,
|
|
Numlock,
|
|
Numpad0,
|
|
Numpad1,
|
|
Numpad2,
|
|
Numpad3,
|
|
Numpad4,
|
|
Numpad5,
|
|
Numpad6,
|
|
Numpad7,
|
|
Numpad8,
|
|
Numpad9,
|
|
AbntC1,
|
|
AbntC2,
|
|
Add,
|
|
Apostrophe,
|
|
Apps,
|
|
At,
|
|
Ax,
|
|
Backslash,
|
|
Calculator,
|
|
Capital,
|
|
Colon,
|
|
Comma,
|
|
Convert,
|
|
Decimal,
|
|
Divide,
|
|
Equals,
|
|
Grave,
|
|
Kana,
|
|
Kanji,
|
|
LAlt,
|
|
LBracket,
|
|
LControl,
|
|
LShift,
|
|
LWin,
|
|
Mail,
|
|
MediaSelect,
|
|
MediaStop,
|
|
Minus,
|
|
Multiply,
|
|
Mute,
|
|
MyComputer,
|
|
NavigateForward,
|
|
NavigateBackward,
|
|
NextTrack,
|
|
NoConvert,
|
|
NumpadComma,
|
|
NumpadEnter,
|
|
NumpadEquals,
|
|
OEM102,
|
|
Period,
|
|
PlayPause,
|
|
Power,
|
|
PrevTrack,
|
|
RAlt,
|
|
RBracket,
|
|
RControl,
|
|
RShift,
|
|
RWin,
|
|
Semicolon,
|
|
Slash,
|
|
Sleep,
|
|
Stop,
|
|
Subtract,
|
|
Sysrq,
|
|
Tab,
|
|
Underline,
|
|
Unlabeled,
|
|
VolumeDown,
|
|
VolumeUp,
|
|
Wake,
|
|
WebBack,
|
|
WebFavorites,
|
|
WebForward,
|
|
WebHome,
|
|
WebRefresh,
|
|
WebSearch,
|
|
WebStop,
|
|
Yen,
|
|
Caret,
|
|
Copy,
|
|
Paste,
|
|
Cut,
|
|
}
|
|
|
|
impl Key {
|
|
pub fn from_glutin_input(key: ::glutin::VirtualKeyCode) -> Self {
|
|
use glutin::VirtualKeyCode::*;
|
|
// Thank you, vim macros and regex!
|
|
match key {
|
|
Key1 => Key::Key1,
|
|
Key2 => Key::Key2,
|
|
Key3 => Key::Key3,
|
|
Key4 => Key::Key4,
|
|
Key5 => Key::Key5,
|
|
Key6 => Key::Key6,
|
|
Key7 => Key::Key7,
|
|
Key8 => Key::Key8,
|
|
Key9 => Key::Key9,
|
|
Key0 => Key::Key0,
|
|
A => Key::A,
|
|
B => Key::B,
|
|
C => Key::C,
|
|
D => Key::D,
|
|
E => Key::E,
|
|
F => Key::F,
|
|
G => Key::G,
|
|
H => Key::H,
|
|
I => Key::I,
|
|
J => Key::J,
|
|
K => Key::K,
|
|
L => Key::L,
|
|
M => Key::M,
|
|
N => Key::N,
|
|
O => Key::O,
|
|
P => Key::P,
|
|
Q => Key::Q,
|
|
R => Key::R,
|
|
S => Key::S,
|
|
T => Key::T,
|
|
U => Key::U,
|
|
V => Key::V,
|
|
W => Key::W,
|
|
X => Key::X,
|
|
Y => Key::Y,
|
|
Z => Key::Z,
|
|
Escape => Key::Escape,
|
|
F1 => Key::F1,
|
|
F2 => Key::F2,
|
|
F3 => Key::F3,
|
|
F4 => Key::F4,
|
|
F5 => Key::F5,
|
|
F6 => Key::F6,
|
|
F7 => Key::F7,
|
|
F8 => Key::F8,
|
|
F9 => Key::F9,
|
|
F10 => Key::F10,
|
|
F11 => Key::F11,
|
|
F12 => Key::F12,
|
|
F13 => Key::F13,
|
|
F14 => Key::F14,
|
|
F15 => Key::F15,
|
|
F16 => Key::F16,
|
|
F17 => Key::F17,
|
|
F18 => Key::F18,
|
|
F19 => Key::F19,
|
|
F20 => Key::F20,
|
|
F21 => Key::F21,
|
|
F22 => Key::F22,
|
|
F23 => Key::F23,
|
|
F24 => Key::F24,
|
|
Snapshot => Key::Snapshot,
|
|
Scroll => Key::Scroll,
|
|
Pause => Key::Pause,
|
|
Insert => Key::Insert,
|
|
Home => Key::Home,
|
|
Delete => Key::Delete,
|
|
End => Key::End,
|
|
PageDown => Key::PageDown,
|
|
PageUp => Key::PageUp,
|
|
Left => Key::Left,
|
|
Up => Key::Up,
|
|
Right => Key::Right,
|
|
Down => Key::Down,
|
|
Back => Key::Back,
|
|
Return => Key::Return,
|
|
Space => Key::Space,
|
|
Compose => Key::Compose,
|
|
Numlock => Key::Numlock,
|
|
Numpad0 => Key::Numpad0,
|
|
Numpad1 => Key::Numpad1,
|
|
Numpad2 => Key::Numpad2,
|
|
Numpad3 => Key::Numpad3,
|
|
Numpad4 => Key::Numpad4,
|
|
Numpad5 => Key::Numpad5,
|
|
Numpad6 => Key::Numpad6,
|
|
Numpad7 => Key::Numpad7,
|
|
Numpad8 => Key::Numpad8,
|
|
Numpad9 => Key::Numpad9,
|
|
AbntC1 => Key::AbntC1,
|
|
AbntC2 => Key::AbntC2,
|
|
Add => Key::Add,
|
|
Apostrophe => Key::Apostrophe,
|
|
Apps => Key::Apps,
|
|
At => Key::At,
|
|
Ax => Key::Ax,
|
|
Backslash => Key::Backslash,
|
|
Calculator => Key::Calculator,
|
|
Capital => Key::Capital,
|
|
Colon => Key::Colon,
|
|
Comma => Key::Comma,
|
|
Convert => Key::Convert,
|
|
Decimal => Key::Decimal,
|
|
Divide => Key::Divide,
|
|
Equals => Key::Equals,
|
|
Grave => Key::Grave,
|
|
Kana => Key::Kana,
|
|
Kanji => Key::Kanji,
|
|
LAlt => Key::LAlt,
|
|
LBracket => Key::LBracket,
|
|
LControl => Key::LControl,
|
|
LShift => Key::LShift,
|
|
LWin => Key::LWin,
|
|
Mail => Key::Mail,
|
|
MediaSelect => Key::MediaSelect,
|
|
MediaStop => Key::MediaStop,
|
|
Minus => Key::Minus,
|
|
Multiply => Key::Multiply,
|
|
Mute => Key::Mute,
|
|
MyComputer => Key::MyComputer,
|
|
NavigateForward => Key::NavigateForward,
|
|
NavigateBackward => Key::NavigateBackward,
|
|
NextTrack => Key::NextTrack,
|
|
NoConvert => Key::NoConvert,
|
|
NumpadComma => Key::NumpadComma,
|
|
NumpadEnter => Key::NumpadEnter,
|
|
NumpadEquals => Key::NumpadEquals,
|
|
OEM102 => Key::OEM102,
|
|
Period => Key::Period,
|
|
PlayPause => Key::PlayPause,
|
|
Power => Key::Power,
|
|
PrevTrack => Key::PrevTrack,
|
|
RAlt => Key::RAlt,
|
|
RBracket => Key::RBracket,
|
|
RControl => Key::RControl,
|
|
RShift => Key::RShift,
|
|
RWin => Key::RWin,
|
|
Semicolon => Key::Semicolon,
|
|
Slash => Key::Slash,
|
|
Sleep => Key::Sleep,
|
|
Stop => Key::Stop,
|
|
Subtract => Key::Subtract,
|
|
Sysrq => Key::Sysrq,
|
|
Tab => Key::Tab,
|
|
Underline => Key::Underline,
|
|
Unlabeled => Key::Unlabeled,
|
|
VolumeDown => Key::VolumeDown,
|
|
VolumeUp => Key::VolumeUp,
|
|
Wake => Key::Wake,
|
|
WebBack => Key::WebBack,
|
|
WebFavorites => Key::WebFavorites,
|
|
WebForward => Key::WebForward,
|
|
WebHome => Key::WebHome,
|
|
WebRefresh => Key::WebRefresh,
|
|
WebSearch => Key::WebSearch,
|
|
WebStop => Key::WebStop,
|
|
Yen => Key::Yen,
|
|
Caret => Key::Caret,
|
|
Copy => Key::Copy,
|
|
Paste => Key::Paste,
|
|
Cut => Key::Cut,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::cli::Options;
|
|
use super::{Config, DEFAULT_ALACRITTY_CONFIG};
|
|
|
|
#[test]
|
|
fn parse_config() {
|
|
let config: Config = ::serde_yaml::from_str(DEFAULT_ALACRITTY_CONFIG)
|
|
.expect("deserialize config");
|
|
|
|
// Sanity check that mouse bindings are being parsed
|
|
assert!(!config.mouse_bindings.is_empty());
|
|
|
|
// Sanity check that key bindings are being parsed
|
|
assert!(!config.key_bindings.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn dynamic_title_ignoring_options_by_default() {
|
|
let config: Config = ::serde_yaml::from_str(DEFAULT_ALACRITTY_CONFIG)
|
|
.expect("deserialize config");
|
|
let old_dynamic_title = config.dynamic_title;
|
|
let options = Options::default();
|
|
let config = config.update_dynamic_title(&options);
|
|
assert_eq!(old_dynamic_title, config.dynamic_title);
|
|
}
|
|
|
|
#[test]
|
|
fn dynamic_title_overridden_by_options() {
|
|
let config: Config = ::serde_yaml::from_str(DEFAULT_ALACRITTY_CONFIG)
|
|
.expect("deserialize config");
|
|
let mut options = Options::default();
|
|
options.title = Some("foo".to_owned());
|
|
let config = config.update_dynamic_title(&options);
|
|
assert!(!config.dynamic_title);
|
|
}
|
|
|
|
#[test]
|
|
fn default_match_empty() {
|
|
let default = Config::default();
|
|
|
|
let empty = serde_yaml::from_str("key: val\n").unwrap();
|
|
|
|
assert_eq!(default, empty);
|
|
}
|
|
}
|