aboutsummaryrefslogtreecommitdiff
path: root/alacritty
diff options
context:
space:
mode:
Diffstat (limited to 'alacritty')
-rw-r--r--alacritty/Cargo.toml14
-rw-r--r--alacritty/src/cli.rs8
-rw-r--r--alacritty/src/config/bindings.rs1409
-rw-r--r--alacritty/src/config/mod.rs (renamed from alacritty/src/config.rs)27
-rw-r--r--alacritty/src/config/monitor.rs58
-rw-r--r--alacritty/src/config/mouse.rs115
-rw-r--r--alacritty/src/config/test.rs24
-rw-r--r--alacritty/src/config/ui_config.rs63
-rw-r--r--alacritty/src/display.rs476
-rw-r--r--alacritty/src/event.rs651
-rw-r--r--alacritty/src/input.rs1169
-rw-r--r--alacritty/src/logging.rs22
-rw-r--r--alacritty/src/main.rs182
-rw-r--r--alacritty/src/window.rs434
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);
+}