diff options
author | Theodore Dubois <tblodt@icloud.com> | 2019-04-28 06:24:58 -0700 |
---|---|---|
committer | Christian Duerr <chrisduerr@users.noreply.github.com> | 2019-04-28 13:24:58 +0000 |
commit | dbd8538762ef8968a493e1bf996e8693479ca783 (patch) | |
tree | 32ac2a6a5e01238a272d4ba534551d2e42903c7a /alacritty_terminal/src/event.rs | |
parent | 9c6d12ea2c863ba76015bdedc00db13b7307725a (diff) | |
download | alacritty-dbd8538762ef8968a493e1bf996e8693479ca783.tar.gz alacritty-dbd8538762ef8968a493e1bf996e8693479ca783.zip |
Split alacritty into a separate crates
The crate containing the entry point is called alacritty, and the crate
containing everything else is called alacritty_terminal.
Diffstat (limited to 'alacritty_terminal/src/event.rs')
-rw-r--r-- | alacritty_terminal/src/event.rs | 595 |
1 files changed, 595 insertions, 0 deletions
diff --git a/alacritty_terminal/src/event.rs b/alacritty_terminal/src/event.rs new file mode 100644 index 00000000..1f3e9ca5 --- /dev/null +++ b/alacritty_terminal/src/event.rs @@ -0,0 +1,595 @@ +//! 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::mpsc; +use std::time::Instant; + +use copypasta::{Buffer as ClipboardBuffer, Clipboard, Load, Store}; +use glutin::dpi::PhysicalSize; +use glutin::{self, ElementState, Event, ModifiersState, MouseButton}; +use parking_lot::MutexGuard; +use serde_json as json; + +use crate::cli::Options; +use crate::config::{self, Config}; +use crate::display::OnResize; +use crate::grid::Scroll; +use crate::index::{Column, Line, Point, Side}; +use crate::input::{self, KeyBinding, MouseBinding}; +use crate::selection::Selection; +use crate::sync::FairMutex; +use crate::term::cell::Cell; +use crate::term::{SizeInfo, Term}; +#[cfg(unix)] +use crate::tty; +use crate::util::fmt::Red; +use crate::util::{limit, start_daemon}; +use crate::window::Window; + +/// Byte sequences are sent to a `Notify` in response to some events +pub trait Notify { + /// Notify that an escape sequence should be written to the pty + /// + /// TODO this needs to be able to error somehow + fn notify<B: Into<Cow<'static, [u8]>>>(&mut self, _: B); +} + +pub struct ActionContext<'a, N> { + pub notifier: &'a mut N, + pub terminal: &'a mut Term, + pub size_info: &'a mut SizeInfo, + pub mouse: &'a mut Mouse, + pub received_count: &'a mut usize, + pub suppress_chars: &'a mut bool, + pub last_modifiers: &'a mut ModifiersState, + pub window_changes: &'a mut WindowChanges, +} + +impl<'a, N: Notify + 'a> input::ActionContext for ActionContext<'a, N> { + 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(&self, buffer: ClipboardBuffer) { + if let Some(selected) = self.terminal.selection_to_string() { + if !selected.is_empty() { + Clipboard::new() + .and_then(|mut clipboard| clipboard.store(selected, buffer)) + .unwrap_or_else(|err| { + warn!("Error storing selection to clipboard. {}", Red(err)); + }); + } + } + } + + 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 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> { + self.terminal.pixels_to_coords(self.mouse.x as usize, self.mouse.y as usize) + } + + #[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 last_modifiers(&mut self) -> &mut ModifiersState { + &mut self.last_modifiers + } + + #[inline] + fn hide_window(&mut self) { + self.window_changes.hide = true; + } + + #[inline] + fn terminal(&self) -> &Term { + self.terminal + } + + #[inline] + fn terminal_mut(&mut self) -> &mut Term { + 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 toggle_fullscreen(&mut self) { + self.window_changes.toggle_fullscreen(); + } + + #[cfg(target_os = "macos")] + fn toggle_simple_fullscreen(&mut self) { + self.window_changes.toggle_simple_fullscreen() + } +} + +/// The ActionContext can't really have direct access to the Window +/// with the current design. Event handlers that want to change the +/// window must set these flags instead. The processor will trigger +/// the actual changes. +#[derive(Default)] +pub struct WindowChanges { + pub hide: bool, + pub toggle_fullscreen: bool, + #[cfg(target_os = "macos")] + pub toggle_simple_fullscreen: bool, +} + +impl WindowChanges { + fn clear(&mut self) { + *self = WindowChanges::default(); + } + + fn toggle_fullscreen(&mut self) { + self.toggle_fullscreen = !self.toggle_fullscreen; + } + + #[cfg(target_os = "macos")] + fn toggle_simple_fullscreen(&mut self) { + self.toggle_simple_fullscreen = !self.toggle_simple_fullscreen; + } +} + +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> { + key_bindings: Vec<KeyBinding>, + mouse_bindings: Vec<MouseBinding>, + mouse_config: config::Mouse, + scrolling_config: config::Scrolling, + print_events: bool, + wait_for_event: bool, + notifier: N, + mouse: Mouse, + resize_tx: mpsc::Sender<PhysicalSize>, + ref_test: bool, + size_info: SizeInfo, + hide_mouse_when_typing: bool, + hide_mouse: bool, + received_count: usize, + suppress_chars: bool, + last_modifiers: ModifiersState, + pending_events: Vec<Event>, + window_changes: WindowChanges, + save_to_clipboard: bool, + alt_send_esc: bool, + is_fullscreen: bool, + is_simple_fullscreen: bool, +} + +/// Notify that the terminal was resized +/// +/// Currently this just forwards the notice to the input processor. +impl<N> OnResize for Processor<N> { + fn on_resize(&mut self, size: &SizeInfo) { + self.size_info = size.to_owned(); + } +} + +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, + resize_tx: mpsc::Sender<PhysicalSize>, + options: &Options, + config: &Config, + ref_test: bool, + size_info: SizeInfo, + ) -> Processor<N> { + Processor { + key_bindings: config.key_bindings().to_vec(), + mouse_bindings: config.mouse_bindings().to_vec(), + mouse_config: config.mouse().to_owned(), + scrolling_config: config.scrolling(), + print_events: options.print_events, + wait_for_event: true, + notifier, + resize_tx, + ref_test, + mouse: Default::default(), + size_info, + hide_mouse_when_typing: config.hide_mouse_when_typing(), + hide_mouse: false, + received_count: 0, + suppress_chars: false, + last_modifiers: Default::default(), + pending_events: Vec::with_capacity(4), + window_changes: Default::default(), + save_to_clipboard: config.selection().save_to_clipboard, + alt_send_esc: config.alt_send_esc(), + is_fullscreen: false, + is_simple_fullscreen: false, + } + } + + /// Handle events from glutin + /// + /// Doesn't take self mutably due to borrow checking. Kinda uggo but w/e. + fn handle_event<'a>( + processor: &mut input::Processor<'a, ActionContext<'a, N>>, + event: Event, + ref_test: bool, + resize_tx: &mpsc::Sender<PhysicalSize>, + hide_mouse: &mut bool, + window_is_focused: &mut bool, + ) { + match event { + // Pass on device events + Event::DeviceEvent { .. } | Event::Suspended { .. } => (), + Event::WindowEvent { event, .. } => { + use glutin::WindowEvent::*; + match event { + CloseRequested => { + if ref_test { + // dump grid state + let mut grid = processor.ctx.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(processor.ctx.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"); + } + + processor.ctx.terminal.exit(); + }, + Resized(lsize) => { + // Resize events are emitted via glutin/winit with logical sizes + // However the terminal, window and renderer use physical sizes + // so a conversion must be done here + resize_tx + .send(lsize.to_physical(processor.ctx.size_info.dpr)) + .expect("send new size"); + processor.ctx.terminal.dirty = true; + }, + KeyboardInput { input, .. } => { + processor.process_key(input); + if input.state == ElementState::Pressed { + // Hide cursor while typing + *hide_mouse = true; + } + }, + ReceivedCharacter(c) => { + processor.received_char(c); + }, + MouseInput { state, button, modifiers, .. } => { + if !cfg!(target_os = "macos") || *window_is_focused { + *hide_mouse = false; + 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); + + *hide_mouse = false; + processor.mouse_moved(x as usize, y as usize, modifiers); + }, + MouseWheel { delta, phase, modifiers, .. } => { + *hide_mouse = false; + processor.on_mouse_wheel(delta, phase, modifiers); + }, + Refresh => { + processor.ctx.terminal.dirty = true; + }, + Focused(is_focused) => { + *window_is_focused = is_focused; + + if is_focused { + processor.ctx.terminal.dirty = true; + processor.ctx.terminal.next_is_urgent = Some(false); + } else { + processor.ctx.terminal.reset_url_highlight(); + processor.ctx.terminal.dirty = true; + *hide_mouse = false; + } + + processor.on_focus_change(is_focused); + }, + DroppedFile(path) => { + use crate::input::ActionContext; + let path: String = path.to_string_lossy().into(); + processor.ctx.write_to_pty(path.into_bytes()); + }, + HiDpiFactorChanged(new_dpr) => { + processor.ctx.size_info.dpr = new_dpr; + processor.ctx.terminal.dirty = true; + }, + _ => (), + } + }, + Event::Awakened => { + processor.ctx.terminal.dirty = true; + }, + } + } + + /// Process events. When `wait_for_event` is set, this method is guaranteed + /// to process at least one event. + pub fn process_events<'a>( + &mut self, + term: &'a FairMutex<Term>, + window: &mut Window, + ) -> MutexGuard<'a, Term> { + // Terminal is lazily initialized the first time an event is returned + // from the blocking WaitEventsIterator. Otherwise, the pty reader would + // be blocked the entire time we wait for input! + let mut terminal; + + self.pending_events.clear(); + + { + // Ditto on lazy initialization for context and processor. + let context; + let mut processor: input::Processor<'_, ActionContext<'_, N>>; + + let print_events = self.print_events; + + let ref_test = self.ref_test; + let resize_tx = &self.resize_tx; + + if self.wait_for_event { + // A Vec is used here since wait_events can potentially yield + // multiple events before the interrupt is handled. For example, + // Resize and Moved events. + let pending_events = &mut self.pending_events; + window.wait_events(|e| { + pending_events.push(e); + glutin::ControlFlow::Break + }); + } + + terminal = term.lock(); + + context = ActionContext { + terminal: &mut terminal, + notifier: &mut self.notifier, + mouse: &mut self.mouse, + size_info: &mut self.size_info, + received_count: &mut self.received_count, + suppress_chars: &mut self.suppress_chars, + last_modifiers: &mut self.last_modifiers, + window_changes: &mut self.window_changes, + }; + + processor = input::Processor { + ctx: context, + scrolling_config: &self.scrolling_config, + mouse_config: &self.mouse_config, + key_bindings: &self.key_bindings[..], + mouse_bindings: &self.mouse_bindings[..], + save_to_clipboard: self.save_to_clipboard, + alt_send_esc: self.alt_send_esc, + }; + + let mut window_is_focused = window.is_focused; + + // Scope needed to that hide_mouse isn't borrowed after the scope + // ends. + { + let hide_mouse = &mut self.hide_mouse; + let mut process = |event| { + if print_events { + info!("glutin event: {:?}", event); + } + Processor::handle_event( + &mut processor, + event, + ref_test, + resize_tx, + hide_mouse, + &mut window_is_focused, + ); + }; + + for event in self.pending_events.drain(..) { + process(event); + } + + window.poll_events(process); + } + + if self.hide_mouse_when_typing { + window.set_mouse_visible(!self.hide_mouse); + } + + window.is_focused = window_is_focused; + } + + if self.window_changes.hide { + window.hide(); + } + + #[cfg(target_os = "macos")] + { + if self.window_changes.toggle_simple_fullscreen && !self.is_fullscreen { + window.set_simple_fullscreen(!self.is_simple_fullscreen); + self.is_simple_fullscreen = !self.is_simple_fullscreen; + } + } + + if self.window_changes.toggle_fullscreen && !self.is_simple_fullscreen { + window.set_fullscreen(!self.is_fullscreen); + self.is_fullscreen = !self.is_fullscreen; + } + + self.window_changes.clear(); + self.wait_for_event = !terminal.dirty; + + terminal + } + + pub fn update_config(&mut self, config: &Config) { + self.key_bindings = config.key_bindings().to_vec(); + self.mouse_bindings = config.mouse_bindings().to_vec(); + self.mouse_config = config.mouse().to_owned(); + self.save_to_clipboard = config.selection().save_to_clipboard; + self.alt_send_esc = config.alt_send_esc(); + } +} |