diff options
Diffstat (limited to 'alacritty')
-rw-r--r-- | alacritty/Cargo.toml | 14 | ||||
-rw-r--r-- | alacritty/src/cli.rs | 8 | ||||
-rw-r--r-- | alacritty/src/config/bindings.rs | 1409 | ||||
-rw-r--r-- | alacritty/src/config/mod.rs (renamed from alacritty/src/config.rs) | 27 | ||||
-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 | ||||
-rw-r--r-- | alacritty/src/display.rs | 476 | ||||
-rw-r--r-- | alacritty/src/event.rs | 651 | ||||
-rw-r--r-- | alacritty/src/input.rs | 1169 | ||||
-rw-r--r-- | alacritty/src/logging.rs | 22 | ||||
-rw-r--r-- | alacritty/src/main.rs | 182 | ||||
-rw-r--r-- | alacritty/src/window.rs | 434 |
14 files changed, 4515 insertions, 137 deletions
diff --git a/alacritty/Cargo.toml b/alacritty/Cargo.toml index c93f7c56..c47aae6c 100644 --- a/alacritty/Cargo.toml +++ b/alacritty/Cargo.toml @@ -14,9 +14,15 @@ clap = "2" log = "0.4" time = "0.1.40" env_logger = "0.6.0" -crossbeam-channel = "0.3.8" +serde = { version = "1", features = ["derive"] } serde_yaml = "0.8" serde_json = "1" +glutin = { git = "https://github.com/chrisduerr/glutin" } +notify = "4" +libc = "0.2" +unicode-width = "0.1" +parking_lot = "0.9" +font = { path = "../font" } [build-dependencies] rustc_tools_util = "0.2.0" @@ -24,9 +30,15 @@ rustc_tools_util = "0.2.0" [target.'cfg(not(windows))'.dependencies] xdg = "2" +[target.'cfg(not(target_os = "macos"))'.dependencies] +image = "0.21.0" + [target.'cfg(any(target_os = "macos", windows))'.dependencies] dirs = "1.0.2" +[target.'cfg(not(any(target_os="windows", target_os="macos")))'.dependencies] +x11-dl = "2" + [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.7", features = ["impl-default", "winuser", "synchapi", "roerrorapi", "winerror", "wincon", "wincontypes"]} diff --git a/alacritty/src/cli.rs b/alacritty/src/cli.rs index d48ab4d7..d5eb12d7 100644 --- a/alacritty/src/cli.rs +++ b/alacritty/src/cli.rs @@ -19,9 +19,10 @@ use std::path::{Path, PathBuf}; use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg}; use log::{self, LevelFilter}; -use alacritty_terminal::config::{Config, Delta, Dimensions, Shell}; +use alacritty_terminal::config::{Delta, Dimensions, Shell, DEFAULT_NAME}; use alacritty_terminal::index::{Column, Line}; -use alacritty_terminal::window::DEFAULT_NAME; + +use crate::config::Config; /// Options specified on the command line pub struct Options { @@ -283,9 +284,10 @@ impl Options { #[cfg(test)] mod test { - use alacritty_terminal::config::{Config, DEFAULT_ALACRITTY_CONFIG}; + use alacritty_terminal::config::DEFAULT_ALACRITTY_CONFIG; use crate::cli::Options; + use crate::config::Config; #[test] fn dynamic_title_ignoring_options_by_default() { 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.rs b/alacritty/src/config/mod.rs index 6d185fe7..fe0ee7af 100644 --- a/alacritty/src/config.rs +++ b/alacritty/src/config/mod.rs @@ -11,9 +11,23 @@ use serde_yaml; #[cfg(not(windows))] use xdg; -use alacritty_terminal::config::{Config, DEFAULT_ALACRITTY_CONFIG}; +use alacritty_terminal::config::{ + Config as TermConfig, DEFAULT_ALACRITTY_CONFIG, LOG_TARGET_CONFIG, +}; -pub const SOURCE_FILE_PATH: &str = file!(); +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>; @@ -169,7 +183,7 @@ pub fn reload_from(path: &PathBuf) -> Result<Config> { match read_config(path) { Ok(config) => Ok(config), Err(err) => { - error!("Unable to load config {:?}: {}", path, err); + error!(target: LOG_TARGET_CONFIG, "Unable to load config {:?}: {}", path, err); Err(err) }, } @@ -199,16 +213,21 @@ fn read_config(path: &PathBuf) -> Result<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!("Config render_timer is deprecated; please use debug.render_timer instead"); + 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) +} diff --git a/alacritty/src/display.rs b/alacritty/src/display.rs new file mode 100644 index 00000000..a8f72b3e --- /dev/null +++ b/alacritty/src/display.rs @@ -0,0 +1,476 @@ +// 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. + +//! The display subsystem including window management, font rasterization, and +//! GPU drawing. +use std::cmp::max; +use std::f64; +use std::fmt; +use std::time::Instant; + +use glutin::dpi::{PhysicalPosition, PhysicalSize}; +use glutin::event_loop::EventLoop; +use log::{debug, info}; +use parking_lot::MutexGuard; + +use font::{self, Rasterize, Size}; + +use alacritty_terminal::config::StartupMode; +use alacritty_terminal::event::{Event, OnResize}; +use alacritty_terminal::index::Line; +use alacritty_terminal::message_bar::MessageBuffer; +use alacritty_terminal::meter::Meter; +use alacritty_terminal::renderer::rects::{RenderLines, RenderRect}; +use alacritty_terminal::renderer::{self, GlyphCache, QuadRenderer}; +use alacritty_terminal::term::color::Rgb; +use alacritty_terminal::term::{RenderableCell, SizeInfo, Term}; + +use crate::config::Config; +use crate::event::{FontResize, Resize}; +use crate::window::{self, Window}; + +/// Font size change interval +pub const FONT_SIZE_STEP: f32 = 0.5; + +#[derive(Debug)] +pub enum Error { + /// Error with window management + Window(window::Error), + + /// Error dealing with fonts + Font(font::Error), + + /// Error in renderer + Render(renderer::Error), + + /// Error during buffer swap + ContextError(glutin::ContextError), +} + +impl std::error::Error for Error { + fn cause(&self) -> Option<&dyn (std::error::Error)> { + match *self { + Error::Window(ref err) => Some(err), + Error::Font(ref err) => Some(err), + Error::Render(ref err) => Some(err), + Error::ContextError(ref err) => Some(err), + } + } + + fn description(&self) -> &str { + match *self { + Error::Window(ref err) => err.description(), + Error::Font(ref err) => err.description(), + Error::Render(ref err) => err.description(), + Error::ContextError(ref err) => err.description(), + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Error::Window(ref err) => err.fmt(f), + Error::Font(ref err) => err.fmt(f), + Error::Render(ref err) => err.fmt(f), + Error::ContextError(ref err) => err.fmt(f), + } + } +} + +impl From<window::Error> for Error { + fn from(val: window::Error) -> Error { + Error::Window(val) + } +} + +impl From<font::Error> for Error { + fn from(val: font::Error) -> Error { + Error::Font(val) + } +} + +impl From<renderer::Error> for Error { + fn from(val: renderer::Error) -> Error { + Error::Render(val) + } +} + +impl From<glutin::ContextError> for Error { + fn from(val: glutin::ContextError) -> Error { + Error::ContextError(val) + } +} + +/// The display wraps a window, font rasterizer, and GPU renderer +pub struct Display { + pub size_info: SizeInfo, + pub font_size: Size, + pub window: Window, + + renderer: QuadRenderer, + glyph_cache: GlyphCache, + meter: Meter, +} + +impl Display { + pub fn new(config: &Config, event_loop: &EventLoop<Event>) -> Result<Display, Error> { + // Guess DPR based on first monitor + let estimated_dpr = + event_loop.available_monitors().next().map(|m| m.hidpi_factor()).unwrap_or(1.); + + // Guess the target window dimensions + let metrics = GlyphCache::static_metrics(config.font.clone(), estimated_dpr)?; + let (cell_width, cell_height) = compute_cell_size(config, &metrics); + let dimensions = + GlyphCache::calculate_dimensions(config, estimated_dpr, cell_width, cell_height); + + debug!("Estimated DPR: {}", estimated_dpr); + debug!("Estimated Cell Size: {} x {}", cell_width, cell_height); + debug!("Estimated Dimensions: {:?}", dimensions); + + // Create the window where Alacritty will be displayed + let logical = dimensions.map(|d| PhysicalSize::new(d.0, d.1).to_logical(estimated_dpr)); + + // Spawn window + let mut window = Window::new(event_loop, &config, logical)?; + + let dpr = window.hidpi_factor(); + info!("Device pixel ratio: {}", dpr); + + // get window properties for initializing the other subsystems + let mut viewport_size = window.inner_size().to_physical(dpr); + + // Create renderer + let mut renderer = QuadRenderer::new()?; + + let (glyph_cache, cell_width, cell_height) = + Self::new_glyph_cache(dpr, &mut renderer, config)?; + + let mut padding_x = f32::from(config.window.padding.x) * dpr as f32; + let mut padding_y = f32::from(config.window.padding.y) * dpr as f32; + + if let Some((width, height)) = + GlyphCache::calculate_dimensions(config, dpr, cell_width, cell_height) + { + let PhysicalSize { width: w, height: h } = window.inner_size().to_physical(dpr); + if (w - width).abs() < f64::EPSILON && (h - height).abs() < f64::EPSILON { + info!("Estimated DPR correctly, skipping resize"); + } else { + viewport_size = PhysicalSize::new(width, height); + window.set_inner_size(viewport_size.to_logical(dpr)); + } + } else if config.window.dynamic_padding { + // Make sure additional padding is spread evenly + padding_x = dynamic_padding(padding_x, viewport_size.width as f32, cell_width); + padding_y = dynamic_padding(padding_y, viewport_size.height as f32, cell_height); + } + + padding_x = padding_x.floor(); + padding_y = padding_y.floor(); + + info!("Cell Size: {} x {}", cell_width, cell_height); + info!("Padding: {} x {}", padding_x, padding_y); + + let size_info = SizeInfo { + dpr, + width: viewport_size.width as f32, + height: viewport_size.height as f32, + cell_width: cell_width as f32, + cell_height: cell_height as f32, + padding_x: padding_x as f32, + padding_y: padding_y as f32, + }; + + // Update OpenGL projection + renderer.resize(&size_info); + + // Clear screen + let background_color = config.colors.primary.background; + renderer.with_api(&config, &size_info, |api| { + api.clear(background_color); + }); + + // We should call `clear` when window is offscreen, so when `window.show()` happens it + // would be with background color instead of uninitialized surface. + window.swap_buffers(); + + window.set_visible(true); + + // Set window position + // + // TODO: replace `set_position` with `with_position` once available + // Upstream issue: https://github.com/tomaka/winit/issues/806 + if let Some(position) = config.window.position { + let physical = PhysicalPosition::from((position.x, position.y)); + let logical = physical.to_logical(dpr); + window.set_outer_position(logical); + } + + #[allow(clippy::single_match)] + match config.window.startup_mode() { + StartupMode::Fullscreen => window.set_fullscreen(true), + #[cfg(target_os = "macos")] + StartupMode::SimpleFullscreen => window.set_simple_fullscreen(true), + #[cfg(not(any(target_os = "macos", windows)))] + StartupMode::Maximized => window.set_maximized(true), + _ => (), + } + + Ok(Display { + window, + renderer, + glyph_cache, + meter: Meter::new(), + size_info, + font_size: config.font.size, + }) + } + + fn new_glyph_cache( + dpr: f64, + renderer: &mut QuadRenderer, + config: &Config, + ) -> Result<(GlyphCache, f32, f32), Error> { + let font = config.font.clone(); + let rasterizer = font::Rasterizer::new(dpr as f32, config.font.use_thin_strokes())?; + + // Initialize glyph cache + let glyph_cache = { + info!("Initializing glyph cache..."); + let init_start = Instant::now(); + + let cache = + renderer.with_loader(|mut api| GlyphCache::new(rasterizer, &font, &mut api))?; + + let stop = init_start.elapsed(); + let stop_f = stop.as_secs() as f64 + f64::from(stop.subsec_nanos()) / 1_000_000_000f64; + info!("... finished initializing glyph cache in {}s", stop_f); + + cache + }; + + // Need font metrics to resize the window properly. This suggests to me the + // font metrics should be computed before creating the window in the first + // place so that a resize is not needed. + let (cw, ch) = compute_cell_size(config, &glyph_cache.font_metrics()); + + Ok((glyph_cache, cw, ch)) + } + + /// Update font size and cell dimensions + fn update_glyph_cache(&mut self, config: &Config, size: Size) { + let size_info = &mut self.size_info; + let cache = &mut self.glyph_cache; + + let font = config.font.clone().with_size(size); + + self.renderer.with_loader(|mut api| { + let _ = cache.update_font_size(font, size_info.dpr, &mut api); + }); + + // Update cell size + let (cell_width, cell_height) = compute_cell_size(config, &self.glyph_cache.font_metrics()); + size_info.cell_width = cell_width; + size_info.cell_height = cell_height; + } + + /// Process resize events + pub fn handle_resize<T>( + &mut self, + terminal: &mut Term<T>, + pty_resize_handle: &mut dyn OnResize, + message_buffer: &MessageBuffer, + config: &Config, + resize_pending: Resize, + ) { + // Update font size and cell dimensions + if let Some(resize) = resize_pending.font_size { + self.font_size = match resize { + FontResize::Delta(delta) => max(self.font_size + delta, FONT_SIZE_STEP.into()), + FontResize::Reset => config.font.size, + }; + + self.update_glyph_cache(config, self.font_size); + } + + // Update the window dimensions + if let Some(size) = resize_pending.dimensions { + self.size_info.width = size.width as f32; + self.size_info.height = size.height as f32; + } + + let dpr = self.size_info.dpr; + let width = self.size_info.width; + let height = self.size_info.height; + let cell_width = self.size_info.cell_width; + let cell_height = self.size_info.cell_height; + + // Recalculate padding + let mut padding_x = f32::from(config.window.padding.x) * dpr as f32; + let mut padding_y = f32::from(config.window.padding.y) * dpr as f32; + + if config.window.dynamic_padding { + padding_x = dynamic_padding(padding_x, width, cell_width); + padding_y = dynamic_padding(padding_y, height, cell_height); + } + + self.size_info.padding_x = padding_x.floor() as f32; + self.size_info.padding_y = padding_y.floor() as f32; + + let mut pty_size = self.size_info; + + // Subtract message bar lines from pty size + if resize_pending.message_buffer.is_some() { + let lines = + message_buffer.message().map(|m| m.text(&self.size_info).len()).unwrap_or(0); + pty_size.height -= pty_size.cell_height * lines as f32; + } + + // Resize PTY + pty_resize_handle.on_resize(&pty_size); + + // Resize terminal + terminal.resize(&pty_size); + + // Resize renderer + let physical = + PhysicalSize::new(f64::from(self.size_info.width), f64::from(self.size_info.height)); + self.renderer.resize(&self.size_info); + self.window.resize(physical); + } + + /// Draw the screen + /// + /// A reference to Term whose state is being drawn must be provided. + /// + /// This call may block if vsync is enabled + pub fn draw<T>( + &mut self, + terminal: MutexGuard<'_, Term<T>>, + message_buffer: &MessageBuffer, + config: &Config, + ) { + let grid_cells: Vec<RenderableCell> = terminal.renderable_cells(config).collect(); + let visual_bell_intensity = terminal.visual_bell.intensity(); + let background_color = terminal.background_color(); + let metrics = self.glyph_cache.font_metrics(); + let glyph_cache = &mut self.glyph_cache; + let size_info = self.size_info; + + // Update IME position + #[cfg(not(windows))] + self.window.update_ime_position(&terminal, &self.size_info); + + // Drop terminal as early as possible to free lock + drop(terminal); + + self.renderer.with_api(&config, &size_info, |api| { + api.clear(background_color); + }); + + let mut lines = RenderLines::new(); + + // Draw grid + { + let _sampler = self.meter.sampler(); + + self.renderer.with_api(&config, &size_info, |mut api| { + // Iterate over all non-empty cells in the grid + for cell in grid_cells { + // Update underline/strikeout + lines.update(cell); + + // Draw the cell + api.render_cell(cell, glyph_cache); + } + }); + } + + let mut rects = lines.into_rects(&metrics, &size_info); + + if let Some(message) = message_buffer.message() { + let text = message.text(&size_info); + + // Create a new rectangle for the background + let start_line = size_info.lines().0 - text.len(); + let y = size_info.padding_y + size_info.cell_height * start_line as f32; + rects.push(RenderRect::new( + 0., + y, + size_info.width, + size_info.height - y, + message.color(), + )); + + // Draw rectangles including the new background + self.renderer.draw_rects( + &size_info, + config.visual_bell.color, + visual_bell_intensity, + rects, + ); + + // Relay messages to the user + let mut offset = 1; + for message_text in text.iter().rev() { + self.renderer.with_api(&config, &size_info, |mut api| { + api.render_string( + &message_text, + Line(size_info.lines().saturating_sub(offset)), + glyph_cache, + None, + ); + }); + offset += 1; + } + } else { + // Draw rectangles + self.renderer.draw_rects( + &size_info, + config.visual_bell.color, + visual_bell_intensity, + rects, + ); + } + + // Draw render timer + if config.render_timer() { + let timing = format!("{:.3} usec", self.meter.average()); + let color = Rgb { r: 0xd5, g: 0x4e, b: 0x53 }; + self.renderer.with_api(&config, &size_info, |mut api| { + api.render_string(&timing[..], size_info.lines() - 2, glyph_cache, Some(color)); + }); + } + + self.window.swap_buffers(); + } +} + +/// Calculate padding to spread it evenly around the terminal content +#[inline] +fn dynamic_padding(padding: f32, dimension: f32, cell_dimension: f32) -> f32 { + padding + ((dimension - 2. * padding) % cell_dimension) / 2. +} + +/// Calculate the cell dimensions based on font metrics. +#[inline] +fn compute_cell_size(config: &Config, metrics: &font::Metrics) -> (f32, f32) { + let offset_x = f64::from(config.font.offset.x); + let offset_y = f64::from(config.font.offset.y); + ( + f32::max(1., ((metrics.average_advance + offset_x) as f32).floor()), + f32::max(1., ((metrics.line_height + offset_y) as f32).floor()), + ) +} diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs new file mode 100644 index 00000000..984352d4 --- /dev/null +++ b/alacritty/src/event.rs @@ -0,0 +1,651 @@ +//! Process window events +use std::borrow::Cow; +use std::env; +#[cfg(unix)] +use std::fs; +use std::fs::File; +use std::io::Write; +use std::sync::Arc; +use std::time::Instant; + +use glutin::dpi::PhysicalSize; +use glutin::event::{ElementState, Event as GlutinEvent, MouseButton}; +use glutin::event_loop::{ControlFlow, EventLoop, EventLoopProxy}; +use glutin::platform::desktop::EventLoopExtDesktop; +#[cfg(not(any(target_os = "macos", windows)))] +use glutin::platform::unix::EventLoopWindowTargetExtUnix; +use log::{debug, info, warn}; +use serde_json as json; + +use font::Size; + +use alacritty_terminal::clipboard::ClipboardType; +use alacritty_terminal::config::LOG_TARGET_CONFIG; +use alacritty_terminal::event::OnResize; +use alacritty_terminal::event::{Event, EventListener, Notify}; +use alacritty_terminal::grid::Scroll; +use alacritty_terminal::index::{Column, Line, Point, Side}; +use alacritty_terminal::message_bar::{Message, MessageBuffer}; +use alacritty_terminal::selection::Selection; +use alacritty_terminal::sync::FairMutex; +use alacritty_terminal::term::cell::Cell; +use alacritty_terminal::term::{SizeInfo, Term}; +use alacritty_terminal::tty; +use alacritty_terminal::util::{limit, start_daemon}; + +use crate::config; +use crate::config::Config; +use crate::display::Display; +use crate::input::{self, ActionContext as _, Modifiers}; +use crate::window::Window; + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum FontResize { + Delta(f32), + Reset, +} + +#[derive(Default, Copy, Clone, Debug, PartialEq)] +pub struct Resize { + pub dimensions: Option<PhysicalSize>, + pub message_buffer: Option<()>, + pub font_size: Option<FontResize>, +} + +impl Resize { + fn is_empty(&self) -> bool { + self.dimensions.is_none() && self.font_size.is_none() && self.message_buffer.is_none() + } +} + +pub struct ActionContext<'a, N, T> { + pub notifier: &'a mut N, + pub terminal: &'a mut Term<T>, + pub size_info: &'a mut SizeInfo, + pub mouse: &'a mut Mouse, + pub received_count: &'a mut usize, + pub suppress_chars: &'a mut bool, + pub modifiers: &'a mut Modifiers, + pub window: &'a mut Window, + pub message_buffer: &'a mut MessageBuffer, + pub resize_pending: &'a mut Resize, + pub font_size: &'a Size, +} + +impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionContext<'a, N, T> { + fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, val: B) { + self.notifier.notify(val); + } + + fn size_info(&self) -> SizeInfo { + *self.size_info + } + + fn scroll(&mut self, scroll: Scroll) { + self.terminal.scroll_display(scroll); + + if let ElementState::Pressed = self.mouse().left_button_state { + let (x, y) = (self.mouse().x, self.mouse().y); + let size_info = self.size_info(); + let point = size_info.pixels_to_coords(x, y); + let cell_side = self.mouse().cell_side; + self.update_selection(Point { line: point.line, col: point.col }, cell_side); + } + } + + fn copy_selection(&mut self, ty: ClipboardType) { + if let Some(selected) = self.terminal.selection_to_string() { + if !selected.is_empty() { + self.terminal.clipboard().store(ty, selected); + } + } + } + + fn selection_is_empty(&self) -> bool { + self.terminal.selection().as_ref().map(Selection::is_empty).unwrap_or(true) + } + + fn clear_selection(&mut self) { + *self.terminal.selection_mut() = None; + self.terminal.dirty = true; + } + + fn update_selection(&mut self, point: Point, side: Side) { + let point = self.terminal.visible_to_buffer(point); + + // Update selection if one exists + if let Some(ref mut selection) = self.terminal.selection_mut() { + selection.update(point, side); + } + + self.terminal.dirty = true; + } + + fn simple_selection(&mut self, point: Point, side: Side) { + let point = self.terminal.visible_to_buffer(point); + *self.terminal.selection_mut() = Some(Selection::simple(point, side)); + self.terminal.dirty = true; + } + + fn block_selection(&mut self, point: Point, side: Side) { + let point = self.terminal.visible_to_buffer(point); + *self.terminal.selection_mut() = Some(Selection::block(point, side)); + self.terminal.dirty = true; + } + + fn semantic_selection(&mut self, point: Point) { + let point = self.terminal.visible_to_buffer(point); + *self.terminal.selection_mut() = Some(Selection::semantic(point)); + self.terminal.dirty = true; + } + + fn line_selection(&mut self, point: Point) { + let point = self.terminal.visible_to_buffer(point); + *self.terminal.selection_mut() = Some(Selection::lines(point)); + self.terminal.dirty = true; + } + + fn mouse_coords(&self) -> Option<Point> { + let x = self.mouse.x as usize; + let y = self.mouse.y as usize; + + if self.size_info.contains_point(x, y, true) { + Some(self.size_info.pixels_to_coords(x, y)) + } else { + None + } + } + + #[inline] + fn mouse_mut(&mut self) -> &mut Mouse { + self.mouse + } + + #[inline] + fn mouse(&self) -> &Mouse { + self.mouse + } + + #[inline] + fn received_count(&mut self) -> &mut usize { + &mut self.received_count + } + + #[inline] + fn suppress_chars(&mut self) -> &mut bool { + &mut self.suppress_chars + } + + #[inline] + fn modifiers(&mut self) -> &mut Modifiers { + &mut self.modifiers + } + + #[inline] + fn window(&self) -> &Window { + self.window + } + + #[inline] + fn window_mut(&mut self) -> &mut Window { + self.window + } + + #[inline] + fn terminal(&self) -> &Term<T> { + self.terminal + } + + #[inline] + fn terminal_mut(&mut self) -> &mut Term<T> { + self.terminal + } + + fn spawn_new_instance(&mut self) { + let alacritty = env::args().next().unwrap(); + + #[cfg(unix)] + let args = { + #[cfg(not(target_os = "freebsd"))] + let proc_prefix = ""; + #[cfg(target_os = "freebsd")] + let proc_prefix = "/compat/linux"; + let link_path = format!("{}/proc/{}/cwd", proc_prefix, tty::child_pid()); + if let Ok(path) = fs::read_link(link_path) { + vec!["--working-directory".into(), path] + } else { + Vec::new() + } + }; + #[cfg(not(unix))] + let args: Vec<String> = Vec::new(); + + match start_daemon(&alacritty, &args) { + Ok(_) => debug!("Started new Alacritty process: {} {:?}", alacritty, args), + Err(_) => warn!("Unable to start new Alacritty process: {} {:?}", alacritty, args), + } + } + + fn change_font_size(&mut self, delta: f32) { + self.resize_pending.font_size = Some(FontResize::Delta(delta)); + self.terminal.dirty = true; + } + + fn reset_font_size(&mut self) { + self.resize_pending.font_size = Some(FontResize::Reset); + self.terminal.dirty = true; + } + + fn pop_message(&mut self) { + self.resize_pending.message_buffer = Some(()); + self.message_buffer.pop(); + } + + fn message(&self) -> Option<&Message> { + self.message_buffer.message() + } +} + +pub enum ClickState { + None, + Click, + DoubleClick, + TripleClick, +} + +/// State of the mouse +pub struct Mouse { + pub x: usize, + pub y: usize, + pub left_button_state: ElementState, + pub middle_button_state: ElementState, + pub right_button_state: ElementState, + pub last_click_timestamp: Instant, + pub click_state: ClickState, + pub scroll_px: i32, + pub line: Line, + pub column: Column, + pub cell_side: Side, + pub lines_scrolled: f32, + pub block_url_launcher: bool, + pub last_button: MouseButton, +} + +impl Default for Mouse { + fn default() -> Mouse { + Mouse { + x: 0, + y: 0, + last_click_timestamp: Instant::now(), + left_button_state: ElementState::Released, + middle_button_state: ElementState::Released, + right_button_state: ElementState::Released, + click_state: ClickState::None, + scroll_px: 0, + line: Line(0), + column: Column(0), + cell_side: Side::Left, + lines_scrolled: 0.0, + block_url_launcher: false, + last_button: MouseButton::Other(0), + } + } +} + +/// The event processor +/// +/// Stores some state from received events and dispatches actions when they are +/// triggered. +pub struct Processor<N> { + notifier: N, + mouse: Mouse, + received_count: usize, + suppress_chars: bool, + modifiers: Modifiers, + config: Config, + pty_resize_handle: Box<dyn OnResize>, + message_buffer: MessageBuffer, + display: Display, +} + +impl<N: Notify> Processor<N> { + /// Create a new event processor + /// + /// Takes a writer which is expected to be hooked up to the write end of a + /// pty. + pub fn new( + notifier: N, + pty_resize_handle: Box<dyn OnResize>, + message_buffer: MessageBuffer, + config: Config, + display: Display, + ) -> Processor<N> { + Processor { + notifier, + mouse: Default::default(), + received_count: 0, + suppress_chars: false, + modifiers: Default::default(), + config, + pty_resize_handle, + message_buffer, + display, + } + } + + /// Run the event loop. + pub fn run<T>(&mut self, terminal: Arc<FairMutex<Term<T>>>, mut event_loop: EventLoop<Event>) + where + T: EventListener, + { + #[cfg(not(any(target_os = "macos", windows)))] + let mut dpr_initialized = false; + + let mut event_queue = Vec::new(); + + event_loop.run_return(|event, _event_loop, control_flow| { + if self.config.debug.print_events { + info!("glutin event: {:?}", event); + } + + match (&event, tty::process_should_exit()) { + // Check for shutdown + (GlutinEvent::UserEvent(Event::Exit), _) | (_, true) => { + *control_flow = ControlFlow::Exit; + return; + }, + // Process events + (GlutinEvent::EventsCleared, _) => { + *control_flow = ControlFlow::Wait; + + if event_queue.is_empty() { + return; + } + }, + // Buffer events + _ => { + *control_flow = ControlFlow::Poll; + if !Self::skip_event(&event) { + event_queue.push(event); + } + return; + }, + } + + let mut terminal = terminal.lock(); + + let mut resize_pending = Resize::default(); + + let context = ActionContext { + terminal: &mut terminal, + notifier: &mut self.notifier, + mouse: &mut self.mouse, + size_info: &mut self.display.size_info, + received_count: &mut self.received_count, + suppress_chars: &mut self.suppress_chars, + modifiers: &mut self.modifiers, + message_buffer: &mut self.message_buffer, + resize_pending: &mut resize_pending, + window: &mut self.display.window, + font_size: &self.display.font_size, + }; + let mut processor = input::Processor::new(context, &mut self.config); + + for event in event_queue.drain(..) { + Processor::handle_event(event, &mut processor); + } + + // TODO: Workaround for incorrect startup DPI on X11 + // https://github.com/rust-windowing/winit/issues/998 + #[cfg(not(any(target_os = "macos", windows)))] + { + if !dpr_initialized && _event_loop.is_x11() { + dpr_initialized = true; + + let dpr = self.display.window.hidpi_factor(); + self.display.size_info.dpr = dpr; + + let size = self.display.window.inner_size().to_physical(dpr); + + resize_pending.font_size = Some(FontResize::Delta(0.)); + resize_pending.dimensions = Some(size); + + terminal.dirty = true; + } + } + + // Process resize events + if !resize_pending.is_empty() { + self.display.handle_resize( + &mut terminal, + self.pty_resize_handle.as_mut(), + &self.message_buffer, + &self.config, + resize_pending, + ); + } + + if terminal.dirty { + // Clear dirty flag + terminal.dirty = !terminal.visual_bell.completed(); + + // Redraw screen + self.display.draw(terminal, &self.message_buffer, &self.config); + } + }); + + // Write ref tests to disk + self.write_ref_test_results(&terminal.lock()); + } + + /// Handle events from glutin + /// + /// Doesn't take self mutably due to borrow checking. Kinda uggo but w/e. + fn handle_event<T>( + event: GlutinEvent<Event>, + processor: &mut input::Processor<T, ActionContext<N, T>>, + ) where + T: EventListener, + { + match event { + GlutinEvent::UserEvent(event) => match event { + Event::Title(title) => processor.ctx.window.set_title(&title), + Event::Wakeup => processor.ctx.terminal.dirty = true, + Event::Urgent => { + processor.ctx.window.set_urgent(!processor.ctx.terminal.is_focused) + }, + Event::ConfigReload(path) => { + processor.ctx.message_buffer.remove_target(LOG_TARGET_CONFIG); + processor.ctx.resize_pending.message_buffer = Some(()); + + if let Ok(config) = config::reload_from(&path) { + processor.ctx.terminal.update_config(&config); + + if *processor.ctx.font_size == processor.config.font.size { + processor.ctx.resize_pending.font_size = Some(FontResize::Reset); + } + + *processor.config = config; + + processor.ctx.terminal.dirty = true; + } + }, + Event::Message(message) => { + processor.ctx.message_buffer.push(message); + processor.ctx.resize_pending.message_buffer = Some(()); + processor.ctx.terminal.dirty = true; + }, + Event::MouseCursorDirty => processor.reset_mouse_cursor(), + Event::Exit => (), + }, + GlutinEvent::WindowEvent { event, window_id, .. } => { + use glutin::event::WindowEvent::*; + match event { + CloseRequested => processor.ctx.terminal.exit(), + Resized(lsize) => { + let psize = lsize.to_physical(processor.ctx.size_info.dpr); + processor.ctx.resize_pending.dimensions = Some(psize); + processor.ctx.terminal.dirty = true; + }, + KeyboardInput { input, .. } => { + processor.process_key(input); + if input.state == ElementState::Pressed { + // Hide cursor while typing + if processor.config.ui_config.mouse.hide_when_typing { + processor.ctx.window.set_mouse_visible(false); + } + } + }, + ReceivedCharacter(c) => processor.received_char(c), + MouseInput { state, button, modifiers, .. } => { + if !cfg!(target_os = "macos") || processor.ctx.terminal.is_focused { + processor.ctx.window.set_mouse_visible(true); + processor.mouse_input(state, button, modifiers); + processor.ctx.terminal.dirty = true; + } + }, + CursorMoved { position: lpos, modifiers, .. } => { + let (x, y) = lpos.to_physical(processor.ctx.size_info.dpr).into(); + let x: i32 = limit(x, 0, processor.ctx.size_info.width as i32); + let y: i32 = limit(y, 0, processor.ctx.size_info.height as i32); + + processor.ctx.window.set_mouse_visible(true); + processor.mouse_moved(x as usize, y as usize, modifiers); + }, + MouseWheel { delta, phase, modifiers, .. } => { + processor.ctx.window.set_mouse_visible(true); + processor.on_mouse_wheel(delta, phase, modifiers); + }, + Focused(is_focused) => { + if window_id == processor.ctx.window.window_id() { + processor.ctx.terminal.is_focused = is_focused; + processor.ctx.terminal.dirty = true; + + if is_focused { + processor.ctx.window.set_urgent(false); + } else { + processor.ctx.window.set_mouse_visible(true); + } + + processor.on_focus_change(is_focused); + } + }, + DroppedFile(path) => { + let path: String = path.to_string_lossy().into(); + processor.ctx.write_to_pty(path.into_bytes()); + }, + HiDpiFactorChanged(dpr) => { + let dpr_change = (dpr / processor.ctx.size_info.dpr) as f32; + let resize_pending = &mut processor.ctx.resize_pending; + + // Push current font to update its DPR + resize_pending.font_size = Some(FontResize::Delta(0.)); + + // Scale window dimensions with new DPR + let old_width = processor.ctx.size_info.width; + let old_height = processor.ctx.size_info.height; + let dimensions = resize_pending.dimensions.get_or_insert_with(|| { + PhysicalSize::new(f64::from(old_width), f64::from(old_height)) + }); + dimensions.width *= f64::from(dpr_change); + dimensions.height *= f64::from(dpr_change); + + processor.ctx.terminal.dirty = true; + processor.ctx.size_info.dpr = dpr; + }, + RedrawRequested => processor.ctx.terminal.dirty = true, + TouchpadPressure { .. } + | CursorEntered { .. } + | CursorLeft { .. } + | AxisMotion { .. } + | HoveredFileCancelled + | Destroyed + | HoveredFile(_) + | Touch(_) + | Moved(_) => (), + // TODO: Add support for proper modifier handling + ModifiersChanged { .. } => (), + } + }, + GlutinEvent::DeviceEvent { .. } + | GlutinEvent::Suspended { .. } + | GlutinEvent::NewEvents { .. } + | GlutinEvent::EventsCleared + | GlutinEvent::Resumed + | GlutinEvent::LoopDestroyed => (), + } + } + + /// Check if an event is irrelevant and can be skipped + fn skip_event(event: &GlutinEvent<Event>) -> bool { + match event { + GlutinEvent::UserEvent(Event::Exit) => true, + GlutinEvent::WindowEvent { event, .. } => { + use glutin::event::WindowEvent::*; + match event { + TouchpadPressure { .. } + | CursorEntered { .. } + | CursorLeft { .. } + | AxisMotion { .. } + | HoveredFileCancelled + | Destroyed + | HoveredFile(_) + | Touch(_) + | Moved(_) => true, + _ => false, + } + }, + GlutinEvent::DeviceEvent { .. } + | GlutinEvent::Suspended { .. } + | GlutinEvent::NewEvents { .. } + | GlutinEvent::EventsCleared + | GlutinEvent::LoopDestroyed => true, + _ => false, + } + } + + // Write the ref test results to the disk + pub fn write_ref_test_results<T>(&self, terminal: &Term<T>) { + if !self.config.debug.ref_test { + return; + } + + // dump grid state + let mut grid = terminal.grid().clone(); + grid.initialize_all(&Cell::default()); + grid.truncate(); + + let serialized_grid = json::to_string(&grid).expect("serialize grid"); + + let serialized_size = json::to_string(&self.display.size_info).expect("serialize size"); + + let serialized_config = format!("{{\"history_size\":{}}}", grid.history_size()); + + File::create("./grid.json") + .and_then(|mut f| f.write_all(serialized_grid.as_bytes())) + .expect("write grid.json"); + + File::create("./size.json") + .and_then(|mut f| f.write_all(serialized_size.as_bytes())) + .expect("write size.json"); + + File::create("./config.json") + .and_then(|mut f| f.write_all(serialized_config.as_bytes())) + .expect("write config.json"); + } +} + +#[derive(Debug, Clone)] +pub struct EventProxy(EventLoopProxy<Event>); + +impl EventProxy { + pub fn new(proxy: EventLoopProxy<Event>) -> Self { + EventProxy(proxy) + } +} + +impl EventListener for EventProxy { + fn send_event(&self, event: Event) { + let _ = self.0.send_event(event); + } +} diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs new file mode 100644 index 00000000..be6a030e --- /dev/null +++ b/alacritty/src/input.rs @@ -0,0 +1,1169 @@ +// 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. +// +//! Handle input from glutin +//! +//! Certain key combinations should send some escape sequence back to the pty. +//! In order to figure that out, state about which modifier keys are pressed +//! needs to be tracked. Additionally, we need a bit of a state machine to +//! determine what to do when a non-modifier key is pressed. +use std::borrow::Cow; +use std::marker::PhantomData; +use std::mem; +use std::time::Instant; + +use glutin::event::{ + ElementState, KeyboardInput, ModifiersState, MouseButton, MouseScrollDelta, TouchPhase, + VirtualKeyCode, +}; +use glutin::window::CursorIcon; +use log::{debug, trace, warn}; + +use alacritty_terminal::ansi::{ClearMode, Handler}; +use alacritty_terminal::clipboard::ClipboardType; +use alacritty_terminal::event::EventListener; +use alacritty_terminal::grid::Scroll; +use alacritty_terminal::index::{Column, Line, Point, Side}; +use alacritty_terminal::message_bar::{self, Message}; +use alacritty_terminal::term::mode::TermMode; +use alacritty_terminal::term::{SizeInfo, Term}; +use alacritty_terminal::url::Url; +use alacritty_terminal::util::start_daemon; + +use crate::config::{Action, Binding, Config, Key, RelaxedEq}; +use crate::display::FONT_SIZE_STEP; +use crate::event::{ClickState, Mouse}; +use crate::window::Window; + +/// Processes input from glutin. +/// +/// An escape sequence may be emitted in case specific keys or key combinations +/// are activated. +pub struct Processor<'a, T: EventListener, A: ActionContext<T> + 'a> { + pub ctx: A, + pub config: &'a mut Config, + _phantom: PhantomData<T>, +} + +pub trait ActionContext<T: EventListener> { + fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, _: B); + fn size_info(&self) -> SizeInfo; + fn copy_selection(&mut self, _: ClipboardType); + fn clear_selection(&mut self); + fn update_selection(&mut self, point: Point, side: Side); + fn simple_selection(&mut self, point: Point, side: Side); + fn block_selection(&mut self, point: Point, side: Side); + fn semantic_selection(&mut self, point: Point); + fn line_selection(&mut self, point: Point); + fn selection_is_empty(&self) -> bool; + fn mouse_mut(&mut self) -> &mut Mouse; + fn mouse(&self) -> &Mouse; + fn mouse_coords(&self) -> Option<Point>; + fn received_count(&mut self) -> &mut usize; + fn suppress_chars(&mut self) -> &mut bool; + fn modifiers(&mut self) -> &mut Modifiers; + fn scroll(&mut self, scroll: Scroll); + fn window(&self) -> &Window; + fn window_mut(&mut self) -> &mut Window; + fn terminal(&self) -> &Term<T>; + fn terminal_mut(&mut self) -> &mut Term<T>; + fn spawn_new_instance(&mut self); + fn change_font_size(&mut self, delta: f32); + fn reset_font_size(&mut self); + fn pop_message(&mut self); + fn message(&self) -> Option<&Message>; +} + +#[derive(Debug, Default, Copy, Clone)] +pub struct Modifiers { + mods: ModifiersState, + lshift: bool, + rshift: bool, +} + +impl Modifiers { + pub fn update(&mut self, input: KeyboardInput) { + match input.virtual_keycode { + Some(VirtualKeyCode::LShift) => self.lshift = input.state == ElementState::Pressed, + Some(VirtualKeyCode::RShift) => self.rshift = input.state == ElementState::Pressed, + _ => (), + } + + self.mods = input.modifiers; + } + + pub fn shift(self) -> bool { + self.lshift || self.rshift + } + + pub fn ctrl(self) -> bool { + self.mods.ctrl + } + + pub fn logo(self) -> bool { + self.mods.logo + } + + pub fn alt(self) -> bool { + self.mods.alt + } +} + +impl From<&mut Modifiers> for ModifiersState { + fn from(mods: &mut Modifiers) -> ModifiersState { + ModifiersState { shift: mods.shift(), ..mods.mods } + } +} + +trait Execute<T: EventListener> { + fn execute<A: ActionContext<T>>(&self, ctx: &mut A, mouse_mode: bool); +} + +impl<T, U: EventListener> Execute<U> for Binding<T> { + /// Execute the action associate with this binding + #[inline] + fn execute<A: ActionContext<U>>(&self, ctx: &mut A, mouse_mode: bool) { + self.action.execute(ctx, mouse_mode) + } +} + +impl<T: EventListener> Execute<T> for Action { + #[inline] + fn execute<A: ActionContext<T>>(&self, ctx: &mut A, mouse_mode: bool) { + match *self { + Action::Esc(ref s) => { + ctx.scroll(Scroll::Bottom); + ctx.write_to_pty(s.clone().into_bytes()) + }, + Action::Copy => { + ctx.copy_selection(ClipboardType::Clipboard); + }, + Action::Paste => { + let text = ctx.terminal_mut().clipboard().load(ClipboardType::Clipboard); + paste(ctx, &text); + }, + Action::PasteSelection => { + // Only paste if mouse events are not captured by an application + if !mouse_mode { + let text = ctx.terminal_mut().clipboard().load(ClipboardType::Selection); + paste(ctx, &text); + } + }, + Action::Command(ref program, ref args) => { + trace!("Running command {} with args {:?}", program, args); + + match start_daemon(program, args) { + Ok(_) => debug!("Spawned new proc"), + Err(err) => warn!("Couldn't run command {}", err), + } + }, + Action::ToggleFullscreen => ctx.window_mut().toggle_fullscreen(), + #[cfg(target_os = "macos")] + Action::ToggleSimpleFullscreen => ctx.window_mut().toggle_simple_fullscreen(), + Action::Hide => ctx.window().set_visible(false), + Action::Quit => ctx.terminal_mut().exit(), + Action::IncreaseFontSize => ctx.change_font_size(FONT_SIZE_STEP), + Action::DecreaseFontSize => ctx.change_font_size(FONT_SIZE_STEP * -1.), + Action::ResetFontSize => ctx.reset_font_size(), + Action::ScrollPageUp => ctx.scroll(Scroll::PageUp), + Action::ScrollPageDown => ctx.scroll(Scroll::PageDown), + Action::ScrollLineUp => ctx.scroll(Scroll::Lines(1)), + Action::ScrollLineDown => ctx.scroll(Scroll::Lines(-1)), + Action::ScrollToTop => ctx.scroll(Scroll::Top), + Action::ScrollToBottom => ctx.scroll(Scroll::Bottom), + Action::ClearHistory => ctx.terminal_mut().clear_screen(ClearMode::Saved), + Action::ClearLogNotice => ctx.pop_message(), + Action::SpawnNewInstance => ctx.spawn_new_instance(), + Action::ReceiveChar | Action::None => (), + } + } +} + +fn paste<T: EventListener, A: ActionContext<T>>(ctx: &mut A, contents: &str) { + if ctx.terminal().mode().contains(TermMode::BRACKETED_PASTE) { + ctx.write_to_pty(&b"\x1b[200~"[..]); + ctx.write_to_pty(contents.replace("\x1b", "").into_bytes()); + ctx.write_to_pty(&b"\x1b[201~"[..]); + } else { + // In non-bracketed (ie: normal) mode, terminal applications cannot distinguish + // pasted data from keystrokes. + // In theory, we should construct the keystrokes needed to produce the data we are + // pasting... since that's neither practical nor sensible (and probably an impossible + // task to solve in a general way), we'll just replace line breaks (windows and unix + // style) with a single carriage return (\r, which is what the Enter key produces). + ctx.write_to_pty(contents.replace("\r\n", "\r").replace("\n", "\r").into_bytes()); + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum MouseState { + Url(Url), + MessageBar, + MessageBarButton, + Mouse, + Text, +} + +impl From<MouseState> for CursorIcon { + fn from(mouse_state: MouseState) -> CursorIcon { + match mouse_state { + MouseState::Url(_) | MouseState::MessageBarButton => CursorIcon::Hand, + MouseState::Text => CursorIcon::Text, + _ => CursorIcon::Default, + } + } +} + +impl<'a, T: EventListener, A: ActionContext<T> + 'a> Processor<'a, T, A> { + pub fn new(ctx: A, config: &'a mut Config) -> Self { + Self { ctx, config, _phantom: Default::default() } + } + + fn mouse_state(&mut self, point: Point, mods: ModifiersState) -> MouseState { + let mouse_mode = + TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG | TermMode::MOUSE_REPORT_CLICK; + + // Check message bar before URL to ignore URLs in the message bar + if let Some(message) = self.message_at_point(Some(point)) { + if self.message_close_at_point(point, message) { + return MouseState::MessageBarButton; + } else { + return MouseState::MessageBar; + } + } + + // Check for URL at point with required modifiers held + if self.config.ui_config.mouse.url.mods().relaxed_eq(mods) + && (!self.ctx.terminal().mode().intersects(mouse_mode) || mods.shift) + && self.config.ui_config.mouse.url.launcher.is_some() + && self.ctx.selection_is_empty() + && self.ctx.mouse().left_button_state != ElementState::Pressed + { + let buffer_point = self.ctx.terminal().visible_to_buffer(point); + if let Some(url) = + self.ctx.terminal().urls().drain(..).find(|url| url.contains(buffer_point)) + { + return MouseState::Url(url); + } + } + + if self.ctx.terminal().mode().intersects(mouse_mode) && !self.ctx.modifiers().shift() { + MouseState::Mouse + } else { + MouseState::Text + } + } + + #[inline] + pub fn mouse_moved(&mut self, x: usize, y: usize, modifiers: ModifiersState) { + self.ctx.mouse_mut().x = x; + self.ctx.mouse_mut().y = y; + + let size_info = self.ctx.size_info(); + let point = size_info.pixels_to_coords(x, y); + + let cell_side = self.get_mouse_side(); + let prev_side = mem::replace(&mut self.ctx.mouse_mut().cell_side, cell_side); + let prev_line = mem::replace(&mut self.ctx.mouse_mut().line, point.line); + let prev_col = mem::replace(&mut self.ctx.mouse_mut().column, point.col); + + let motion_mode = TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG; + let report_mode = TermMode::MOUSE_REPORT_CLICK | motion_mode; + + let cell_changed = + prev_line != self.ctx.mouse().line || prev_col != self.ctx.mouse().column; + + // If the mouse hasn't changed cells, do nothing + if !cell_changed && prev_side == cell_side { + return; + } + + // Don't launch URLs if mouse has moved + self.ctx.mouse_mut().block_url_launcher = true; + + let mouse_state = self.mouse_state(point, modifiers); + self.update_mouse_cursor(mouse_state); + match mouse_state { + MouseState::Url(url) => { + let url_bounds = url.linear_bounds(self.ctx.terminal()); + self.ctx.terminal_mut().set_url_highlight(url_bounds); + }, + MouseState::MessageBar | MouseState::MessageBarButton => { + self.ctx.terminal_mut().reset_url_highlight(); + return; + }, + _ => self.ctx.terminal_mut().reset_url_highlight(), + } + + if self.ctx.mouse().left_button_state == ElementState::Pressed + && (modifiers.shift || !self.ctx.terminal().mode().intersects(report_mode)) + { + self.ctx.update_selection(Point { line: point.line, col: point.col }, cell_side); + } else if self.ctx.terminal().mode().intersects(motion_mode) + && size_info.contains_point(x, y, false) + && cell_changed + { + if self.ctx.mouse().left_button_state == ElementState::Pressed { + self.mouse_report(32, ElementState::Pressed, modifiers); + } else if self.ctx.mouse().middle_button_state == ElementState::Pressed { + self.mouse_report(33, ElementState::Pressed, modifiers); + } else if self.ctx.mouse().right_button_state == ElementState::Pressed { + self.mouse_report(34, ElementState::Pressed, modifiers); + } else if self.ctx.terminal().mode().contains(TermMode::MOUSE_MOTION) { + self.mouse_report(35, ElementState::Pressed, modifiers); + } + } + } + + fn get_mouse_side(&self) -> Side { + let size_info = self.ctx.size_info(); + let x = self.ctx.mouse().x; + + let cell_x = x.saturating_sub(size_info.padding_x as usize) % size_info.cell_width as usize; + let half_cell_width = (size_info.cell_width / 2.0) as usize; + + let additional_padding = + (size_info.width - size_info.padding_x * 2.) % size_info.cell_width; + let end_of_grid = size_info.width - size_info.padding_x - additional_padding; + + if cell_x > half_cell_width + // Edge case when mouse leaves the window + || x as f32 >= end_of_grid + { + Side::Right + } else { + Side::Left + } + } + + pub fn normal_mouse_report(&mut self, button: u8) { + let (line, column) = (self.ctx.mouse().line, self.ctx.mouse().column); + + if line < Line(223) && column < Column(223) { + let msg = vec![ + b'\x1b', + b'[', + b'M', + 32 + button, + 32 + 1 + column.0 as u8, + 32 + 1 + line.0 as u8, + ]; + + self.ctx.write_to_pty(msg); + } + } + + pub fn sgr_mouse_report(&mut self, button: u8, state: ElementState) { + let (line, column) = (self.ctx.mouse().line, self.ctx.mouse().column); + let c = match state { + ElementState::Pressed => 'M', + ElementState::Released => 'm', + }; + + let msg = format!("\x1b[<{};{};{}{}", button, column + 1, line + 1, c); + self.ctx.write_to_pty(msg.into_bytes()); + } + + pub fn mouse_report(&mut self, button: u8, state: ElementState, modifiers: ModifiersState) { + // Calculate modifiers value + let mut mods = 0; + if modifiers.shift { + mods += 4; + } + if modifiers.alt { + mods += 8; + } + if modifiers.ctrl { + mods += 16; + } + + // Report mouse events + if self.ctx.terminal().mode().contains(TermMode::SGR_MOUSE) { + self.sgr_mouse_report(button + mods, state); + } else if let ElementState::Released = state { + self.normal_mouse_report(3 + mods); + } else { + self.normal_mouse_report(button + mods); + } + } + + pub fn on_mouse_double_click(&mut self, button: MouseButton, point: Option<Point>) { + if let (Some(point), true) = (point, button == MouseButton::Left) { + self.ctx.semantic_selection(point); + } + } + + pub fn on_mouse_triple_click(&mut self, button: MouseButton, point: Option<Point>) { + if let (Some(point), true) = (point, button == MouseButton::Left) { + self.ctx.line_selection(point); + } + } + + pub fn on_mouse_press( + &mut self, + button: MouseButton, + modifiers: ModifiersState, + point: Option<Point>, + ) { + let now = Instant::now(); + let elapsed = self.ctx.mouse().last_click_timestamp.elapsed(); + self.ctx.mouse_mut().last_click_timestamp = now; + + let button_changed = self.ctx.mouse().last_button != button; + + self.ctx.mouse_mut().click_state = match self.ctx.mouse().click_state { + ClickState::Click + if !button_changed + && elapsed < self.config.ui_config.mouse.double_click.threshold => + { + self.ctx.mouse_mut().block_url_launcher = true; + self.on_mouse_double_click(button, point); + ClickState::DoubleClick + } + ClickState::DoubleClick + if !button_changed + && elapsed < self.config.ui_config.mouse.triple_click.threshold => + { + self.ctx.mouse_mut().block_url_launcher = true; + self.on_mouse_triple_click(button, point); + ClickState::TripleClick + } + _ => { + // Don't launch URLs if this click cleared the selection + self.ctx.mouse_mut().block_url_launcher = !self.ctx.selection_is_empty(); + + self.ctx.clear_selection(); + + // Start new empty selection + let side = self.ctx.mouse().cell_side; + if let Some(point) = point { + if modifiers.ctrl { + self.ctx.block_selection(point, side); + } else { + self.ctx.simple_selection(point, side); + } + } + + let report_modes = + TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION; + if !modifiers.shift && self.ctx.terminal().mode().intersects(report_modes) { + let code = match button { + MouseButton::Left => 0, + MouseButton::Middle => 1, + MouseButton::Right => 2, + // Can't properly report more than three buttons. + MouseButton::Other(_) => return, + }; + self.mouse_report(code, ElementState::Pressed, modifiers); + return; + } + + ClickState::Click + }, + }; + } + + pub fn on_mouse_release( + &mut self, + button: MouseButton, + modifiers: ModifiersState, + point: Option<Point>, + ) { + let report_modes = + TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION; + if !modifiers.shift && self.ctx.terminal().mode().intersects(report_modes) { + let code = match button { + MouseButton::Left => 0, + MouseButton::Middle => 1, + MouseButton::Right => 2, + // Can't properly report more than three buttons. + MouseButton::Other(_) => return, + }; + self.mouse_report(code, ElementState::Released, modifiers); + return; + } else if let (Some(point), true) = (point, button == MouseButton::Left) { + let mouse_state = self.mouse_state(point, modifiers); + self.update_mouse_cursor(mouse_state); + if let MouseState::Url(url) = mouse_state { + let url_bounds = url.linear_bounds(self.ctx.terminal()); + self.ctx.terminal_mut().set_url_highlight(url_bounds); + self.launch_url(url); + } + } + + self.copy_selection(); + } + + /// Spawn URL launcher when clicking on URLs. + fn launch_url(&self, url: Url) { + if self.ctx.mouse().block_url_launcher { + return; + } + + if let Some(ref launcher) = self.config.ui_config.mouse.url.launcher { + let mut args = launcher.args().to_vec(); + args.push(self.ctx.terminal().url_to_string(url)); + + match start_daemon(launcher.program(), &args) { + Ok(_) => debug!("Launched {} with args {:?}", launcher.program(), args), + Err(_) => warn!("Unable to launch {} with args {:?}", launcher.program(), args), + } + } + } + + pub fn on_mouse_wheel( + &mut self, + delta: MouseScrollDelta, + phase: TouchPhase, + modifiers: ModifiersState, + ) { + match delta { + MouseScrollDelta::LineDelta(_columns, lines) => { + let new_scroll_px = lines * self.ctx.size_info().cell_height; + self.scroll_terminal(modifiers, new_scroll_px as i32); + }, + MouseScrollDelta::PixelDelta(lpos) => { + match phase { + TouchPhase::Started => { + // Reset offset to zero + self.ctx.mouse_mut().scroll_px = 0; + }, + TouchPhase::Moved => { + self.scroll_terminal(modifiers, lpos.y as i32); + }, + _ => (), + } + }, + } + } + + fn scroll_terminal(&mut self, modifiers: ModifiersState, new_scroll_px: i32) { + let mouse_modes = + TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION; + let height = self.ctx.size_info().cell_height as i32; + + // Make sure the new and deprecated setting are both allowed + let faux_multiplier = self.config.scrolling.faux_multiplier() as usize; + + if self.ctx.terminal().mode().intersects(mouse_modes) { + self.ctx.mouse_mut().scroll_px += new_scroll_px; + + let code = if new_scroll_px > 0 { 64 } else { 65 }; + let lines = (self.ctx.mouse().scroll_px / height).abs(); + + for _ in 0..lines { + self.mouse_report(code, ElementState::Pressed, modifiers); + } + } else if self.ctx.terminal().mode().contains(TermMode::ALT_SCREEN) + && faux_multiplier > 0 + && !modifiers.shift + { + self.ctx.mouse_mut().scroll_px += new_scroll_px * faux_multiplier as i32; + + let cmd = if new_scroll_px > 0 { b'A' } else { b'B' }; + let lines = (self.ctx.mouse().scroll_px / height).abs(); + + let mut content = Vec::with_capacity(lines as usize * 3); + for _ in 0..lines { + content.push(0x1b); + content.push(b'O'); + content.push(cmd); + } + self.ctx.write_to_pty(content); + } else { + let multiplier = i32::from(self.config.scrolling.multiplier()); + self.ctx.mouse_mut().scroll_px += new_scroll_px * multiplier; + + let lines = self.ctx.mouse().scroll_px / height; + + self.ctx.scroll(Scroll::Lines(lines as isize)); + } + + self.ctx.mouse_mut().scroll_px %= height; + } + + pub fn on_focus_change(&mut self, is_focused: bool) { + if self.ctx.terminal().mode().contains(TermMode::FOCUS_IN_OUT) { + let chr = if is_focused { "I" } else { "O" }; + + let msg = format!("\x1b[{}", chr); + self.ctx.write_to_pty(msg.into_bytes()); + } + } + + pub fn mouse_input( + &mut self, + state: ElementState, + button: MouseButton, + modifiers: ModifiersState, + ) { + match button { + MouseButton::Left => self.ctx.mouse_mut().left_button_state = state, + MouseButton::Middle => self.ctx.mouse_mut().middle_button_state = state, + MouseButton::Right => self.ctx.mouse_mut().right_button_state = state, + _ => (), + } + + let point = self.ctx.mouse_coords(); + + // Skip normal mouse events if the message bar has been clicked + if let Some(message) = self.message_at_point(point) { + // Message should never be `Some` if point is `None` + debug_assert!(point.is_some()); + self.on_message_bar_click(state, point.unwrap(), message, modifiers); + } else { + match state { + ElementState::Pressed => { + self.process_mouse_bindings(modifiers, button); + self.on_mouse_press(button, modifiers, point); + }, + ElementState::Released => self.on_mouse_release(button, modifiers, point), + } + } + + self.ctx.mouse_mut().last_button = button; + } + + /// Process key input. + pub fn process_key(&mut self, input: KeyboardInput) { + self.ctx.modifiers().update(input); + + // Update mouse cursor for temporarily disabling mouse mode + if input.virtual_keycode == Some(VirtualKeyCode::LShift) + || input.virtual_keycode == Some(VirtualKeyCode::RShift) + { + if let Some(point) = self.ctx.mouse_coords() { + let mods = self.ctx.modifiers().into(); + let mouse_state = self.mouse_state(point, mods); + self.update_mouse_cursor(mouse_state); + } + } + + match input.state { + ElementState::Pressed => { + *self.ctx.received_count() = 0; + self.process_key_bindings(input); + }, + ElementState::Released => *self.ctx.suppress_chars() = false, + } + } + + /// Process a received character. + pub fn received_char(&mut self, c: char) { + if *self.ctx.suppress_chars() { + return; + } + + self.ctx.scroll(Scroll::Bottom); + self.ctx.clear_selection(); + + let utf8_len = c.len_utf8(); + let mut bytes = Vec::with_capacity(utf8_len); + unsafe { + bytes.set_len(utf8_len); + c.encode_utf8(&mut bytes[..]); + } + + if self.config.alt_send_esc() + && *self.ctx.received_count() == 0 + && self.ctx.modifiers().alt() + && utf8_len == 1 + { + bytes.insert(0, b'\x1b'); + } + + self.ctx.write_to_pty(bytes); + + *self.ctx.received_count() += 1; + self.ctx.terminal_mut().dirty = false; + } + + /// Attempt to find a binding and execute its action. + /// + /// The provided mode, mods, and key must match what is allowed by a binding + /// for its action to be executed. + fn process_key_bindings(&mut self, input: KeyboardInput) { + let mut suppress_chars = None; + + for binding in &self.config.ui_config.key_bindings { + let key = match (binding.trigger, input.virtual_keycode) { + (Key::Scancode(_), _) => Key::Scancode(input.scancode), + (_, Some(key)) => Key::from_glutin_input(key), + _ => continue, + }; + + if binding.is_triggered_by(*self.ctx.terminal().mode(), input.modifiers, &key, false) { + // Binding was triggered; run the action + binding.execute(&mut self.ctx, false); + + // Don't suppress when there has been a `ReceiveChar` action + *suppress_chars.get_or_insert(true) &= binding.action != Action::ReceiveChar; + } + } + + // Don't suppress char if no bindings were triggered + *self.ctx.suppress_chars() = suppress_chars.unwrap_or(false); + } + + /// Attempt to find a binding and execute its action. + /// + /// The provided mode, mods, and key must match what is allowed by a binding + /// for its action to be executed. + fn process_mouse_bindings(&mut self, mods: ModifiersState, button: MouseButton) { + for binding in &self.config.ui_config.mouse_bindings { + if binding.is_triggered_by(*self.ctx.terminal().mode(), mods, &button, true) { + // binding was triggered; run the action + let mouse_mode = !mods.shift + && self.ctx.terminal().mode().intersects( + TermMode::MOUSE_REPORT_CLICK + | TermMode::MOUSE_DRAG + | TermMode::MOUSE_MOTION, + ); + binding.execute(&mut self.ctx, mouse_mode); + } + } + } + + /// Return the message bar's message if there is some at the specified point + fn message_at_point(&mut self, point: Option<Point>) -> Option<Message> { + let size = &self.ctx.size_info(); + if let (Some(point), Some(message)) = (point, self.ctx.message()) { + if point.line.0 >= size.lines().saturating_sub(message.text(size).len()) { + return Some(message.to_owned()); + } + } + + None + } + + /// Whether the point is over the message bar's close button + fn message_close_at_point(&self, point: Point, message: Message) -> bool { + let size = self.ctx.size_info(); + point.col + message_bar::CLOSE_BUTTON_TEXT.len() >= size.cols() + && point.line == size.lines() - message.text(&size).len() + } + + /// Handle clicks on the message bar. + fn on_message_bar_click( + &mut self, + button_state: ElementState, + point: Point, + message: Message, + mods: ModifiersState, + ) { + match button_state { + ElementState::Released => self.copy_selection(), + ElementState::Pressed => { + if self.message_close_at_point(point, message) { + let mouse_state = self.mouse_state(point, mods); + self.update_mouse_cursor(mouse_state); + self.ctx.pop_message(); + } + + self.ctx.clear_selection(); + }, + } + } + + /// Copy text selection. + fn copy_selection(&mut self) { + if self.config.selection.save_to_clipboard { + self.ctx.copy_selection(ClipboardType::Clipboard); + } + self.ctx.copy_selection(ClipboardType::Selection); + } + + #[inline] + fn update_mouse_cursor(&mut self, mouse_state: MouseState) { + self.ctx.window_mut().set_mouse_cursor(mouse_state.into()); + } + + #[inline] + pub fn reset_mouse_cursor(&mut self) { + if let Some(point) = self.ctx.mouse_coords() { + let mods = self.ctx.modifiers().into(); + let mouse_state = self.mouse_state(point, mods); + self.update_mouse_cursor(mouse_state); + } + } +} + +#[cfg(test)] +mod tests { + use std::borrow::Cow; + use std::time::Duration; + + use glutin::event::{ + ElementState, Event, ModifiersState, MouseButton, VirtualKeyCode, WindowEvent, + }; + + use alacritty_terminal::clipboard::{Clipboard, ClipboardType}; + use alacritty_terminal::event::{Event as TerminalEvent, EventListener}; + use alacritty_terminal::grid::Scroll; + use alacritty_terminal::index::{Point, Side}; + use alacritty_terminal::message_bar::{Message, MessageBuffer}; + use alacritty_terminal::selection::Selection; + use alacritty_terminal::term::{SizeInfo, Term, TermMode}; + + use crate::config::{ClickHandler, Config}; + use crate::event::{ClickState, Mouse}; + use crate::window::Window; + + use super::{Action, Binding, Modifiers, Processor}; + + const KEY: VirtualKeyCode = VirtualKeyCode::Key0; + + struct MockEventProxy; + + impl EventListener for MockEventProxy { + fn send_event(&self, _event: TerminalEvent) {} + } + + #[derive(PartialEq)] + enum MultiClick { + DoubleClick, + TripleClick, + None, + } + + struct ActionContext<'a, T> { + pub terminal: &'a mut Term<T>, + pub selection: &'a mut Option<Selection>, + pub size_info: &'a SizeInfo, + pub mouse: &'a mut Mouse, + pub message_buffer: &'a mut MessageBuffer, + pub last_action: MultiClick, + pub received_count: usize, + pub suppress_chars: bool, + pub modifiers: Modifiers, + } + + impl<'a, T: EventListener> super::ActionContext<T> for ActionContext<'a, T> { + fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, _val: B) {} + + fn update_selection(&mut self, _point: Point, _side: Side) {} + + fn simple_selection(&mut self, _point: Point, _side: Side) {} + + fn block_selection(&mut self, _point: Point, _side: Side) {} + + fn copy_selection(&mut self, _: ClipboardType) {} + + fn clear_selection(&mut self) {} + + fn spawn_new_instance(&mut self) {} + + fn change_font_size(&mut self, _delta: f32) {} + + fn reset_font_size(&mut self) {} + + fn terminal(&self) -> &Term<T> { + &self.terminal + } + + fn terminal_mut(&mut self) -> &mut Term<T> { + &mut self.terminal + } + + fn size_info(&self) -> SizeInfo { + *self.size_info + } + + fn semantic_selection(&mut self, _point: Point) { + // set something that we can check for here + self.last_action = MultiClick::DoubleClick; + } + + fn line_selection(&mut self, _point: Point) { + self.last_action = MultiClick::TripleClick; + } + + fn selection_is_empty(&self) -> bool { + true + } + + fn scroll(&mut self, scroll: Scroll) { + self.terminal.scroll_display(scroll); + } + + fn mouse_coords(&self) -> Option<Point> { + let x = self.mouse.x as usize; + let y = self.mouse.y as usize; + + if self.size_info.contains_point(x, y, true) { + Some(self.size_info.pixels_to_coords(x, y)) + } else { + None + } + } + + #[inline] + fn mouse_mut(&mut self) -> &mut Mouse { + self.mouse + } + + #[inline] + fn mouse(&self) -> &Mouse { + self.mouse + } + + fn received_count(&mut self) -> &mut usize { + &mut self.received_count + } + + fn suppress_chars(&mut self) -> &mut bool { + &mut self.suppress_chars + } + + fn modifiers(&mut self) -> &mut Modifiers { + &mut self.modifiers + } + + fn window(&self) -> &Window { + unimplemented!(); + } + + fn window_mut(&mut self) -> &mut Window { + unimplemented!(); + } + + fn pop_message(&mut self) { + self.message_buffer.pop(); + } + + fn message(&self) -> Option<&Message> { + self.message_buffer.message() + } + } + + macro_rules! test_clickstate { + { + name: $name:ident, + initial_state: $initial_state:expr, + initial_button: $initial_button:expr, + input: $input:expr, + end_state: $end_state:pat, + last_action: $last_action:expr + } => { + #[test] + fn $name() { + let mut cfg = Config::default(); + cfg.ui_config.mouse = crate::config::Mouse { + double_click: ClickHandler { + threshold: Duration::from_millis(1000), + }, + triple_click: ClickHandler { + threshold: Duration::from_millis(1000), + }, + hide_when_typing: false, + url: Default::default(), + }; + + let size = SizeInfo { + width: 21.0, + height: 51.0, + cell_width: 3.0, + cell_height: 3.0, + padding_x: 0.0, + padding_y: 0.0, + dpr: 1.0, + }; + + let mut terminal = Term::new(&cfg, &size, Clipboard::new_nop(), MockEventProxy); + + let mut mouse = Mouse::default(); + mouse.click_state = $initial_state; + mouse.last_button = $initial_button; + + let mut selection = None; + + let mut message_buffer = MessageBuffer::new(); + + let context = ActionContext { + terminal: &mut terminal, + selection: &mut selection, + mouse: &mut mouse, + size_info: &size, + last_action: MultiClick::None, + received_count: 0, + suppress_chars: false, + modifiers: Modifiers::default(), + message_buffer: &mut message_buffer, + }; + + let mut processor = Processor::new(context, &mut cfg); + + if let Event::WindowEvent { event: WindowEvent::MouseInput { state, button, modifiers, .. }, .. } = $input { + processor.mouse_input(state, button, modifiers); + }; + + assert!(match processor.ctx.mouse.click_state { + $end_state => processor.ctx.last_action == $last_action, + _ => false + }); + } + } + } + + macro_rules! test_process_binding { + { + name: $name:ident, + binding: $binding:expr, + triggers: $triggers:expr, + mode: $mode:expr, + mods: $mods:expr + } => { + #[test] + fn $name() { + if $triggers { + assert!($binding.is_triggered_by($mode, $mods, &KEY, false)); + } else { + assert!(!$binding.is_triggered_by($mode, $mods, &KEY, false)); + } + } + } + } + + test_clickstate! { + name: single_click, + initial_state: ClickState::None, + initial_button: MouseButton::Other(0), + input: Event::<TerminalEvent>::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + device_id: unsafe { ::std::mem::transmute_copy(&0) }, + modifiers: ModifiersState::default(), + }, + window_id: unsafe { ::std::mem::transmute_copy(&0) }, + }, + end_state: ClickState::Click, + last_action: MultiClick::None + } + + test_clickstate! { + name: double_click, + initial_state: ClickState::Click, + initial_button: MouseButton::Left, + input: Event::<TerminalEvent>::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + device_id: unsafe { ::std::mem::transmute_copy(&0) }, + modifiers: ModifiersState::default(), + }, + window_id: unsafe { ::std::mem::transmute_copy(&0) }, + }, + end_state: ClickState::DoubleClick, + last_action: MultiClick::DoubleClick + } + + test_clickstate! { + name: triple_click, + initial_state: ClickState::DoubleClick, + initial_button: MouseButton::Left, + input: Event::<TerminalEvent>::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + device_id: unsafe { ::std::mem::transmute_copy(&0) }, + modifiers: ModifiersState::default(), + }, + window_id: unsafe { ::std::mem::transmute_copy(&0) }, + }, + end_state: ClickState::TripleClick, + last_action: MultiClick::TripleClick + } + + test_clickstate! { + name: multi_click_separate_buttons, + initial_state: ClickState::DoubleClick, + initial_button: MouseButton::Left, + input: Event::<TerminalEvent>::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Right, + device_id: unsafe { ::std::mem::transmute_copy(&0) }, + modifiers: ModifiersState::default(), + }, + window_id: unsafe { ::std::mem::transmute_copy(&0) }, + }, + end_state: ClickState::Click, + last_action: MultiClick::None + } + + test_process_binding! { + name: process_binding_nomode_shiftmod_require_shift, + binding: Binding { trigger: KEY, mods: ModifiersState { shift: true, ctrl: false, alt: false, logo: false }, action: Action::from("\x1b[1;2D"), mode: TermMode::NONE, notmode: TermMode::NONE }, + triggers: true, + mode: TermMode::NONE, + mods: ModifiersState { shift: true, ctrl: false, alt: false, logo: false } + } + + test_process_binding! { + name: process_binding_nomode_nomod_require_shift, + binding: Binding { trigger: KEY, mods: ModifiersState { shift: true, ctrl: false, alt: false, logo: false }, action: Action::from("\x1b[1;2D"), mode: TermMode::NONE, notmode: TermMode::NONE }, + triggers: false, + mode: TermMode::NONE, + mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false } + } + + test_process_binding! { + name: process_binding_nomode_controlmod, + binding: Binding { trigger: KEY, mods: ModifiersState { ctrl: true, shift: false, alt: false, logo: false }, action: Action::from("\x1b[1;5D"), mode: TermMode::NONE, notmode: TermMode::NONE }, + triggers: true, + mode: TermMode::NONE, + mods: ModifiersState { ctrl: true, shift: false, alt: false, logo: false } + } + + test_process_binding! { + name: process_binding_nomode_nomod_require_not_appcursor, + binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }, action: Action::from("\x1b[D"), mode: TermMode::NONE, notmode: TermMode::APP_CURSOR }, + triggers: true, + mode: TermMode::NONE, + mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false } + } + + test_process_binding! { + name: process_binding_appcursormode_nomod_require_appcursor, + binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }, action: Action::from("\x1bOD"), mode: TermMode::APP_CURSOR, notmode: TermMode::NONE }, + triggers: true, + mode: TermMode::APP_CURSOR, + mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false } + } + + test_process_binding! { + name: process_binding_nomode_nomod_require_appcursor, + binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }, action: Action::from("\x1bOD"), mode: TermMode::APP_CURSOR, notmode: TermMode::NONE }, + triggers: false, + mode: TermMode::NONE, + mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false } + } + + test_process_binding! { + name: process_binding_appcursormode_appkeypadmode_nomod_require_appcursor, + binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }, action: Action::from("\x1bOD"), mode: TermMode::APP_CURSOR, notmode: TermMode::NONE }, + triggers: true, + mode: TermMode::APP_CURSOR | TermMode::APP_KEYPAD, + mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false } + } + + test_process_binding! { + name: process_binding_fail_with_extra_mods, + binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: true }, action: Action::from("arst"), mode: TermMode::NONE, notmode: TermMode::NONE }, + triggers: false, + mode: TermMode::NONE, + mods: ModifiersState { shift: false, ctrl: false, alt: true, logo: true } + } +} diff --git a/alacritty/src/logging.rs b/alacritty/src/logging.rs index d4cb70c5..d1c95e43 100644 --- a/alacritty/src/logging.rs +++ b/alacritty/src/logging.rs @@ -25,10 +25,11 @@ use std::process; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; -use crossbeam_channel::Sender; +use glutin::event_loop::EventLoopProxy; use log::{self, Level}; use time; +use alacritty_terminal::event::Event; use alacritty_terminal::message_bar::Message; use alacritty_terminal::term::color; @@ -38,7 +39,7 @@ const ALACRITTY_LOG_ENV: &str = "ALACRITTY_LOG"; pub fn initialize( options: &Options, - message_tx: Sender<Message>, + event_proxy: EventLoopProxy<Event>, ) -> Result<Option<PathBuf>, log::SetLoggerError> { log::set_max_level(options.log_level); @@ -48,7 +49,7 @@ pub fn initialize( ::env_logger::try_init()?; Ok(None) } else { - let logger = Logger::new(message_tx); + let logger = Logger::new(event_proxy); let path = logger.file_path(); log::set_boxed_logger(Box::new(logger))?; Ok(path) @@ -58,15 +59,15 @@ pub fn initialize( pub struct Logger { logfile: Mutex<OnDemandLogFile>, stdout: Mutex<LineWriter<Stdout>>, - message_tx: Sender<Message>, + event_proxy: Mutex<EventLoopProxy<Event>>, } impl Logger { - fn new(message_tx: Sender<Message>) -> Self { + fn new(event_proxy: EventLoopProxy<Event>) -> Self { let logfile = Mutex::new(OnDemandLogFile::new()); let stdout = Mutex::new(LineWriter::new(io::stdout())); - Logger { logfile, stdout, message_tx } + Logger { logfile, stdout, event_proxy: Mutex::new(event_proxy) } } fn file_path(&self) -> Option<PathBuf> { @@ -122,9 +123,12 @@ impl log::Log for Logger { _ => unreachable!(), }; - let mut message = Message::new(msg, color); - message.set_topic(record.file().unwrap_or("?").into()); - let _ = self.message_tx.send(message); + if let Ok(event_proxy) = self.event_proxy.lock() { + let mut message = Message::new(msg, color); + message.set_target(record.target().to_owned()); + + let _ = event_proxy.send_event(Event::Message(message)); + } } } diff --git a/alacritty/src/main.rs b/alacritty/src/main.rs index 65313cbe..146709fd 100644 --- a/alacritty/src/main.rs +++ b/alacritty/src/main.rs @@ -25,7 +25,7 @@ #[cfg(target_os = "macos")] use std::env; use std::error::Error; -use std::fs::{self, File}; +use std::fs; use std::io::{self, Write}; #[cfg(not(windows))] use std::os::unix::io::AsRawFd; @@ -33,29 +33,35 @@ use std::sync::Arc; #[cfg(target_os = "macos")] use dirs; +use glutin::event_loop::EventLoop as GlutinEventLoop; use log::{error, info}; -use serde_json as json; #[cfg(windows)] use winapi::um::wincon::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS}; use alacritty_terminal::clipboard::Clipboard; -use alacritty_terminal::config::{Config, Monitor}; -use alacritty_terminal::display::Display; +use alacritty_terminal::event::Event; use alacritty_terminal::event_loop::{self, EventLoop, Msg}; #[cfg(target_os = "macos")] use alacritty_terminal::locale; use alacritty_terminal::message_bar::MessageBuffer; use alacritty_terminal::panic; use alacritty_terminal::sync::FairMutex; -use alacritty_terminal::term::{cell::Cell, Term}; +use alacritty_terminal::term::Term; use alacritty_terminal::tty; -use alacritty_terminal::{die, event}; mod cli; mod config; +mod display; +mod event; +mod input; mod logging; +mod window; use crate::cli::Options; +use crate::config::monitor::Monitor; +use crate::config::Config; +use crate::display::Display; +use crate::event::{EventProxy, Processor}; fn main() { panic::attach_handler(); @@ -71,12 +77,12 @@ fn main() { // Load command line options let options = Options::new(); - // Setup storage for message UI - let message_buffer = MessageBuffer::new(); + // Setup glutin event loop + let window_event_loop = GlutinEventLoop::<Event>::with_user_event(); // Initialize the logger as soon as possible as to capture output from other subsystems - let log_file = - logging::initialize(&options, message_buffer.tx()).expect("Unable to initialize logger"); + let log_file = logging::initialize(&options, window_event_loop.create_proxy()) + .expect("Unable to initialize logger"); // Load configuration file // If the file is a command line argument, we won't write a generated default file @@ -107,8 +113,9 @@ fn main() { let persistent_logging = config.persistent_logging(); // Run alacritty - if let Err(err) = run(config, message_buffer) { - die!("Alacritty encountered an unrecoverable error:\n\n\t{}\n", err); + if let Err(err) = run(window_event_loop, config) { + println!("Alacritty encountered an unrecoverable error:\n\n\t{}\n", err); + std::process::exit(1); } // Clean up logfile @@ -123,7 +130,7 @@ fn main() { /// /// Creates a window, the terminal state, pty, I/O event loop, input processor, /// config change monitor, and runs the main display loop. -fn run(config: Config, message_buffer: MessageBuffer) -> Result<(), Box<dyn Error>> { +fn run(window_event_loop: GlutinEventLoop<Event>, config: Config) -> Result<(), Box<dyn Error>> { info!("Welcome to Alacritty"); if let Some(config_path) = &config.config_path { info!("Configuration loaded from {:?}", config_path.display()); @@ -132,17 +139,19 @@ fn run(config: Config, message_buffer: MessageBuffer) -> Result<(), Box<dyn Erro // Set environment variables tty::setup_env(&config); - // Create a display. + let event_proxy = EventProxy::new(window_event_loop.create_proxy()); + + // Create a display // - // The display manages a window and can draw the terminal - let mut display = Display::new(&config)?; + // The display manages a window and can draw the terminal. + let display = Display::new(&config, &window_event_loop)?; - info!("PTY Dimensions: {:?} x {:?}", display.size().lines(), display.size().cols()); + info!("PTY Dimensions: {:?} x {:?}", display.size_info.lines(), display.size_info.cols()); // Create new native clipboard - #[cfg(not(any(target_os = "macos", target_os = "windows")))] - let clipboard = Clipboard::new(display.get_wayland_display()); - #[cfg(any(target_os = "macos", target_os = "windows"))] + #[cfg(not(any(target_os = "macos", windows)))] + let clipboard = Clipboard::new(display.window.wayland_display()); + #[cfg(any(target_os = "macos", windows))] let clipboard = Clipboard::new(); // Create the terminal @@ -150,28 +159,28 @@ fn run(config: Config, message_buffer: MessageBuffer) -> Result<(), Box<dyn Erro // This object contains all of the state about what's being displayed. It's // wrapped in a clonable mutex since both the I/O loop and display need to // access it. - let terminal = Term::new(&config, display.size().to_owned(), message_buffer, clipboard); + let terminal = Term::new(&config, &display.size_info, clipboard, event_proxy.clone()); let terminal = Arc::new(FairMutex::new(terminal)); - // Find the window ID for setting $WINDOWID - let window_id = display.get_window_id(); - // Create the pty // // The pty forks a process to run the shell on the slave side of the // pseudoterminal. A file descriptor for the master side is retained for // reading/writing to the shell. - let pty = tty::new(&config, &display.size(), window_id); + #[cfg(not(any(target_os = "macos", windows)))] + let pty = tty::new(&config, &display.size_info, display.window.x11_window_id()); + #[cfg(any(target_os = "macos", windows))] + let pty = tty::new(&config, &display.size_info, None); - // Get a reference to something that we can resize + // Create PTY resize handle // // This exists because rust doesn't know the interface is thread-safe // and we need to be able to resize the PTY from the main thread while the IO // thread owns the EventedRW object. #[cfg(windows)] - let mut resize_handle = pty.resize_handle(); + let resize_handle = pty.resize_handle(); #[cfg(not(windows))] - let mut resize_handle = pty.fd.as_raw_fd(); + let resize_handle = pty.fd.as_raw_fd(); // Create the pseudoterminal I/O loop // @@ -180,86 +189,45 @@ fn run(config: Config, message_buffer: MessageBuffer) -> Result<(), Box<dyn Erro // synchronized since the I/O loop updates the state, and the display // consumes it periodically. let event_loop = - EventLoop::new(Arc::clone(&terminal), display.notifier(), pty, config.debug.ref_test); + EventLoop::new(Arc::clone(&terminal), event_proxy.clone(), pty, config.debug.ref_test); // The event loop channel allows write requests from the event processor - // to be sent to the loop and ultimately written to the pty. + // to be sent to the pty loop and ultimately written to the pty. let loop_tx = event_loop.channel(); - // Event processor - // - // Need the Rc<RefCell<_>> here since a ref is shared in the resize callback - let mut processor = event::Processor::new( - event_loop::Notifier(event_loop.channel()), - display.resize_channel(), - &config, - display.size().to_owned(), - ); - // Create a config monitor when config was loaded from path // // The monitor watches the config file for changes and reloads it. Pending // config changes are processed in the main loop. - let config_monitor = if config.live_config_reload() { - config.config_path.as_ref().map(|path| Monitor::new(path, display.notifier())) - } else { - None - }; - - // Kick off the I/O thread - let _io_thread = event_loop.spawn(None); - - info!("Initialisation complete"); - - // Main display loop - loop { - // Process input and window events - let mut terminal_lock = processor.process_events(&terminal, display.window()); - - // Handle config reloads - if let Some(ref path) = config_monitor.as_ref().and_then(Monitor::pending) { - // Clear old config messages from bar - terminal_lock.message_buffer_mut().remove_topic(config::SOURCE_FILE_PATH); - - if let Ok(config) = config::reload_from(path) { - display.update_config(&config); - processor.update_config(&config); - terminal_lock.update_config(&config); - } - - terminal_lock.dirty = true; - } - - // Begin shutdown if the flag was raised - if terminal_lock.should_exit() || tty::process_should_exit() { - break; - } + if config.live_config_reload() { + config.config_path.as_ref().map(|path| Monitor::new(path, event_proxy.clone())); + } - // Maybe draw the terminal - if terminal_lock.needs_draw() { - // Try to update the position of the input method editor - #[cfg(not(windows))] - display.update_ime_position(&terminal_lock); + // Setup storage for message UI + let message_buffer = MessageBuffer::new(); - // Handle pending resize events - // - // The second argument is a list of types that want to be notified - // of display size changes. - display.handle_resize(&mut terminal_lock, &config, &mut resize_handle, &mut processor); + // Event processor + // + // Need the Rc<RefCell<_>> here since a ref is shared in the resize callback + let mut processor = Processor::new( + event_loop::Notifier(loop_tx.clone()), + Box::new(resize_handle), + message_buffer, + config, + display, + ); - drop(terminal_lock); + // Kick off the I/O thread + let io_thread = event_loop.spawn(); - // Draw the current state of the terminal - display.draw(&terminal, &config); - } - } + info!("Initialisation complete"); - // Write ref tests to disk - if config.debug.ref_test { - write_ref_test_results(&terminal.lock()); - } + // Start event loop and block until shutdown + processor.run(terminal, window_event_loop); - loop_tx.send(Msg::Shutdown).expect("Error sending shutdown to event loop"); + // Shutdown PTY parser event loop + loop_tx.send(Msg::Shutdown).expect("Error sending shutdown to pty event loop"); + io_thread.join().expect("join io thread"); // FIXME patch notify library to have a shutdown method // config_reloader.join().ok(); @@ -274,29 +242,3 @@ fn run(config: Config, message_buffer: MessageBuffer) -> Result<(), Box<dyn Erro Ok(()) } - -// Write the ref test results to the disk -fn write_ref_test_results(terminal: &Term) { - // dump grid state - let mut grid = terminal.grid().clone(); - grid.initialize_all(&Cell::default()); - grid.truncate(); - - let serialized_grid = json::to_string(&grid).expect("serialize grid"); - - let serialized_size = json::to_string(terminal.size_info()).expect("serialize size"); - - let serialized_config = format!("{{\"history_size\":{}}}", grid.history_size()); - - File::create("./grid.json") - .and_then(|mut f| f.write_all(serialized_grid.as_bytes())) - .expect("write grid.json"); - - File::create("./size.json") - .and_then(|mut f| f.write_all(serialized_size.as_bytes())) - .expect("write size.json"); - - File::create("./config.json") - .and_then(|mut f| f.write_all(serialized_config.as_bytes())) - .expect("write config.json"); -} diff --git a/alacritty/src/window.rs b/alacritty/src/window.rs new file mode 100644 index 00000000..4b5de2c9 --- /dev/null +++ b/alacritty/src/window.rs @@ -0,0 +1,434 @@ +// 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::convert::From; +#[cfg(not(any(target_os = "macos", windows)))] +use std::ffi::c_void; +use std::fmt; + +use glutin::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; +use glutin::event_loop::EventLoop; +#[cfg(target_os = "macos")] +use glutin::platform::macos::{RequestUserAttentionType, WindowBuilderExtMacOS, WindowExtMacOS}; +#[cfg(not(any(target_os = "macos", windows)))] +use glutin::platform::unix::{EventLoopWindowTargetExtUnix, WindowBuilderExtUnix, WindowExtUnix}; +#[cfg(not(target_os = "macos"))] +use glutin::window::Icon; +use glutin::window::{CursorIcon, Fullscreen, Window as GlutinWindow, WindowBuilder, WindowId}; +use glutin::{self, ContextBuilder, PossiblyCurrent, WindowedContext}; +#[cfg(not(target_os = "macos"))] +use image::ImageFormat; +#[cfg(not(any(target_os = "macos", windows)))] +use x11_dl::xlib::{Display as XDisplay, PropModeReplace, XErrorEvent, Xlib}; + +use alacritty_terminal::config::{Decorations, StartupMode, WindowConfig, DEFAULT_NAME}; +use alacritty_terminal::event::Event; +use alacritty_terminal::gl; +use alacritty_terminal::term::{SizeInfo, Term}; + +use crate::config::Config; + +// It's required to be in this directory due to the `windows.rc` file +#[cfg(not(target_os = "macos"))] +static WINDOW_ICON: &[u8] = include_bytes!("../../extra/windows/alacritty.ico"); + +/// Window errors +#[derive(Debug)] +pub enum Error { + /// Error creating the window + ContextCreation(glutin::CreationError), + + /// Error dealing with fonts + Font(font::Error), + + /// Error manipulating the rendering context + Context(glutin::ContextError), +} + +/// Result of fallible operations concerning a Window. +type Result<T> = ::std::result::Result<T, Error>; + +impl ::std::error::Error for Error { + fn cause(&self) -> Option<&dyn (::std::error::Error)> { + match *self { + Error::ContextCreation(ref err) => Some(err), + Error::Context(ref err) => Some(err), + Error::Font(ref err) => Some(err), + } + } + + fn description(&self) -> &str { + match *self { + Error::ContextCreation(ref _err) => "Error creating gl context", + Error::Context(ref _err) => "Error operating on render context", + Error::Font(ref err) => err.description(), + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Error::ContextCreation(ref err) => write!(f, "Error creating GL context; {}", err), + Error::Context(ref err) => write!(f, "Error operating on render context; {}", err), + Error::Font(ref err) => err.fmt(f), + } + } +} + +impl From<glutin::CreationError> for Error { + fn from(val: glutin::CreationError) -> Error { + Error::ContextCreation(val) + } +} + +impl From<glutin::ContextError> for Error { + fn from(val: glutin::ContextError) -> Error { + Error::Context(val) + } +} + +impl From<font::Error> for Error { + fn from(val: font::Error) -> Error { + Error::Font(val) + } +} + +fn create_gl_window( + mut window: WindowBuilder, + event_loop: &EventLoop<Event>, + srgb: bool, + dimensions: Option<LogicalSize>, +) -> Result<WindowedContext<PossiblyCurrent>> { + if let Some(dimensions) = dimensions { + window = window.with_inner_size(dimensions); + } + + let windowed_context = ContextBuilder::new() + .with_srgb(srgb) + .with_vsync(true) + .with_hardware_acceleration(None) + .build_windowed(window, event_loop)?; + + // Make the context current so OpenGL operations can run + let windowed_context = unsafe { windowed_context.make_current().map_err(|(_, e)| e)? }; + + Ok(windowed_context) +} + +/// A window which can be used for displaying the terminal +/// +/// Wraps the underlying windowing library to provide a stable API in Alacritty +pub struct Window { + windowed_context: WindowedContext<PossiblyCurrent>, + current_mouse_cursor: CursorIcon, + mouse_visible: bool, +} + +impl Window { + /// Create a new window + /// + /// This creates a window and fully initializes a window. + pub fn new( + event_loop: &EventLoop<Event>, + config: &Config, + logical: Option<LogicalSize>, + ) -> Result<Window> { + let title = config.window.title.as_ref().map_or(DEFAULT_NAME, |t| t); + + let window_builder = Window::get_platform_window(title, &config.window); + let windowed_context = + create_gl_window(window_builder.clone(), &event_loop, false, logical) + .or_else(|_| create_gl_window(window_builder, &event_loop, true, logical))?; + + // Text cursor + windowed_context.window().set_cursor_icon(CursorIcon::Text); + + // Set OpenGL symbol loader. This call MUST be after window.make_current on windows. + gl::load_with(|symbol| windowed_context.get_proc_address(symbol) as *const _); + + // On X11, embed the window inside another if the parent ID has been set + #[cfg(not(any(target_os = "macos", windows)))] + { + if event_loop.is_x11() { + if let Some(parent_window_id) = config.window.embed { + x_embed_window(windowed_context.window(), parent_window_id); + } + } + } + + Ok(Window { + current_mouse_cursor: CursorIcon::Default, + mouse_visible: true, + windowed_context, + }) + } + + pub fn set_inner_size(&mut self, size: LogicalSize) { + self.window().set_inner_size(size); + } + + pub fn inner_size(&self) -> LogicalSize { + self.window().inner_size() + } + + pub fn hidpi_factor(&self) -> f64 { + self.window().hidpi_factor() + } + + #[inline] + pub fn set_visible(&self, visibility: bool) { + self.window().set_visible(visibility); + } + + /// Set the window title + #[inline] + pub fn set_title(&self, title: &str) { + self.window().set_title(title); + } + + #[inline] + pub fn set_mouse_cursor(&mut self, cursor: CursorIcon) { + if cursor != self.current_mouse_cursor { + self.current_mouse_cursor = cursor; + self.window().set_cursor_icon(cursor); + } + } + + /// Set mouse cursor visible + pub fn set_mouse_visible(&mut self, visible: bool) { + if visible != self.mouse_visible { + self.mouse_visible = visible; + self.window().set_cursor_visible(visible); + } + } + + #[cfg(not(any(target_os = "macos", windows)))] + pub fn get_platform_window(title: &str, window_config: &WindowConfig) -> WindowBuilder { + let decorations = match window_config.decorations { + Decorations::None => false, + _ => true, + }; + + let image = image::load_from_memory_with_format(WINDOW_ICON, ImageFormat::ICO) + .expect("loading icon") + .to_rgba(); + let (width, height) = image.dimensions(); + let icon = Icon::from_rgba(image.into_raw(), width, height); + + let class = &window_config.class; + + let mut builder = WindowBuilder::new() + .with_title(title) + .with_visible(false) + .with_transparent(true) + .with_decorations(decorations) + .with_maximized(window_config.startup_mode() == StartupMode::Maximized) + .with_window_icon(icon.ok()) + // X11 + .with_class(class.instance.clone(), class.general.clone()) + // Wayland + .with_app_id(class.instance.clone()); + + if let Some(ref val) = window_config.gtk_theme_variant { + builder = builder.with_gtk_theme_variant(val.clone()) + } + + builder + } + + #[cfg(windows)] + pub fn get_platform_window(title: &str, window_config: &WindowConfig) -> WindowBuilder { + let decorations = match window_config.decorations { + Decorations::None => false, + _ => true, + }; + + let image = image::load_from_memory_with_format(WINDOW_ICON, ImageFormat::ICO) + .expect("loading icon") + .to_rgba(); + let (width, height) = image.dimensions(); + let icon = Icon::from_rgba(image.into_raw(), width, height); + + WindowBuilder::new() + .with_title(title) + .with_visible(true) + .with_decorations(decorations) + .with_transparent(true) + .with_maximized(window_config.startup_mode() == StartupMode::Maximized) + .with_window_icon(icon.ok()) + } + + #[cfg(target_os = "macos")] + pub fn get_platform_window(title: &str, window_config: &WindowConfig) -> WindowBuilder { + let window = WindowBuilder::new() + .with_title(title) + .with_visible(false) + .with_transparent(true) + .with_maximized(window_config.startup_mode() == StartupMode::Maximized); + + match window_config.decorations { + Decorations::Full => window, + Decorations::Transparent => window + .with_title_hidden(true) + .with_titlebar_transparent(true) + .with_fullsize_content_view(true), + Decorations::Buttonless => window + .with_title_hidden(true) + .with_titlebar_buttons_hidden(true) + .with_titlebar_transparent(true) + .with_fullsize_content_view(true), + Decorations::None => window.with_titlebar_hidden(true), + } + } + + #[cfg(not(any(target_os = "macos", windows)))] + pub fn set_urgent(&self, is_urgent: bool) { + self.window().set_urgent(is_urgent); + } + + #[cfg(target_os = "macos")] + pub fn set_urgent(&self, is_urgent: bool) { + if !is_urgent { + return; + } + + self.window().request_user_attention(RequestUserAttentionType::Critical); + } + + #[cfg(windows)] + pub fn set_urgent(&self, _is_urgent: bool) {} + + pub fn set_outer_position(&self, pos: LogicalPosition) { + self.window().set_outer_position(pos); + } + + #[cfg(not(any(target_os = "macos", windows)))] + pub fn x11_window_id(&self) -> Option<usize> { + self.window().xlib_window().map(|xlib_window| xlib_window as usize) + } + + pub fn window_id(&self) -> WindowId { + self.window().id() + } + + #[cfg(not(any(target_os = "macos", windows)))] + pub fn set_maximized(&self, maximized: bool) { + self.window().set_maximized(maximized); + } + + /// Toggle the window's fullscreen state + pub fn toggle_fullscreen(&mut self) { + self.set_fullscreen(self.window().fullscreen().is_none()); + } + + #[cfg(target_os = "macos")] + pub fn toggle_simple_fullscreen(&mut self) { + self.set_simple_fullscreen(!self.window().simple_fullscreen()); + } + + pub fn set_fullscreen(&mut self, fullscreen: bool) { + #[cfg(macos)] + { + if self.window().simple_fullscreen() { + return; + } + } + + if fullscreen { + let current_monitor = self.window().current_monitor(); + self.window().set_fullscreen(Some(Fullscreen::Borderless(current_monitor))); + } else { + self.window().set_fullscreen(None); + } + } + + #[cfg(target_os = "macos")] + pub fn set_simple_fullscreen(&mut self, simple_fullscreen: bool) { + if self.window().fullscreen().is_some() { + return; + } + + self.window().set_simple_fullscreen(simple_fullscreen); + } + + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + pub fn wayland_display(&self) -> Option<*mut c_void> { + self.window().wayland_display() + } + + /// Adjust the IME editor position according to the new location of the cursor + #[cfg(not(windows))] + pub fn update_ime_position<T>(&mut self, terminal: &Term<T>, size_info: &SizeInfo) { + let point = terminal.cursor().point; + let SizeInfo { cell_width: cw, cell_height: ch, padding_x: px, padding_y: py, dpr, .. } = + size_info; + + let nspot_x = f64::from(px + point.col.0 as f32 * cw); + let nspot_y = f64::from(py + (point.line.0 + 1) as f32 * ch); + + self.window().set_ime_position(PhysicalPosition::from((nspot_x, nspot_y)).to_logical(*dpr)); + } + + pub fn swap_buffers(&self) { + self.windowed_context.swap_buffers().expect("swap buffers"); + } + + pub fn resize(&self, size: PhysicalSize) { + self.windowed_context.resize(size); + } + + fn window(&self) -> &GlutinWindow { + self.windowed_context.window() + } +} + +#[cfg(not(any(target_os = "macos", windows)))] +fn x_embed_window(window: &GlutinWindow, parent_id: u64) { + let (xlib_display, xlib_window) = match (window.xlib_display(), window.xlib_window()) { + (Some(display), Some(window)) => (display, window), + _ => return, + }; + + let xlib = Xlib::open().expect("get xlib"); + + unsafe { + let atom = (xlib.XInternAtom)(xlib_display as *mut _, "_XEMBED".as_ptr() as *const _, 0); + (xlib.XChangeProperty)( + xlib_display as _, + xlib_window as _, + atom, + atom, + 32, + PropModeReplace, + [0, 1].as_ptr(), + 2, + ); + + // Register new error handler + let old_handler = (xlib.XSetErrorHandler)(Some(xembed_error_handler)); + + // Check for the existence of the target before attempting reparenting + (xlib.XReparentWindow)(xlib_display as _, xlib_window as _, parent_id, 0, 0); + + // Drain errors and restore original error handler + (xlib.XSync)(xlib_display as _, 0); + (xlib.XSetErrorHandler)(old_handler); + } +} + +#[cfg(not(any(target_os = "macos", windows)))] +unsafe extern "C" fn xembed_error_handler(_: *mut XDisplay, _: *mut XErrorEvent) -> i32 { + println!("Could not embed into specified window."); + std::process::exit(1); +} |