diff options
Diffstat (limited to 'alacritty/src/config')
-rw-r--r-- | alacritty/src/config/bindings.rs | 1409 | ||||
-rw-r--r-- | alacritty/src/config/mod.rs | 234 | ||||
-rw-r--r-- | alacritty/src/config/monitor.rs | 58 | ||||
-rw-r--r-- | alacritty/src/config/mouse.rs | 115 | ||||
-rw-r--r-- | alacritty/src/config/test.rs | 24 | ||||
-rw-r--r-- | alacritty/src/config/ui_config.rs | 63 |
6 files changed, 1903 insertions, 0 deletions
diff --git a/alacritty/src/config/bindings.rs b/alacritty/src/config/bindings.rs new file mode 100644 index 00000000..17a6d0b7 --- /dev/null +++ b/alacritty/src/config/bindings.rs @@ -0,0 +1,1409 @@ +// 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. + +use std::fmt; +use std::str::FromStr; + +use glutin::event::{ModifiersState, MouseButton}; +use log::error; +use serde::de::Error as SerdeError; +use serde::de::{self, MapAccess, Unexpected, Visitor}; +use serde::{Deserialize, Deserializer}; + +use alacritty_terminal::config::LOG_TARGET_CONFIG; +use alacritty_terminal::term::TermMode; + +/// Describes a state and action to take in that state +/// +/// This is the shared component of `MouseBinding` and `KeyBinding` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Binding<T> { + /// Modifier keys required to activate binding + pub mods: ModifiersState, + + /// String to send to pty if mods and mode match + pub action: Action, + + /// Terminal mode required to activate binding + pub mode: TermMode, + + /// excluded terminal modes where the binding won't be activated + pub notmode: TermMode, + + /// This property is used as part of the trigger detection code. + /// + /// For example, this might be a key like "G", or a mouse button. + pub trigger: T, +} + +/// Bindings that are triggered by a keyboard key +pub type KeyBinding = Binding<Key>; + +/// Bindings that are triggered by a mouse button +pub type MouseBinding = Binding<MouseButton>; + +impl Default for KeyBinding { + fn default() -> KeyBinding { + KeyBinding { + mods: Default::default(), + action: Action::Esc(String::new()), + mode: TermMode::NONE, + notmode: TermMode::NONE, + trigger: Key::A, + } + } +} + +impl Default for MouseBinding { + fn default() -> MouseBinding { + MouseBinding { + mods: Default::default(), + action: Action::Esc(String::new()), + mode: TermMode::NONE, + notmode: TermMode::NONE, + trigger: MouseButton::Left, + } + } +} + +impl<T: Eq> Binding<T> { + #[inline] + pub fn is_triggered_by( + &self, + mode: TermMode, + mods: ModifiersState, + input: &T, + relaxed: bool, + ) -> bool { + // Check input first since bindings are stored in one big list. This is + // the most likely item to fail so prioritizing it here allows more + // checks to be short circuited. + self.trigger == *input + && mode.contains(self.mode) + && !mode.intersects(self.notmode) + && (self.mods == mods || (relaxed && self.mods.relaxed_eq(mods))) + } + + #[inline] + pub fn triggers_match(&self, binding: &Binding<T>) -> bool { + // Check the binding's key and modifiers + if self.trigger != binding.trigger || self.mods != binding.mods { + return false; + } + + // Completely empty modes match all modes + if (self.mode.is_empty() && self.notmode.is_empty()) + || (binding.mode.is_empty() && binding.notmode.is_empty()) + { + return true; + } + + // Check for intersection (equality is required since empty does not intersect itself) + (self.mode == binding.mode || self.mode.intersects(binding.mode)) + && (self.notmode == binding.notmode || self.notmode.intersects(binding.notmode)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub enum Action { + /// Write an escape sequence. + #[serde(skip)] + Esc(String), + + /// Paste contents of system clipboard. + Paste, + + /// Store current selection into clipboard. + Copy, + + /// Paste contents of selection buffer. + PasteSelection, + + /// Increase font size. + IncreaseFontSize, + + /// Decrease font size. + DecreaseFontSize, + + /// Reset font size to the config value. + ResetFontSize, + + /// Scroll exactly one page up. + ScrollPageUp, + + /// Scroll exactly one page down. + ScrollPageDown, + + /// Scroll one line up. + ScrollLineUp, + + /// Scroll one line down. + ScrollLineDown, + + /// Scroll all the way to the top. + ScrollToTop, + + /// Scroll all the way to the bottom. + ScrollToBottom, + + /// Clear the display buffer(s) to remove history. + ClearHistory, + + /// Run given command. + #[serde(skip)] + Command(String, Vec<String>), + + /// Hide the Alacritty window. + Hide, + + /// Quit Alacritty. + Quit, + + /// Clear warning and error notices. + ClearLogNotice, + + /// Spawn a new instance of Alacritty. + SpawnNewInstance, + + /// Toggle fullscreen. + ToggleFullscreen, + + /// Toggle simple fullscreen on macos. + #[cfg(target_os = "macos")] + ToggleSimpleFullscreen, + + /// Allow receiving char input. + ReceiveChar, + + /// No action. + None, +} + +impl Default for Action { + fn default() -> Action { + Action::None + } +} + +impl From<&'static str> for Action { + fn from(s: &'static str) -> Action { + Action::Esc(s.into()) + } +} + +pub trait RelaxedEq<T: ?Sized = Self> { + fn relaxed_eq(&self, other: T) -> bool; +} + +impl RelaxedEq for ModifiersState { + // Make sure that modifiers in the config are always present, + // but ignore surplus modifiers. + fn relaxed_eq(&self, other: Self) -> bool { + (!self.logo || other.logo) + && (!self.alt || other.alt) + && (!self.ctrl || other.ctrl) + && (!self.shift || other.shift) + } +} + +macro_rules! bindings { + ( + $ty:ident; + $( + $key:path + $(,[$($mod:ident: $enabled:expr),*])* + $(,+$mode:expr)* + $(,~$notmode:expr)* + ;$action:expr + );* + $(;)* + ) => {{ + let mut v = Vec::new(); + + $( + let mut _mods = ModifiersState { + $($($mod: $enabled),*,)* + ..Default::default() + }; + let mut _mode = TermMode::empty(); + $(_mode = $mode;)* + let mut _notmode = TermMode::empty(); + $(_notmode = $notmode;)* + + v.push($ty { + trigger: $key, + mods: _mods, + mode: _mode, + notmode: _notmode, + action: $action, + }); + )* + + v + }} +} + +pub fn default_mouse_bindings() -> Vec<MouseBinding> { + bindings!( + MouseBinding; + MouseButton::Middle; Action::PasteSelection; + ) +} + +pub fn default_key_bindings() -> Vec<KeyBinding> { + let mut bindings = bindings!( + KeyBinding; + Key::Paste; Action::Paste; + Key::Copy; Action::Copy; + Key::L, [ctrl: true]; Action::ClearLogNotice; + Key::L, [ctrl: true]; Action::Esc("\x0c".into()); + Key::PageUp, [shift: true], ~TermMode::ALT_SCREEN; Action::ScrollPageUp; + Key::PageDown, [shift: true], ~TermMode::ALT_SCREEN; Action::ScrollPageDown; + Key::Home, [shift: true], ~TermMode::ALT_SCREEN; Action::ScrollToTop; + Key::End, [shift: true], ~TermMode::ALT_SCREEN; Action::ScrollToBottom; + Key::Home, +TermMode::APP_CURSOR; Action::Esc("\x1bOH".into()); + Key::Home, ~TermMode::APP_CURSOR; Action::Esc("\x1b[H".into()); + Key::Home, [shift: true], +TermMode::ALT_SCREEN; Action::Esc("\x1b[1;2H".into()); + Key::End, +TermMode::APP_CURSOR; Action::Esc("\x1bOF".into()); + Key::End, ~TermMode::APP_CURSOR; Action::Esc("\x1b[F".into()); + Key::End, [shift: true], +TermMode::ALT_SCREEN; Action::Esc("\x1b[1;2F".into()); + Key::PageUp; Action::Esc("\x1b[5~".into()); + Key::PageUp, [shift: true], +TermMode::ALT_SCREEN; Action::Esc("\x1b[5;2~".into()); + Key::PageDown; Action::Esc("\x1b[6~".into()); + Key::PageDown, [shift: true], +TermMode::ALT_SCREEN; Action::Esc("\x1b[6;2~".into()); + Key::Tab, [shift: true]; Action::Esc("\x1b[Z".into()); + Key::Back; Action::Esc("\x7f".into()); + Key::Back, [alt: true]; Action::Esc("\x1b\x7f".into()); + Key::Insert; Action::Esc("\x1b[2~".into()); + Key::Delete; Action::Esc("\x1b[3~".into()); + Key::Up, +TermMode::APP_CURSOR; Action::Esc("\x1bOA".into()); + Key::Up, ~TermMode::APP_CURSOR; Action::Esc("\x1b[A".into()); + Key::Down, +TermMode::APP_CURSOR; Action::Esc("\x1bOB".into()); + Key::Down, ~TermMode::APP_CURSOR; Action::Esc("\x1b[B".into()); + Key::Right, +TermMode::APP_CURSOR; Action::Esc("\x1bOC".into()); + Key::Right, ~TermMode::APP_CURSOR; Action::Esc("\x1b[C".into()); + Key::Left, +TermMode::APP_CURSOR; Action::Esc("\x1bOD".into()); + Key::Left, ~TermMode::APP_CURSOR; Action::Esc("\x1b[D".into()); + Key::F1; Action::Esc("\x1bOP".into()); + Key::F2; Action::Esc("\x1bOQ".into()); + Key::F3; Action::Esc("\x1bOR".into()); + Key::F4; Action::Esc("\x1bOS".into()); + Key::F5; Action::Esc("\x1b[15~".into()); + Key::F6; Action::Esc("\x1b[17~".into()); + Key::F7; Action::Esc("\x1b[18~".into()); + Key::F8; Action::Esc("\x1b[19~".into()); + Key::F9; Action::Esc("\x1b[20~".into()); + Key::F10; Action::Esc("\x1b[21~".into()); + Key::F11; Action::Esc("\x1b[23~".into()); + Key::F12; Action::Esc("\x1b[24~".into()); + Key::F13; Action::Esc("\x1b[25~".into()); + Key::F14; Action::Esc("\x1b[26~".into()); + Key::F15; Action::Esc("\x1b[28~".into()); + Key::F16; Action::Esc("\x1b[29~".into()); + Key::F17; Action::Esc("\x1b[31~".into()); + Key::F18; Action::Esc("\x1b[32~".into()); + Key::F19; Action::Esc("\x1b[33~".into()); + Key::F20; Action::Esc("\x1b[34~".into()); + Key::NumpadEnter; Action::Esc("\n".into()); + ); + + // Code Modifiers + // ---------+--------------------------- + // 2 | Shift + // 3 | Alt + // 4 | Shift + Alt + // 5 | Control + // 6 | Shift + Control + // 7 | Alt + Control + // 8 | Shift + Alt + Control + // ---------+--------------------------- + // + // from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys + let modifiers = vec![ + ModifiersState { shift: true, ..ModifiersState::default() }, + ModifiersState { alt: true, ..ModifiersState::default() }, + ModifiersState { shift: true, alt: true, ..ModifiersState::default() }, + ModifiersState { ctrl: true, ..ModifiersState::default() }, + ModifiersState { shift: true, ctrl: true, ..ModifiersState::default() }, + ModifiersState { alt: true, ctrl: true, ..ModifiersState::default() }, + ModifiersState { shift: true, alt: true, ctrl: true, ..ModifiersState::default() }, + ]; + + for (index, mods) in modifiers.iter().enumerate() { + let modifiers_code = index + 2; + bindings.extend(bindings!( + KeyBinding; + Key::Up, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[1;{}A", modifiers_code)); + Key::Down, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[1;{}B", modifiers_code)); + Key::Right, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[1;{}C", modifiers_code)); + Key::Left, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[1;{}D", modifiers_code)); + Key::F1, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[1;{}P", modifiers_code)); + Key::F2, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[1;{}Q", modifiers_code)); + Key::F3, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[1;{}R", modifiers_code)); + Key::F4, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[1;{}S", modifiers_code)); + Key::F5, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[15;{}~", modifiers_code)); + Key::F6, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[17;{}~", modifiers_code)); + Key::F7, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[18;{}~", modifiers_code)); + Key::F8, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[19;{}~", modifiers_code)); + Key::F9, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[20;{}~", modifiers_code)); + Key::F10, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[21;{}~", modifiers_code)); + Key::F11, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[23;{}~", modifiers_code)); + Key::F12, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[24;{}~", modifiers_code)); + Key::F13, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[25;{}~", modifiers_code)); + Key::F14, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[26;{}~", modifiers_code)); + Key::F15, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[28;{}~", modifiers_code)); + Key::F16, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[29;{}~", modifiers_code)); + Key::F17, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[31;{}~", modifiers_code)); + Key::F18, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[32;{}~", modifiers_code)); + Key::F19, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[33;{}~", modifiers_code)); + Key::F20, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[34;{}~", modifiers_code)); + )); + + // We're adding the following bindings with `Shift` manually above, so skipping them here + // modifiers_code != Shift + if modifiers_code != 2 { + bindings.extend(bindings!( + KeyBinding; + Key::PageUp, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[5;{}~", modifiers_code)); + Key::PageDown, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[6;{}~", modifiers_code)); + Key::End, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[1;{}F", modifiers_code)); + Key::Home, [shift: mods.shift, alt: mods.alt, ctrl: mods.ctrl]; + Action::Esc(format!("\x1b[1;{}H", modifiers_code)); + )); + } + } + + bindings.extend(platform_key_bindings()); + + bindings +} + +#[cfg(not(any(target_os = "macos", test)))] +fn common_keybindings() -> Vec<KeyBinding> { + bindings!( + KeyBinding; + Key::V, [ctrl: true, shift: true]; Action::Paste; + Key::C, [ctrl: true, shift: true]; Action::Copy; + Key::Insert, [shift: true]; Action::PasteSelection; + Key::Key0, [ctrl: true]; Action::ResetFontSize; + Key::Equals, [ctrl: true]; Action::IncreaseFontSize; + Key::Add, [ctrl: true]; Action::IncreaseFontSize; + Key::Subtract, [ctrl: true]; Action::DecreaseFontSize; + Key::Minus, [ctrl: true]; Action::DecreaseFontSize; + ) +} + +#[cfg(not(any(target_os = "macos", target_os = "windows", test)))] +pub fn platform_key_bindings() -> Vec<KeyBinding> { + common_keybindings() +} + +#[cfg(all(target_os = "windows", not(test)))] +pub fn platform_key_bindings() -> Vec<KeyBinding> { + let mut bindings = bindings!( + KeyBinding; + Key::Return, [alt: true]; Action::ToggleFullscreen; + ); + bindings.extend(common_keybindings()); + bindings +} + +#[cfg(all(target_os = "macos", not(test)))] +pub fn platform_key_bindings() -> Vec<KeyBinding> { + bindings!( + KeyBinding; + Key::Key0, [logo: true]; Action::ResetFontSize; + Key::Equals, [logo: true]; Action::IncreaseFontSize; + Key::Add, [logo: true]; Action::IncreaseFontSize; + Key::Minus, [logo: true]; Action::DecreaseFontSize; + Key::F, [ctrl: true, logo: true]; Action::ToggleFullscreen; + Key::K, [logo: true]; Action::ClearHistory; + Key::K, [logo: true]; Action::Esc("\x0c".into()); + Key::V, [logo: true]; Action::Paste; + Key::C, [logo: true]; Action::Copy; + Key::H, [logo: true]; Action::Hide; + Key::Q, [logo: true]; Action::Quit; + Key::W, [logo: true]; Action::Quit; + ) +} + +// Don't return any bindings for tests since they are commented-out by default +#[cfg(test)] +pub fn platform_key_bindings() -> Vec<KeyBinding> { + vec![] +} + +#[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::event::VirtualKeyCode) -> Self { + use glutin::event::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, + } + } +} + +struct ModeWrapper { + pub mode: TermMode, + pub not_mode: TermMode, +} + +impl<'a> Deserialize<'a> for ModeWrapper { + fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error> + where + D: 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().to_lowercase().as_str() { + "appcursor" => res.mode |= TermMode::APP_CURSOR, + "~appcursor" => res.not_mode |= TermMode::APP_CURSOR, + "appkeypad" => res.mode |= TermMode::APP_KEYPAD, + "~appkeypad" => res.not_mode |= TermMode::APP_KEYPAD, + "~alt" => res.not_mode |= TermMode::ALT_SCREEN, + "alt" => res.mode |= TermMode::ALT_SCREEN, + _ => error!(target: LOG_TARGET_CONFIG, "Unknown mode {:?}", modifier), + } + } + + Ok(res) + } + } + deserializer.deserialize_str(ModeVisitor) + } +} + +struct MouseButtonWrapper(MouseButton); + +impl MouseButtonWrapper { + fn into_inner(self) -> MouseButton { + self.0 + } +} + +impl<'a> Deserialize<'a> for MouseButtonWrapper { + fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error> + where + D: Deserializer<'a>, + { + struct MouseButtonVisitor; + + impl<'a> Visitor<'a> for MouseButtonVisitor { + type Value = MouseButtonWrapper; + + 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<MouseButtonWrapper, E> + where + E: de::Error, + { + match value { + "Left" => Ok(MouseButtonWrapper(MouseButton::Left)), + "Right" => Ok(MouseButtonWrapper(MouseButton::Right)), + "Middle" => Ok(MouseButtonWrapper(MouseButton::Middle)), + _ => { + if let Ok(index) = u8::from_str(value) { + Ok(MouseButtonWrapper(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<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> Deserialize<'a> for RawBinding { + fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error> + where + D: Deserializer<'a>, + { + enum Field { + Key, + Mods, + Mode, + Action, + Chars, + Mouse, + Command, + } + + impl<'a> Deserialize<'a> for Field { + fn deserialize<D>(deserializer: D) -> ::std::result::Result<Field, D::Error> + where + D: Deserializer<'a>, + { + struct FieldVisitor; + + static FIELDS: &[&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<Action> = None; + let mut mode: Option<TermMode> = None; + let mut not_mode: Option<TermMode> = None; + let mut mouse: Option<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::<Action>()?); + }, + 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::<MouseButtonWrapper>()?.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> Deserialize<'a> for MouseBinding { + fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error> + where + D: Deserializer<'a>, + { + let raw = RawBinding::deserialize(deserializer)?; + raw.into_mouse_binding().map_err(|_| D::Error::custom("expected mouse binding")) + } +} + +impl<'a> Deserialize<'a> for KeyBinding { + fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error> + where + D: Deserializer<'a>, + { + let raw = RawBinding::deserialize(deserializer)?; + raw.into_key_binding().map_err(|_| D::Error::custom("expected key binding")) + } +} + +#[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, + } + } +} + +/// 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)] +pub struct ModsWrapper(ModifiersState); + +impl ModsWrapper { + pub 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().to_lowercase().as_str() { + "command" | "super" => res.logo = true, + "shift" => res.shift = true, + "alt" | "option" => res.alt = true, + "control" => res.ctrl = true, + "none" => (), + _ => error!(target: LOG_TARGET_CONFIG, "Unknown modifier {:?}", modifier), + } + } + + Ok(ModsWrapper(res)) + } + } + + deserializer.deserialize_str(ModsVisitor) + } +} + +#[cfg(test)] +mod test { + use glutin::event::ModifiersState; + + use alacritty_terminal::term::TermMode; + + use crate::config::{Action, Binding}; + + type MockBinding = Binding<usize>; + + impl Default for MockBinding { + fn default() -> Self { + Self { + mods: Default::default(), + action: Default::default(), + mode: TermMode::empty(), + notmode: TermMode::empty(), + trigger: Default::default(), + } + } + } + + #[test] + fn binding_matches_itself() { + let binding = MockBinding::default(); + let identical_binding = MockBinding::default(); + + assert!(binding.triggers_match(&identical_binding)); + assert!(identical_binding.triggers_match(&binding)); + } + + #[test] + fn binding_matches_different_action() { + let binding = MockBinding::default(); + let mut different_action = MockBinding::default(); + different_action.action = Action::ClearHistory; + + assert!(binding.triggers_match(&different_action)); + assert!(different_action.triggers_match(&binding)); + } + + #[test] + fn mods_binding_requires_strict_match() { + let mut superset_mods = MockBinding::default(); + superset_mods.mods = ModifiersState { alt: true, logo: true, ctrl: true, shift: true }; + let mut subset_mods = MockBinding::default(); + subset_mods.mods = ModifiersState { alt: true, logo: false, ctrl: false, shift: false }; + + assert!(!superset_mods.triggers_match(&subset_mods)); + assert!(!subset_mods.triggers_match(&superset_mods)); + } + + #[test] + fn binding_matches_identical_mode() { + let mut b1 = MockBinding::default(); + b1.mode = TermMode::ALT_SCREEN; + let mut b2 = MockBinding::default(); + b2.mode = TermMode::ALT_SCREEN; + + assert!(b1.triggers_match(&b2)); + } + + #[test] + fn binding_without_mode_matches_any_mode() { + let b1 = MockBinding::default(); + let mut b2 = MockBinding::default(); + b2.mode = TermMode::APP_KEYPAD; + b2.notmode = TermMode::ALT_SCREEN; + + assert!(b1.triggers_match(&b2)); + } + + #[test] + fn binding_with_mode_matches_empty_mode() { + let mut b1 = MockBinding::default(); + b1.mode = TermMode::APP_KEYPAD; + b1.notmode = TermMode::ALT_SCREEN; + let b2 = MockBinding::default(); + + assert!(b1.triggers_match(&b2)); + } + + #[test] + fn binding_matches_superset_mode() { + let mut b1 = MockBinding::default(); + b1.mode = TermMode::APP_KEYPAD; + let mut b2 = MockBinding::default(); + b2.mode = TermMode::ALT_SCREEN | TermMode::APP_KEYPAD; + + assert!(b1.triggers_match(&b2)); + } + + #[test] + fn binding_matches_subset_mode() { + let mut b1 = MockBinding::default(); + b1.mode = TermMode::ALT_SCREEN | TermMode::APP_KEYPAD; + let mut b2 = MockBinding::default(); + b2.mode = TermMode::APP_KEYPAD; + + assert!(b1.triggers_match(&b2)); + } + + #[test] + fn binding_matches_partial_intersection() { + let mut b1 = MockBinding::default(); + b1.mode = TermMode::ALT_SCREEN | TermMode::APP_KEYPAD; + let mut b2 = MockBinding::default(); + b2.mode = TermMode::APP_KEYPAD | TermMode::APP_CURSOR; + + assert!(b1.triggers_match(&b2)); + } + + #[test] + fn binding_mismatches_notmode() { + let mut b1 = MockBinding::default(); + b1.mode = TermMode::ALT_SCREEN; + let mut b2 = MockBinding::default(); + b2.notmode = TermMode::ALT_SCREEN; + + assert!(!b1.triggers_match(&b2)); + } + + #[test] + fn binding_mismatches_unrelated() { + let mut b1 = MockBinding::default(); + b1.mode = TermMode::ALT_SCREEN; + let mut b2 = MockBinding::default(); + b2.mode = TermMode::APP_KEYPAD; + + assert!(!b1.triggers_match(&b2)); + } + + #[test] + fn binding_trigger_input() { + let mut binding = MockBinding::default(); + binding.trigger = 13; + + let mods = binding.mods; + let mode = binding.mode; + + assert!(binding.is_triggered_by(mode, mods, &13, true)); + assert!(!binding.is_triggered_by(mode, mods, &32, true)); + } + + #[test] + fn binding_trigger_mods() { + let mut binding = MockBinding::default(); + binding.mods = ModifiersState { alt: true, logo: true, ctrl: false, shift: false }; + + let superset_mods = ModifiersState { alt: true, logo: true, ctrl: true, shift: true }; + let subset_mods = ModifiersState { alt: false, logo: false, ctrl: false, shift: false }; + + let t = binding.trigger; + let mode = binding.mode; + + assert!(binding.is_triggered_by(mode, binding.mods, &t, true)); + assert!(binding.is_triggered_by(mode, binding.mods, &t, false)); + + assert!(binding.is_triggered_by(mode, superset_mods, &t, true)); + assert!(!binding.is_triggered_by(mode, superset_mods, &t, false)); + + assert!(!binding.is_triggered_by(mode, subset_mods, &t, true)); + assert!(!binding.is_triggered_by(mode, subset_mods, &t, false)); + } + + #[test] + fn binding_trigger_modes() { + let mut binding = MockBinding::default(); + binding.mode = TermMode::ALT_SCREEN; + + let t = binding.trigger; + let mods = binding.mods; + + assert!(!binding.is_triggered_by(TermMode::INSERT, mods, &t, true)); + assert!(binding.is_triggered_by(TermMode::ALT_SCREEN, mods, &t, true)); + assert!(binding.is_triggered_by(TermMode::ALT_SCREEN | TermMode::INSERT, mods, &t, true)); + } + + #[test] + fn binding_trigger_notmodes() { + let mut binding = MockBinding::default(); + binding.notmode = TermMode::ALT_SCREEN; + + let t = binding.trigger; + let mods = binding.mods; + + assert!(binding.is_triggered_by(TermMode::INSERT, mods, &t, true)); + assert!(!binding.is_triggered_by(TermMode::ALT_SCREEN, mods, &t, true)); + assert!(!binding.is_triggered_by(TermMode::ALT_SCREEN | TermMode::INSERT, mods, &t, true)); + } +} diff --git a/alacritty/src/config/mod.rs b/alacritty/src/config/mod.rs new file mode 100644 index 00000000..fe0ee7af --- /dev/null +++ b/alacritty/src/config/mod.rs @@ -0,0 +1,234 @@ +use std::borrow::Cow; +use std::env; +use std::fs::File; +use std::io::{self, Read, Write}; +use std::path::{Path, PathBuf}; + +#[cfg(windows)] +use dirs; +use log::{error, warn}; +use serde_yaml; +#[cfg(not(windows))] +use xdg; + +use alacritty_terminal::config::{ + Config as TermConfig, DEFAULT_ALACRITTY_CONFIG, LOG_TARGET_CONFIG, +}; + +mod bindings; +pub mod monitor; +mod mouse; +#[cfg(test)] +mod test; +mod ui_config; + +pub use crate::config::bindings::{Action, Binding, Key, RelaxedEq}; +#[cfg(test)] +pub use crate::config::mouse::{ClickHandler, Mouse}; +use crate::config::ui_config::UIConfig; + +pub type Config = TermConfig<UIConfig>; + +/// Result from config loading +pub type Result<T> = ::std::result::Result<T, Error>; + +/// 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), +} + +impl ::std::error::Error for Error { + fn cause(&self) -> Option<&dyn (::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 => "Couldn't 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, "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) + } +} + +/// 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(Into::into) +} + +#[cfg(windows)] +pub fn installed_config<'a>() -> Option<Cow<'a, Path>> { + dirs::config_dir() + .map(|path| path.join("alacritty\\alacritty.yml")) + .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"); + + std::fs::create_dir_all(path.parent().unwrap())?; + + File::create(&path)?.write_all(DEFAULT_ALACRITTY_CONFIG.as_bytes())?; + + Ok(path.into()) +} + +pub fn load_from(path: PathBuf) -> Config { + let mut config = reload_from(&path).unwrap_or_else(|_| Config::default()); + config.config_path = Some(path); + config +} + +pub fn reload_from(path: &PathBuf) -> Result<Config> { + match read_config(path) { + Ok(config) => Ok(config), + Err(err) => { + error!(target: LOG_TARGET_CONFIG, "Unable to load config {:?}: {}", path, err); + Err(err) + }, + } +} + +fn read_config(path: &PathBuf) -> Result<Config> { + let mut contents = String::new(); + File::open(path)?.read_to_string(&mut contents)?; + + // Remove UTF-8 BOM + if contents.chars().nth(0) == Some('\u{FEFF}') { + contents = contents.split_off(3); + } + + // Prevent parsing error with empty string + if contents.is_empty() { + return Ok(Config::default()); + } + + let config = serde_yaml::from_str(&contents)?; + + print_deprecation_warnings(&config); + + Ok(config) +} + +fn print_deprecation_warnings(config: &Config) { + if config.window.start_maximized.is_some() { + warn!( + target: LOG_TARGET_CONFIG, + "Config window.start_maximized is deprecated; please use window.startup_mode instead" + ); + } + + if config.render_timer.is_some() { + warn!( + target: LOG_TARGET_CONFIG, + "Config render_timer is deprecated; please use debug.render_timer instead" + ); + } + + if config.persistent_logging.is_some() { + warn!( + target: LOG_TARGET_CONFIG, + "Config persistent_logging is deprecated; please use debug.persistent_logging instead" + ); + } +} diff --git a/alacritty/src/config/monitor.rs b/alacritty/src/config/monitor.rs new file mode 100644 index 00000000..8dc5379a --- /dev/null +++ b/alacritty/src/config/monitor.rs @@ -0,0 +1,58 @@ +use std::path::PathBuf; +use std::sync::mpsc; +use std::time::Duration; + +use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; + +use alacritty_terminal::event::{Event, EventListener}; +use alacritty_terminal::util; + +use crate::event::EventProxy; + +pub struct Monitor { + _thread: ::std::thread::JoinHandle<()>, +} + +impl Monitor { + pub fn new<P>(path: P, event_proxy: EventProxy) -> Monitor + where + P: Into<PathBuf>, + { + let path = path.into(); + + Monitor { + _thread: 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) => { + if path != config_path { + continue; + } + + event_proxy.send_event(Event::ConfigReload(path)); + }, + _ => {}, + } + } + }), + } + } +} diff --git a/alacritty/src/config/mouse.rs b/alacritty/src/config/mouse.rs new file mode 100644 index 00000000..b7832b4a --- /dev/null +++ b/alacritty/src/config/mouse.rs @@ -0,0 +1,115 @@ +use std::time::Duration; + +use glutin::event::ModifiersState; +use log::error; +use serde::{Deserialize, Deserializer}; + +use alacritty_terminal::config::{failure_default, LOG_TARGET_CONFIG}; + +use crate::config::bindings::{CommandWrapper, ModsWrapper}; + +#[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, +} + +#[serde(default)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +pub struct Url { + // Program for opening links + #[serde(deserialize_with = "deserialize_launcher")] + pub launcher: Option<CommandWrapper>, + + // Modifier used to open links + #[serde(deserialize_with = "failure_default")] + modifiers: ModsWrapper, +} + +impl Url { + pub fn mods(&self) -> ModifiersState { + self.modifiers.into_inner() + } +} + +fn deserialize_launcher<'a, D>( + deserializer: D, +) -> ::std::result::Result<Option<CommandWrapper>, D::Error> +where + D: Deserializer<'a>, +{ + let default = Url::default().launcher; + + // Deserialize to generic value + let val = serde_yaml::Value::deserialize(deserializer)?; + + // Accept `None` to disable the launcher + if val.as_str().filter(|v| v.to_lowercase() == "none").is_some() { + return Ok(None); + } + + match <Option<CommandWrapper>>::deserialize(val) { + Ok(launcher) => Ok(launcher), + Err(err) => { + error!( + target: LOG_TARGET_CONFIG, + "Problem with config: {}; using {}", + err, + default.clone().unwrap().program() + ); + Ok(default) + }, + } +} + +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(), + } + } +} + +#[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: Deserializer<'a>, +{ + let value = serde_yaml::Value::deserialize(deserializer)?; + match u64::deserialize(value) { + Ok(threshold_ms) => Ok(Duration::from_millis(threshold_ms)), + Err(err) => { + error!(target: LOG_TARGET_CONFIG, "Problem with config: {}; using default value", err); + Ok(default_threshold_ms()) + }, + } +} diff --git a/alacritty/src/config/test.rs b/alacritty/src/config/test.rs new file mode 100644 index 00000000..8da6cef5 --- /dev/null +++ b/alacritty/src/config/test.rs @@ -0,0 +1,24 @@ +use alacritty_terminal::config::DEFAULT_ALACRITTY_CONFIG; + +use crate::config::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.ui_config.mouse_bindings.is_empty()); + + // Sanity check that key bindings are being parsed + assert!(!config.ui_config.key_bindings.is_empty()); +} + +#[test] +fn default_match_empty() { + let default = Config::default(); + + let empty = serde_yaml::from_str("key: val\n").unwrap(); + + assert_eq!(default, empty); +} diff --git a/alacritty/src/config/ui_config.rs b/alacritty/src/config/ui_config.rs new file mode 100644 index 00000000..6230c5bb --- /dev/null +++ b/alacritty/src/config/ui_config.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize, Deserializer}; + +use alacritty_terminal::config::failure_default; + +use crate::config::bindings::{self, Binding, KeyBinding, MouseBinding}; +use crate::config::mouse::Mouse; + +#[derive(Debug, PartialEq, Deserialize)] +pub struct UIConfig { + #[serde(default, deserialize_with = "failure_default")] + pub mouse: Mouse, + + /// Keybindings + #[serde(default = "default_key_bindings", deserialize_with = "deserialize_key_bindings")] + pub key_bindings: Vec<KeyBinding>, + + /// Bindings for the mouse + #[serde(default = "default_mouse_bindings", deserialize_with = "deserialize_mouse_bindings")] + pub mouse_bindings: Vec<MouseBinding>, +} + +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) -> Result<Vec<KeyBinding>, D::Error> +where + D: Deserializer<'a>, +{ + deserialize_bindings(deserializer, bindings::default_key_bindings()) +} + +fn deserialize_mouse_bindings<'a, D>(deserializer: D) -> Result<Vec<MouseBinding>, D::Error> +where + D: Deserializer<'a>, +{ + deserialize_bindings(deserializer, bindings::default_mouse_bindings()) +} + +fn deserialize_bindings<'a, D, T>( + deserializer: D, + mut default: Vec<Binding<T>>, +) -> Result<Vec<Binding<T>>, D::Error> +where + D: Deserializer<'a>, + T: Copy + Eq, + Binding<T>: Deserialize<'a>, +{ + let mut bindings: Vec<Binding<T>> = failure_default(deserializer)?; + + // Remove matching default bindings + for binding in bindings.iter() { + default.retain(|b| !b.triggers_match(binding)); + } + + bindings.extend(default); + + Ok(bindings) +} |