use std::cell::RefCell; use std::path::PathBuf; use std::rc::Rc; use glutin::event::{ModifiersState, VirtualKeyCode}; use log::error; use serde::de::Error as SerdeError; use serde::{self, Deserialize, Deserializer}; use unicode_width::UnicodeWidthChar; use alacritty_config_derive::ConfigDeserialize; use alacritty_terminal::config::{Percentage, Program, LOG_TARGET_CONFIG}; use alacritty_terminal::term::search::RegexSearch; use crate::config::bell::BellConfig; use crate::config::bindings::{ self, Action, Binding, Key, KeyBinding, ModeWrapper, ModsWrapper, MouseBinding, }; use crate::config::color::Colors; use crate::config::debug::Debug; use crate::config::font::Font; use crate::config::mouse::Mouse; use crate::config::window::WindowConfig; /// Regex used for the default URL hint. #[rustfmt::skip] const URL_REGEX: &str = "(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)\ [^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+"; #[derive(ConfigDeserialize, Debug, PartialEq)] pub struct UiConfig { /// Font configuration. pub font: Font, /// Window configuration. pub window: WindowConfig, pub mouse: Mouse, /// Debug options. pub debug: Debug, /// Send escape sequences using the alt key. pub alt_send_esc: bool, /// Live config reload. pub live_config_reload: bool, /// Bell configuration. pub bell: BellConfig, /// RGB values for colors. pub colors: Colors, /// Should draw bold text with brighter colors instead of bold font. pub draw_bold_text_with_bright_colors: bool, /// Path where config was loaded from. #[config(skip)] pub config_paths: Vec, /// Regex hints for interacting with terminal content. pub hints: Hints, /// Keybindings. key_bindings: KeyBindings, /// Bindings for the mouse. mouse_bindings: MouseBindings, /// Background opacity from 0.0 to 1.0. background_opacity: Percentage, } impl Default for UiConfig { fn default() -> Self { Self { alt_send_esc: true, live_config_reload: true, font: Default::default(), window: Default::default(), mouse: Default::default(), debug: Default::default(), config_paths: Default::default(), key_bindings: Default::default(), mouse_bindings: Default::default(), background_opacity: Default::default(), bell: Default::default(), colors: Default::default(), draw_bold_text_with_bright_colors: Default::default(), hints: Default::default(), } } } impl UiConfig { /// Generate key bindings for all keyboard hints. pub fn generate_hint_bindings(&mut self) { for hint in &self.hints.enabled { let binding = match hint.binding { Some(binding) => binding, None => continue, }; let binding = KeyBinding { trigger: binding.key, mods: binding.mods.0, mode: binding.mode.mode, notmode: binding.mode.not_mode, action: Action::Hint(hint.clone()), }; self.key_bindings.0.push(binding); } } #[inline] pub fn background_opacity(&self) -> f32 { self.background_opacity.as_f32() } #[inline] pub fn key_bindings(&self) -> &[KeyBinding] { self.key_bindings.0.as_slice() } #[inline] pub fn mouse_bindings(&self) -> &[MouseBinding] { self.mouse_bindings.0.as_slice() } } #[derive(Debug, PartialEq)] struct KeyBindings(Vec); impl Default for KeyBindings { fn default() -> Self { Self(bindings::default_key_bindings()) } } impl<'de> Deserialize<'de> for KeyBindings { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { Ok(Self(deserialize_bindings(deserializer, Self::default().0)?)) } } #[derive(Debug, PartialEq)] struct MouseBindings(Vec); impl Default for MouseBindings { fn default() -> Self { Self(bindings::default_mouse_bindings()) } } impl<'de> Deserialize<'de> for MouseBindings { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { Ok(Self(deserialize_bindings(deserializer, Self::default().0)?)) } } fn deserialize_bindings<'a, D, T>( deserializer: D, mut default: Vec>, ) -> Result>, D::Error> where D: Deserializer<'a>, T: Copy + Eq, Binding: Deserialize<'a>, { let values = Vec::::deserialize(deserializer)?; // Skip all invalid values. let mut bindings = Vec::with_capacity(values.len()); for value in values { match Binding::::deserialize(value) { Ok(binding) => bindings.push(binding), Err(err) => { error!(target: LOG_TARGET_CONFIG, "Config error: {}; ignoring binding", err); }, } } // Remove matching default bindings. for binding in bindings.iter() { default.retain(|b| !b.triggers_match(binding)); } bindings.extend(default); Ok(bindings) } /// A delta for a point in a 2 dimensional plane. #[derive(ConfigDeserialize, Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct Delta { /// Horizontal change. pub x: T, /// Vertical change. pub y: T, } /// Regex terminal hints. #[derive(ConfigDeserialize, Debug, PartialEq, Eq)] pub struct Hints { /// Characters for the hint labels. alphabet: HintsAlphabet, /// All configured terminal hints. pub enabled: Vec, } impl Default for Hints { fn default() -> Self { // Add URL hint by default when no other hint is present. let pattern = LazyRegexVariant::Pattern(String::from(URL_REGEX)); let regex = LazyRegex(Rc::new(RefCell::new(pattern))); #[cfg(not(any(target_os = "macos", windows)))] let action = HintAction::Command(Program::Just(String::from("xdg-open"))); #[cfg(target_os = "macos")] let action = HintAction::Command(Program::Just(String::from("open"))); #[cfg(windows)] let action = HintAction::Command(Program::WithArgs { program: String::from("cmd"), args: vec!["/c".to_string(), "start".to_string(), "".to_string()], }); Self { enabled: vec![Hint { regex, action, post_processing: true, mouse: Some(HintMouse { enabled: true, mods: Default::default() }), binding: Some(HintBinding { key: Key::Keycode(VirtualKeyCode::U), mods: ModsWrapper(ModifiersState::SHIFT | ModifiersState::CTRL), mode: Default::default(), }), }], alphabet: Default::default(), } } } impl Hints { /// Characters for the hint labels. pub fn alphabet(&self) -> &str { &self.alphabet.0 } } #[derive(Clone, Debug, PartialEq, Eq)] struct HintsAlphabet(String); impl Default for HintsAlphabet { fn default() -> Self { Self(String::from("jfkdls;ahgurieowpq")) } } impl<'de> Deserialize<'de> for HintsAlphabet { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let value = String::deserialize(deserializer)?; let mut character_count = 0; for character in value.chars() { if character.width() != Some(1) { return Err(D::Error::custom("characters must be of width 1")); } character_count += 1; } if character_count < 2 { return Err(D::Error::custom("must include at last 2 characters")); } Ok(Self(value)) } } /// Built-in actions for hint mode. #[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] pub enum HintInternalAction { /// Copy the text to the clipboard. Copy, /// Write the text to the PTY/search. Paste, /// Select the text matching the hint. Select, /// Move the vi mode cursor to the beginning of the hint. MoveViModeCursor, } /// Actions for hint bindings. #[derive(Deserialize, Clone, Debug, PartialEq, Eq)] pub enum HintAction { /// Built-in hint action. #[serde(rename = "action")] Action(HintInternalAction), /// Command the text will be piped to. #[serde(rename = "command")] Command(Program), } /// Hint configuration. #[derive(Deserialize, Clone, Debug, PartialEq, Eq)] pub struct Hint { /// Regex for finding matches. pub regex: LazyRegex, /// Action executed when this hint is triggered. #[serde(flatten)] pub action: HintAction, /// Hint text post processing. #[serde(default)] pub post_processing: bool, /// Hint mouse highlighting. pub mouse: Option, /// Binding required to search for this hint. binding: Option, } /// Binding for triggering a keyboard hint. #[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq)] pub struct HintBinding { pub key: Key, #[serde(default)] pub mods: ModsWrapper, #[serde(default)] pub mode: ModeWrapper, } /// Hint mouse highlighting. #[derive(ConfigDeserialize, Default, Copy, Clone, Debug, PartialEq, Eq)] pub struct HintMouse { /// Hint mouse highlighting availability. pub enabled: bool, /// Required mouse modifiers for hint highlighting. pub mods: ModsWrapper, } /// Lazy regex with interior mutability. #[derive(Clone, Debug, PartialEq, Eq)] pub struct LazyRegex(Rc>); impl LazyRegex { /// Execute a function with the compiled regex DFAs as parameter. pub fn with_compiled(&self, mut f: F) -> T where F: FnMut(&RegexSearch) -> T, { f(self.0.borrow_mut().compiled()) } } impl<'de> Deserialize<'de> for LazyRegex { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let regex = LazyRegexVariant::Pattern(String::deserialize(deserializer)?); Ok(Self(Rc::new(RefCell::new(regex)))) } } /// Regex which is compiled on demand, to avoid expensive computations at startup. #[derive(Clone, Debug)] pub enum LazyRegexVariant { Compiled(Box), Pattern(String), } impl LazyRegexVariant { /// Get a reference to the compiled regex. /// /// If the regex is not already compiled, this will compile the DFAs and store them for future /// access. fn compiled(&mut self) -> &RegexSearch { // Check if the regex has already been compiled. let regex = match self { Self::Compiled(regex_search) => return regex_search, Self::Pattern(regex) => regex, }; // Compile the regex. let regex_search = match RegexSearch::new(regex) { Ok(regex_search) => regex_search, Err(error) => { error!("hint regex is invalid: {}", error); RegexSearch::new("").unwrap() }, }; *self = Self::Compiled(Box::new(regex_search)); // Return a reference to the compiled DFAs. match self { Self::Compiled(dfas) => dfas, Self::Pattern(_) => unreachable!(), } } } impl PartialEq for LazyRegexVariant { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Pattern(regex), Self::Pattern(other_regex)) => regex == other_regex, _ => false, } } } impl Eq for LazyRegexVariant {}