| //! Process window events. |
| |
| use std::borrow::Cow; |
| use std::cmp::min; |
| use std::collections::{HashMap, HashSet, VecDeque}; |
| use std::error::Error; |
| use std::ffi::OsStr; |
| use std::fmt::Debug; |
| #[cfg(not(windows))] |
| use std::os::unix::io::RawFd; |
| use std::path::PathBuf; |
| use std::rc::Rc; |
| use std::time::{Duration, Instant}; |
| use std::{env, f32, mem}; |
| |
| use ahash::RandomState; |
| use crossfont::Size as FontSize; |
| use glutin::display::{Display as GlutinDisplay, GetGlDisplay}; |
| use log::{debug, error, info, warn}; |
| use raw_window_handle::HasRawDisplayHandle; |
| use winit::event::{ |
| ElementState, Event as WinitEvent, Ime, Modifiers, MouseButton, StartCause, |
| Touch as TouchEvent, WindowEvent, |
| }; |
| use winit::event_loop::{ |
| ControlFlow, DeviceEvents, EventLoop, EventLoopProxy, EventLoopWindowTarget, |
| }; |
| use winit::window::WindowId; |
| |
| use alacritty_terminal::event::{Event as TerminalEvent, EventListener, Notify}; |
| use alacritty_terminal::event_loop::Notifier; |
| use alacritty_terminal::grid::{BidirectionalIterator, Dimensions, Scroll}; |
| use alacritty_terminal::index::{Boundary, Column, Direction, Line, Point, Side}; |
| use alacritty_terminal::selection::{Selection, SelectionType}; |
| use alacritty_terminal::term::search::{Match, RegexSearch}; |
| use alacritty_terminal::term::{self, ClipboardType, Term, TermMode}; |
| |
| #[cfg(unix)] |
| use crate::cli::{IpcConfig, ParsedOptions}; |
| use crate::cli::{Options as CliOptions, WindowOptions}; |
| use crate::clipboard::Clipboard; |
| use crate::config::ui_config::{HintAction, HintInternalAction}; |
| use crate::config::{self, UiConfig}; |
| #[cfg(not(windows))] |
| use crate::daemon::foreground_process_path; |
| use crate::daemon::spawn_daemon; |
| use crate::display::color::Rgb; |
| use crate::display::hint::HintMatch; |
| use crate::display::window::Window; |
| use crate::display::{Display, Preedit, SizeInfo}; |
| use crate::input::{self, ActionContext as _, FONT_SIZE_STEP}; |
| use crate::logging::LOG_TARGET_CONFIG; |
| use crate::message_bar::{Message, MessageBuffer}; |
| use crate::scheduler::{Scheduler, TimerId, Topic}; |
| use crate::window_context::WindowContext; |
| |
| /// Duration after the last user input until an unlimited search is performed. |
| pub const TYPING_SEARCH_DELAY: Duration = Duration::from_millis(500); |
| |
| /// Maximum number of lines for the blocking search while still typing the search regex. |
| const MAX_SEARCH_WHILE_TYPING: Option<usize> = Some(1000); |
| |
| /// Maximum number of search terms stored in the history. |
| const MAX_SEARCH_HISTORY_SIZE: usize = 255; |
| |
| /// Touch zoom speed. |
| const TOUCH_ZOOM_FACTOR: f32 = 0.01; |
| |
| /// Alacritty events. |
| #[derive(Debug, Clone)] |
| pub struct Event { |
| /// Limit event to a specific window. |
| window_id: Option<WindowId>, |
| |
| /// Event payload. |
| payload: EventType, |
| } |
| |
| impl Event { |
| pub fn new<I: Into<Option<WindowId>>>(payload: EventType, window_id: I) -> Self { |
| Self { window_id: window_id.into(), payload } |
| } |
| } |
| |
| impl From<Event> for WinitEvent<Event> { |
| fn from(event: Event) -> Self { |
| WinitEvent::UserEvent(event) |
| } |
| } |
| |
| /// Alacritty events. |
| #[derive(Debug, Clone)] |
| pub enum EventType { |
| Terminal(TerminalEvent), |
| ConfigReload(PathBuf), |
| Message(Message), |
| Scroll(Scroll), |
| CreateWindow(WindowOptions), |
| #[cfg(unix)] |
| IpcConfig(IpcConfig), |
| BlinkCursor, |
| BlinkCursorTimeout, |
| SearchNext, |
| Frame, |
| } |
| |
| impl From<TerminalEvent> for EventType { |
| fn from(event: TerminalEvent) -> Self { |
| Self::Terminal(event) |
| } |
| } |
| |
| /// Regex search state. |
| pub struct SearchState { |
| /// Search direction. |
| pub direction: Direction, |
| |
| /// Current position in the search history. |
| pub history_index: Option<usize>, |
| |
| /// Change in display offset since the beginning of the search. |
| display_offset_delta: i32, |
| |
| /// Search origin in viewport coordinates relative to original display offset. |
| origin: Point, |
| |
| /// Focused match during active search. |
| focused_match: Option<Match>, |
| |
| /// Search regex and history. |
| /// |
| /// During an active search, the first element is the user's current input. |
| /// |
| /// While going through history, the [`SearchState::history_index`] will point to the element |
| /// in history which is currently being previewed. |
| history: VecDeque<String>, |
| |
| /// Compiled search automatons. |
| dfas: Option<RegexSearch>, |
| } |
| |
| impl SearchState { |
| /// Search regex text if a search is active. |
| pub fn regex(&self) -> Option<&String> { |
| self.history_index.and_then(|index| self.history.get(index)) |
| } |
| |
| /// Direction of the search from the search origin. |
| pub fn direction(&self) -> Direction { |
| self.direction |
| } |
| |
| /// Focused match during vi-less search. |
| pub fn focused_match(&self) -> Option<&Match> { |
| self.focused_match.as_ref() |
| } |
| |
| /// Clear the focused match. |
| pub fn clear_focused_match(&mut self) { |
| self.focused_match = None; |
| } |
| |
| /// Active search dfas. |
| pub fn dfas(&mut self) -> Option<&mut RegexSearch> { |
| self.dfas.as_mut() |
| } |
| |
| /// Search regex text if a search is active. |
| fn regex_mut(&mut self) -> Option<&mut String> { |
| self.history_index.and_then(move |index| self.history.get_mut(index)) |
| } |
| } |
| |
| impl Default for SearchState { |
| fn default() -> Self { |
| Self { |
| direction: Direction::Right, |
| display_offset_delta: Default::default(), |
| focused_match: Default::default(), |
| history_index: Default::default(), |
| history: Default::default(), |
| origin: Default::default(), |
| dfas: Default::default(), |
| } |
| } |
| } |
| |
| /// Vi inline search state. |
| pub struct InlineSearchState { |
| /// Whether inline search is currently waiting for search character input. |
| pub char_pending: bool, |
| pub character: Option<char>, |
| |
| direction: Direction, |
| stop_short: bool, |
| } |
| |
| impl Default for InlineSearchState { |
| fn default() -> Self { |
| Self { |
| direction: Direction::Right, |
| char_pending: Default::default(), |
| stop_short: Default::default(), |
| character: Default::default(), |
| } |
| } |
| } |
| |
| pub struct ActionContext<'a, N, T> { |
| pub notifier: &'a mut N, |
| pub terminal: &'a mut Term<T>, |
| pub clipboard: &'a mut Clipboard, |
| pub mouse: &'a mut Mouse, |
| pub touch: &'a mut TouchPurpose, |
| pub modifiers: &'a mut Modifiers, |
| pub display: &'a mut Display, |
| pub message_buffer: &'a mut MessageBuffer, |
| pub config: &'a UiConfig, |
| pub cursor_blink_timed_out: &'a mut bool, |
| #[cfg(target_os = "macos")] |
| pub event_loop: &'a EventLoopWindowTarget<Event>, |
| pub event_proxy: &'a EventLoopProxy<Event>, |
| pub scheduler: &'a mut Scheduler, |
| pub search_state: &'a mut SearchState, |
| pub inline_search_state: &'a mut InlineSearchState, |
| pub dirty: &'a mut bool, |
| pub occluded: &'a mut bool, |
| pub preserve_title: bool, |
| #[cfg(not(windows))] |
| pub master_fd: RawFd, |
| #[cfg(not(windows))] |
| pub shell_pid: u32, |
| } |
| |
| impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionContext<'a, N, T> { |
| #[inline] |
| fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&self, val: B) { |
| self.notifier.notify(val); |
| } |
| |
| /// Request a redraw. |
| #[inline] |
| fn mark_dirty(&mut self) { |
| *self.dirty = true; |
| } |
| |
| #[inline] |
| fn size_info(&self) -> SizeInfo { |
| self.display.size_info |
| } |
| |
| fn scroll(&mut self, scroll: Scroll) { |
| let old_offset = self.terminal.grid().display_offset() as i32; |
| |
| let old_vi_cursor = self.terminal.vi_mode_cursor; |
| self.terminal.scroll_display(scroll); |
| |
| let lines_changed = old_offset - self.terminal.grid().display_offset() as i32; |
| |
| // Keep track of manual display offset changes during search. |
| if self.search_active() { |
| self.search_state.display_offset_delta += lines_changed; |
| } |
| |
| let vi_mode = self.terminal.mode().contains(TermMode::VI); |
| |
| // Update selection. |
| if vi_mode && self.terminal.selection.as_ref().map_or(false, |s| !s.is_empty()) { |
| self.update_selection(self.terminal.vi_mode_cursor.point, Side::Right); |
| } else if self.mouse.left_button_state == ElementState::Pressed |
| || self.mouse.right_button_state == ElementState::Pressed |
| { |
| let display_offset = self.terminal.grid().display_offset(); |
| let point = self.mouse.point(&self.size_info(), display_offset); |
| self.update_selection(point, self.mouse.cell_side); |
| } |
| |
| // Scrolling inside Vi mode moves the cursor, so start typing. |
| if vi_mode { |
| self.on_typing_start(); |
| } |
| |
| // Update dirty if actually scrolled or moved Vi cursor in Vi mode. |
| *self.dirty |= |
| lines_changed != 0 || (vi_mode && old_vi_cursor != self.terminal.vi_mode_cursor); |
| } |
| |
| // Copy text selection. |
| fn copy_selection(&mut self, ty: ClipboardType) { |
| let text = match self.terminal.selection_to_string().filter(|s| !s.is_empty()) { |
| Some(text) => text, |
| None => return, |
| }; |
| |
| if ty == ClipboardType::Selection && self.config.selection.save_to_clipboard { |
| self.clipboard.store(ClipboardType::Clipboard, text.clone()); |
| } |
| self.clipboard.store(ty, text); |
| } |
| |
| fn selection_is_empty(&self) -> bool { |
| self.terminal.selection.as_ref().map_or(true, Selection::is_empty) |
| } |
| |
| fn clear_selection(&mut self) { |
| // Clear the selection on the terminal. |
| let selection = self.terminal.selection.take(); |
| // Mark the terminal as dirty when selection wasn't empty. |
| *self.dirty |= selection.map_or(false, |s| !s.is_empty()); |
| } |
| |
| fn update_selection(&mut self, mut point: Point, side: Side) { |
| let mut selection = match self.terminal.selection.take() { |
| Some(selection) => selection, |
| None => return, |
| }; |
| |
| // Treat motion over message bar like motion over the last line. |
| point.line = min(point.line, self.terminal.bottommost_line()); |
| |
| // Update selection. |
| selection.update(point, side); |
| |
| // Move vi cursor and expand selection. |
| if self.terminal.mode().contains(TermMode::VI) && !self.search_active() { |
| self.terminal.vi_mode_cursor.point = point; |
| selection.include_all(); |
| } |
| |
| self.terminal.selection = Some(selection); |
| *self.dirty = true; |
| } |
| |
| fn start_selection(&mut self, ty: SelectionType, point: Point, side: Side) { |
| self.terminal.selection = Some(Selection::new(ty, point, side)); |
| *self.dirty = true; |
| |
| self.copy_selection(ClipboardType::Selection); |
| } |
| |
| fn toggle_selection(&mut self, ty: SelectionType, point: Point, side: Side) { |
| match &mut self.terminal.selection { |
| Some(selection) if selection.ty == ty && !selection.is_empty() => { |
| self.clear_selection(); |
| }, |
| Some(selection) if !selection.is_empty() => { |
| selection.ty = ty; |
| *self.dirty = true; |
| |
| self.copy_selection(ClipboardType::Selection); |
| }, |
| _ => self.start_selection(ty, point, side), |
| } |
| } |
| |
| #[inline] |
| fn mouse_mode(&self) -> bool { |
| self.terminal.mode().intersects(TermMode::MOUSE_MODE) |
| && !self.terminal.mode().contains(TermMode::VI) |
| } |
| |
| #[inline] |
| fn mouse_mut(&mut self) -> &mut Mouse { |
| self.mouse |
| } |
| |
| #[inline] |
| fn mouse(&self) -> &Mouse { |
| self.mouse |
| } |
| |
| #[inline] |
| fn touch_purpose(&mut self) -> &mut TouchPurpose { |
| self.touch |
| } |
| |
| #[inline] |
| fn modifiers(&mut self) -> &mut Modifiers { |
| self.modifiers |
| } |
| |
| #[inline] |
| fn window(&mut self) -> &mut Window { |
| &mut self.display.window |
| } |
| |
| #[inline] |
| fn display(&mut self) -> &mut Display { |
| self.display |
| } |
| |
| #[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 mut env_args = env::args(); |
| let alacritty = env_args.next().unwrap(); |
| |
| let mut args: Vec<String> = Vec::new(); |
| |
| // Reuse the arguments passed to Alacritty for the new instance. |
| #[allow(clippy::while_let_on_iterator)] |
| while let Some(arg) = env_args.next() { |
| // New instances shouldn't inherit command. |
| if arg == "-e" || arg == "--command" { |
| break; |
| } |
| |
| // On unix, the working directory of the foreground shell is used by `start_daemon`. |
| #[cfg(not(windows))] |
| if arg == "--working-directory" { |
| let _ = env_args.next(); |
| continue; |
| } |
| |
| args.push(arg); |
| } |
| |
| self.spawn_daemon(&alacritty, &args); |
| } |
| |
| #[cfg(not(windows))] |
| fn create_new_window(&mut self, #[cfg(target_os = "macos")] tabbing_id: Option<String>) { |
| let mut options = WindowOptions::default(); |
| if let Ok(working_directory) = foreground_process_path(self.master_fd, self.shell_pid) { |
| options.terminal_options.working_directory = Some(working_directory); |
| } |
| |
| #[cfg(target_os = "macos")] |
| { |
| options.window_tabbing_id = tabbing_id; |
| } |
| |
| let _ = self.event_proxy.send_event(Event::new(EventType::CreateWindow(options), None)); |
| } |
| |
| #[cfg(windows)] |
| fn create_new_window(&mut self) { |
| let _ = self |
| .event_proxy |
| .send_event(Event::new(EventType::CreateWindow(WindowOptions::default()), None)); |
| } |
| |
| fn spawn_daemon<I, S>(&self, program: &str, args: I) |
| where |
| I: IntoIterator<Item = S> + Debug + Copy, |
| S: AsRef<OsStr>, |
| { |
| #[cfg(not(windows))] |
| let result = spawn_daemon(program, args, self.master_fd, self.shell_pid); |
| #[cfg(windows)] |
| let result = spawn_daemon(program, args); |
| |
| match result { |
| Ok(_) => debug!("Launched {} with args {:?}", program, args), |
| Err(_) => warn!("Unable to launch {} with args {:?}", program, args), |
| } |
| } |
| |
| fn change_font_size(&mut self, delta: f32) { |
| // Round to pick integral px steps, since fonts look better on them. |
| let new_size = self.display.font_size.as_px().round() + delta; |
| self.display.font_size = FontSize::from_px(new_size); |
| let font = self.config.font.clone().with_size(self.display.font_size); |
| self.display.pending_update.set_font(font); |
| } |
| |
| fn reset_font_size(&mut self) { |
| let scale_factor = self.display.window.scale_factor as f32; |
| self.display.font_size = self.config.font.size().scale(scale_factor); |
| self.display |
| .pending_update |
| .set_font(self.config.font.clone().with_size(self.display.font_size)); |
| } |
| |
| #[inline] |
| fn pop_message(&mut self) { |
| if !self.message_buffer.is_empty() { |
| self.display.pending_update.dirty = true; |
| self.message_buffer.pop(); |
| } |
| } |
| |
| #[inline] |
| fn start_search(&mut self, direction: Direction) { |
| // Only create new history entry if the previous regex wasn't empty. |
| if self.search_state.history.front().map_or(true, |regex| !regex.is_empty()) { |
| self.search_state.history.push_front(String::new()); |
| self.search_state.history.truncate(MAX_SEARCH_HISTORY_SIZE); |
| } |
| |
| self.search_state.history_index = Some(0); |
| self.search_state.direction = direction; |
| self.search_state.focused_match = None; |
| |
| // Store original search position as origin and reset location. |
| if self.terminal.mode().contains(TermMode::VI) { |
| self.search_state.origin = self.terminal.vi_mode_cursor.point; |
| self.search_state.display_offset_delta = 0; |
| |
| // Adjust origin for content moving upward on search start. |
| if self.terminal.grid().cursor.point.line + 1 == self.terminal.screen_lines() { |
| self.search_state.origin.line -= 1; |
| } |
| } else { |
| let viewport_top = Line(-(self.terminal.grid().display_offset() as i32)) - 1; |
| let viewport_bottom = viewport_top + self.terminal.bottommost_line(); |
| let last_column = self.terminal.last_column(); |
| self.search_state.origin = match direction { |
| Direction::Right => Point::new(viewport_top, Column(0)), |
| Direction::Left => Point::new(viewport_bottom, last_column), |
| }; |
| } |
| |
| // Enable IME so we can input into the search bar with it if we were in Vi mode. |
| self.window().set_ime_allowed(true); |
| |
| self.display.damage_tracker.frame().mark_fully_damaged(); |
| self.display.pending_update.dirty = true; |
| } |
| |
| #[inline] |
| fn confirm_search(&mut self) { |
| // Just cancel search when not in vi mode. |
| if !self.terminal.mode().contains(TermMode::VI) { |
| self.cancel_search(); |
| return; |
| } |
| |
| // Force unlimited search if the previous one was interrupted. |
| let timer_id = TimerId::new(Topic::DelayedSearch, self.display.window.id()); |
| if self.scheduler.scheduled(timer_id) { |
| self.goto_match(None); |
| } |
| |
| self.exit_search(); |
| } |
| |
| #[inline] |
| fn cancel_search(&mut self) { |
| if self.terminal.mode().contains(TermMode::VI) { |
| // Recover pre-search state in vi mode. |
| self.search_reset_state(); |
| } else if let Some(focused_match) = &self.search_state.focused_match { |
| // Create a selection for the focused match. |
| let start = *focused_match.start(); |
| let end = *focused_match.end(); |
| self.start_selection(SelectionType::Simple, start, Side::Left); |
| self.update_selection(end, Side::Right); |
| self.copy_selection(ClipboardType::Selection); |
| } |
| |
| self.search_state.dfas = None; |
| |
| self.exit_search(); |
| } |
| |
| #[inline] |
| fn search_input(&mut self, c: char) { |
| match self.search_state.history_index { |
| Some(0) => (), |
| // When currently in history, replace active regex with history on change. |
| Some(index) => { |
| self.search_state.history[0] = self.search_state.history[index].clone(); |
| self.search_state.history_index = Some(0); |
| }, |
| None => return, |
| } |
| let regex = &mut self.search_state.history[0]; |
| |
| match c { |
| // Handle backspace/ctrl+h. |
| '\x08' | '\x7f' => { |
| let _ = regex.pop(); |
| }, |
| // Add ascii and unicode text. |
| ' '..='~' | '\u{a0}'..='\u{10ffff}' => regex.push(c), |
| // Ignore non-printable characters. |
| _ => return, |
| } |
| |
| if !self.terminal.mode().contains(TermMode::VI) { |
| // Clear selection so we do not obstruct any matches. |
| self.terminal.selection = None; |
| } |
| |
| self.update_search(); |
| } |
| |
| #[inline] |
| fn search_pop_word(&mut self) { |
| if let Some(regex) = self.search_state.regex_mut() { |
| *regex = regex.trim_end().to_owned(); |
| regex.truncate(regex.rfind(' ').map_or(0, |i| i + 1)); |
| self.update_search(); |
| } |
| } |
| |
| /// Go to the previous regex in the search history. |
| #[inline] |
| fn search_history_previous(&mut self) { |
| let index = match &mut self.search_state.history_index { |
| None => return, |
| Some(index) if *index + 1 >= self.search_state.history.len() => return, |
| Some(index) => index, |
| }; |
| |
| *index += 1; |
| self.update_search(); |
| } |
| |
| /// Go to the previous regex in the search history. |
| #[inline] |
| fn search_history_next(&mut self) { |
| let index = match &mut self.search_state.history_index { |
| Some(0) | None => return, |
| Some(index) => index, |
| }; |
| |
| *index -= 1; |
| self.update_search(); |
| } |
| |
| #[inline] |
| fn advance_search_origin(&mut self, direction: Direction) { |
| // Use focused match as new search origin if available. |
| if let Some(focused_match) = &self.search_state.focused_match { |
| let new_origin = match direction { |
| Direction::Right => focused_match.end().add(self.terminal, Boundary::None, 1), |
| Direction::Left => focused_match.start().sub(self.terminal, Boundary::None, 1), |
| }; |
| |
| self.terminal.scroll_to_point(new_origin); |
| |
| self.search_state.display_offset_delta = 0; |
| self.search_state.origin = new_origin; |
| } |
| |
| // Search for the next match using the supplied direction. |
| let search_direction = mem::replace(&mut self.search_state.direction, direction); |
| self.goto_match(None); |
| self.search_state.direction = search_direction; |
| |
| // If we found a match, we set the search origin right in front of it to make sure that |
| // after modifications to the regex the search is started without moving the focused match |
| // around. |
| let focused_match = match &self.search_state.focused_match { |
| Some(focused_match) => focused_match, |
| None => return, |
| }; |
| |
| // Set new origin to the left/right of the match, depending on search direction. |
| let new_origin = match self.search_state.direction { |
| Direction::Right => *focused_match.start(), |
| Direction::Left => *focused_match.end(), |
| }; |
| |
| // Store the search origin with display offset by checking how far we need to scroll to it. |
| let old_display_offset = self.terminal.grid().display_offset() as i32; |
| self.terminal.scroll_to_point(new_origin); |
| let new_display_offset = self.terminal.grid().display_offset() as i32; |
| self.search_state.display_offset_delta = new_display_offset - old_display_offset; |
| |
| // Store origin and scroll back to the match. |
| self.terminal.scroll_display(Scroll::Delta(-self.search_state.display_offset_delta)); |
| self.search_state.origin = new_origin; |
| } |
| |
| /// Find the next search match. |
| fn search_next(&mut self, origin: Point, direction: Direction, side: Side) -> Option<Match> { |
| self.search_state |
| .dfas |
| .as_mut() |
| .and_then(|dfas| self.terminal.search_next(dfas, origin, direction, side, None)) |
| } |
| |
| #[inline] |
| fn search_direction(&self) -> Direction { |
| self.search_state.direction |
| } |
| |
| #[inline] |
| fn search_active(&self) -> bool { |
| self.search_state.history_index.is_some() |
| } |
| |
| /// Handle keyboard typing start. |
| /// |
| /// This will temporarily disable some features like terminal cursor blinking or the mouse |
| /// cursor. |
| /// |
| /// All features are re-enabled again automatically. |
| #[inline] |
| fn on_typing_start(&mut self) { |
| // Disable cursor blinking. |
| let timer_id = TimerId::new(Topic::BlinkCursor, self.display.window.id()); |
| if self.scheduler.unschedule(timer_id).is_some() { |
| self.schedule_blinking(); |
| |
| // Mark the cursor as visible and queue redraw if the cursor was hidden. |
| if mem::take(&mut self.display.cursor_hidden) { |
| *self.dirty = true; |
| } |
| } else if *self.cursor_blink_timed_out { |
| self.update_cursor_blinking(); |
| } |
| |
| // Hide mouse cursor. |
| if self.config.mouse.hide_when_typing { |
| self.display.window.set_mouse_visible(false); |
| } |
| } |
| |
| /// Process a new character for keyboard hints. |
| fn hint_input(&mut self, c: char) { |
| if let Some(hint) = self.display.hint_state.keyboard_input(self.terminal, c) { |
| self.mouse.block_hint_launcher = false; |
| self.trigger_hint(&hint); |
| } |
| *self.dirty = true; |
| } |
| |
| /// Trigger a hint action. |
| fn trigger_hint(&mut self, hint: &HintMatch) { |
| if self.mouse.block_hint_launcher { |
| return; |
| } |
| |
| let hint_bounds = hint.bounds(); |
| let text = match hint.hyperlink() { |
| Some(hyperlink) => hyperlink.uri().to_owned(), |
| None => self.terminal.bounds_to_string(*hint_bounds.start(), *hint_bounds.end()), |
| }; |
| |
| match &hint.action() { |
| // Launch an external program. |
| HintAction::Command(command) => { |
| let mut args = command.args().to_vec(); |
| args.push(text); |
| self.spawn_daemon(command.program(), &args); |
| }, |
| // Copy the text to the clipboard. |
| HintAction::Action(HintInternalAction::Copy) => { |
| self.clipboard.store(ClipboardType::Clipboard, text); |
| }, |
| // Write the text to the PTY/search. |
| HintAction::Action(HintInternalAction::Paste) => self.paste(&text, true), |
| // Select the text. |
| HintAction::Action(HintInternalAction::Select) => { |
| self.start_selection(SelectionType::Simple, *hint_bounds.start(), Side::Left); |
| self.update_selection(*hint_bounds.end(), Side::Right); |
| self.copy_selection(ClipboardType::Selection); |
| }, |
| // Move the vi mode cursor. |
| HintAction::Action(HintInternalAction::MoveViModeCursor) => { |
| // Enter vi mode if we're not in it already. |
| if !self.terminal.mode().contains(TermMode::VI) { |
| self.terminal.toggle_vi_mode(); |
| } |
| |
| self.terminal.vi_goto_point(*hint_bounds.start()); |
| self.mark_dirty(); |
| }, |
| } |
| } |
| |
| /// Expand the selection to the current mouse cursor position. |
| #[inline] |
| fn expand_selection(&mut self) { |
| let selection_type = match self.mouse().click_state { |
| ClickState::Click => { |
| if self.modifiers().state().control_key() { |
| SelectionType::Block |
| } else { |
| SelectionType::Simple |
| } |
| }, |
| ClickState::DoubleClick => SelectionType::Semantic, |
| ClickState::TripleClick => SelectionType::Lines, |
| ClickState::None => return, |
| }; |
| |
| // Load mouse point, treating message bar and padding as the closest cell. |
| let display_offset = self.terminal().grid().display_offset(); |
| let point = self.mouse().point(&self.size_info(), display_offset); |
| |
| let cell_side = self.mouse().cell_side; |
| |
| let selection = match &mut self.terminal_mut().selection { |
| Some(selection) => selection, |
| None => return, |
| }; |
| |
| selection.ty = selection_type; |
| self.update_selection(point, cell_side); |
| |
| // Move vi mode cursor to mouse click position. |
| if self.terminal().mode().contains(TermMode::VI) && !self.search_active() { |
| self.terminal_mut().vi_mode_cursor.point = point; |
| } |
| } |
| |
| /// Handle beginning of terminal text input. |
| fn on_terminal_input_start(&mut self) { |
| self.on_typing_start(); |
| self.clear_selection(); |
| |
| if self.terminal().grid().display_offset() != 0 { |
| self.scroll(Scroll::Bottom); |
| } |
| } |
| |
| /// Paste a text into the terminal. |
| fn paste(&mut self, text: &str, bracketed: bool) { |
| if self.search_active() { |
| for c in text.chars() { |
| self.search_input(c); |
| } |
| } else if bracketed && self.terminal().mode().contains(TermMode::BRACKETED_PASTE) { |
| self.on_terminal_input_start(); |
| |
| self.write_to_pty(&b"\x1b[200~"[..]); |
| |
| // Write filtered escape sequences. |
| // |
| // We remove `\x1b` to ensure it's impossible for the pasted text to write the bracketed |
| // paste end escape `\x1b[201~` and `\x03` since some shells incorrectly terminate |
| // bracketed paste when they receive it. |
| let filtered = text.replace(['\x1b', '\x03'], ""); |
| self.write_to_pty(filtered.into_bytes()); |
| |
| self.write_to_pty(&b"\x1b[201~"[..]); |
| } else { |
| self.on_terminal_input_start(); |
| |
| let payload = if bracketed { |
| // 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). |
| text.replace("\r\n", "\r").replace('\n', "\r").into_bytes() |
| } else { |
| // When we explicitly disable bracketed paste don't manipulate with the input, |
| // so we pass user input as is. |
| text.to_owned().into_bytes() |
| }; |
| |
| self.write_to_pty(payload); |
| } |
| } |
| |
| /// Toggle the vi mode status. |
| #[inline] |
| fn toggle_vi_mode(&mut self) { |
| let was_in_vi_mode = self.terminal.mode().contains(TermMode::VI); |
| if was_in_vi_mode { |
| // If we had search running when leaving Vi mode we should mark terminal fully damaged |
| // to cleanup highlighted results. |
| if self.search_state.dfas.take().is_some() { |
| self.display.damage_tracker.frame().mark_fully_damaged(); |
| } |
| } else { |
| self.clear_selection(); |
| } |
| |
| if self.search_active() { |
| self.cancel_search(); |
| } |
| |
| // We don't want IME in Vi mode. |
| self.window().set_ime_allowed(was_in_vi_mode); |
| |
| self.terminal.toggle_vi_mode(); |
| |
| *self.dirty = true; |
| } |
| |
| /// Get vi inline search state. |
| fn inline_search_state(&mut self) -> &mut InlineSearchState { |
| self.inline_search_state |
| } |
| |
| /// Start vi mode inline search. |
| fn start_inline_search(&mut self, direction: Direction, stop_short: bool) { |
| self.inline_search_state.stop_short = stop_short; |
| self.inline_search_state.direction = direction; |
| self.inline_search_state.char_pending = true; |
| } |
| |
| /// Jump to the next matching character in the line. |
| fn inline_search_next(&mut self) { |
| let direction = self.inline_search_state.direction; |
| self.inline_search(direction); |
| } |
| |
| /// Jump to the next matching character in the line. |
| fn inline_search_previous(&mut self) { |
| let direction = self.inline_search_state.direction.opposite(); |
| self.inline_search(direction); |
| } |
| |
| fn message(&self) -> Option<&Message> { |
| self.message_buffer.message() |
| } |
| |
| fn config(&self) -> &UiConfig { |
| self.config |
| } |
| |
| #[cfg(target_os = "macos")] |
| fn event_loop(&self) -> &EventLoopWindowTarget<Event> { |
| self.event_loop |
| } |
| |
| fn clipboard_mut(&mut self) -> &mut Clipboard { |
| self.clipboard |
| } |
| |
| fn scheduler_mut(&mut self) -> &mut Scheduler { |
| self.scheduler |
| } |
| } |
| |
| impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { |
| fn update_search(&mut self) { |
| let regex = match self.search_state.regex() { |
| Some(regex) => regex, |
| None => return, |
| }; |
| |
| // Hide cursor while typing into the search bar. |
| if self.config.mouse.hide_when_typing { |
| self.display.window.set_mouse_visible(false); |
| } |
| |
| if regex.is_empty() { |
| // Stop search if there's nothing to search for. |
| self.search_reset_state(); |
| self.search_state.dfas = None; |
| } else { |
| // Create search dfas for the new regex string. |
| self.search_state.dfas = RegexSearch::new(regex).ok(); |
| |
| // Update search highlighting. |
| self.goto_match(MAX_SEARCH_WHILE_TYPING); |
| } |
| |
| *self.dirty = true; |
| } |
| |
| /// Reset terminal to the state before search was started. |
| fn search_reset_state(&mut self) { |
| // Unschedule pending timers. |
| let timer_id = TimerId::new(Topic::DelayedSearch, self.display.window.id()); |
| self.scheduler.unschedule(timer_id); |
| |
| // Clear focused match. |
| self.search_state.focused_match = None; |
| |
| // The viewport reset logic is only needed for vi mode, since without it our origin is |
| // always at the current display offset instead of at the vi cursor position which we need |
| // to recover to. |
| if !self.terminal.mode().contains(TermMode::VI) { |
| return; |
| } |
| |
| // Reset display offset and cursor position. |
| self.terminal.vi_mode_cursor.point = self.search_state.origin; |
| self.terminal.scroll_display(Scroll::Delta(self.search_state.display_offset_delta)); |
| self.search_state.display_offset_delta = 0; |
| |
| *self.dirty = true; |
| } |
| |
| /// Jump to the first regex match from the search origin. |
| fn goto_match(&mut self, mut limit: Option<usize>) { |
| let dfas = match &mut self.search_state.dfas { |
| Some(dfas) => dfas, |
| None => return, |
| }; |
| |
| // Limit search only when enough lines are available to run into the limit. |
| limit = limit.filter(|&limit| limit <= self.terminal.total_lines()); |
| |
| // Jump to the next match. |
| let direction = self.search_state.direction; |
| let clamped_origin = self.search_state.origin.grid_clamp(self.terminal, Boundary::Grid); |
| match self.terminal.search_next(dfas, clamped_origin, direction, Side::Left, limit) { |
| Some(regex_match) => { |
| let old_offset = self.terminal.grid().display_offset() as i32; |
| |
| if self.terminal.mode().contains(TermMode::VI) { |
| // Move vi cursor to the start of the match. |
| self.terminal.vi_goto_point(*regex_match.start()); |
| } else { |
| // Select the match when vi mode is not active. |
| self.terminal.scroll_to_point(*regex_match.start()); |
| } |
| |
| // Update the focused match. |
| self.search_state.focused_match = Some(regex_match); |
| |
| // Store number of lines the viewport had to be moved. |
| let display_offset = self.terminal.grid().display_offset(); |
| self.search_state.display_offset_delta += old_offset - display_offset as i32; |
| |
| // Since we found a result, we require no delayed re-search. |
| let timer_id = TimerId::new(Topic::DelayedSearch, self.display.window.id()); |
| self.scheduler.unschedule(timer_id); |
| }, |
| // Reset viewport only when we know there is no match, to prevent unnecessary jumping. |
| None if limit.is_none() => self.search_reset_state(), |
| None => { |
| // Schedule delayed search if we ran into our search limit. |
| let timer_id = TimerId::new(Topic::DelayedSearch, self.display.window.id()); |
| if !self.scheduler.scheduled(timer_id) { |
| let event = Event::new(EventType::SearchNext, self.display.window.id()); |
| self.scheduler.schedule(event, TYPING_SEARCH_DELAY, false, timer_id); |
| } |
| |
| // Clear focused match. |
| self.search_state.focused_match = None; |
| }, |
| } |
| |
| *self.dirty = true; |
| } |
| |
| /// Cleanup the search state. |
| fn exit_search(&mut self) { |
| let vi_mode = self.terminal.mode().contains(TermMode::VI); |
| self.window().set_ime_allowed(!vi_mode); |
| |
| self.display.damage_tracker.frame().mark_fully_damaged(); |
| self.display.pending_update.dirty = true; |
| self.search_state.history_index = None; |
| |
| // Clear focused match. |
| self.search_state.focused_match = None; |
| } |
| |
| /// Update the cursor blinking state. |
| fn update_cursor_blinking(&mut self) { |
| // Get config cursor style. |
| let mut cursor_style = self.config.cursor.style; |
| let vi_mode = self.terminal.mode().contains(TermMode::VI); |
| if vi_mode { |
| cursor_style = self.config.cursor.vi_mode_style.unwrap_or(cursor_style); |
| } |
| |
| // Check terminal cursor style. |
| let terminal_blinking = self.terminal.cursor_style().blinking; |
| let mut blinking = cursor_style.blinking_override().unwrap_or(terminal_blinking); |
| blinking &= (vi_mode || self.terminal().mode().contains(TermMode::SHOW_CURSOR)) |
| && self.display().ime.preedit().is_none(); |
| |
| // Update cursor blinking state. |
| let window_id = self.display.window.id(); |
| self.scheduler.unschedule(TimerId::new(Topic::BlinkCursor, window_id)); |
| self.scheduler.unschedule(TimerId::new(Topic::BlinkTimeout, window_id)); |
| |
| // Reset blinkinig timeout. |
| *self.cursor_blink_timed_out = false; |
| |
| if blinking && self.terminal.is_focused { |
| self.schedule_blinking(); |
| self.schedule_blinking_timeout(); |
| } else { |
| self.display.cursor_hidden = false; |
| *self.dirty = true; |
| } |
| } |
| |
| fn schedule_blinking(&mut self) { |
| let window_id = self.display.window.id(); |
| let timer_id = TimerId::new(Topic::BlinkCursor, window_id); |
| let event = Event::new(EventType::BlinkCursor, window_id); |
| let blinking_interval = Duration::from_millis(self.config.cursor.blink_interval()); |
| self.scheduler.schedule(event, blinking_interval, true, timer_id); |
| } |
| |
| fn schedule_blinking_timeout(&mut self) { |
| let blinking_timeout = self.config.cursor.blink_timeout(); |
| if blinking_timeout == Duration::ZERO { |
| return; |
| } |
| |
| let window_id = self.display.window.id(); |
| let event = Event::new(EventType::BlinkCursorTimeout, window_id); |
| let timer_id = TimerId::new(Topic::BlinkTimeout, window_id); |
| |
| self.scheduler.schedule(event, blinking_timeout, false, timer_id); |
| } |
| |
| /// Perform vi mode inline search in the specified direction. |
| fn inline_search(&mut self, direction: Direction) { |
| let c = match self.inline_search_state.character { |
| Some(c) => c, |
| None => return, |
| }; |
| let mut buf = [0; 4]; |
| let search_character = c.encode_utf8(&mut buf); |
| |
| // Find next match in this line. |
| let vi_point = self.terminal.vi_mode_cursor.point; |
| let point = match direction { |
| Direction::Right => self.terminal.inline_search_right(vi_point, search_character), |
| Direction::Left => self.terminal.inline_search_left(vi_point, search_character), |
| }; |
| |
| // Jump to point if there's a match. |
| if let Ok(mut point) = point { |
| if self.inline_search_state.stop_short { |
| let grid = self.terminal.grid(); |
| point = match direction { |
| Direction::Right => { |
| grid.iter_from(point).prev().map_or(point, |cell| cell.point) |
| }, |
| Direction::Left => { |
| grid.iter_from(point).next().map_or(point, |cell| cell.point) |
| }, |
| }; |
| } |
| |
| self.terminal.vi_goto_point(point); |
| self.mark_dirty(); |
| } |
| } |
| } |
| |
| /// Identified purpose of the touch input. |
| #[derive(Debug)] |
| pub enum TouchPurpose { |
| None, |
| Select(TouchEvent), |
| Scroll(TouchEvent), |
| Zoom(TouchZoom), |
| Tap(TouchEvent), |
| Invalid(HashSet<u64, RandomState>), |
| } |
| |
| impl Default for TouchPurpose { |
| fn default() -> Self { |
| Self::None |
| } |
| } |
| |
| /// Touch zooming state. |
| #[derive(Debug)] |
| pub struct TouchZoom { |
| slots: (TouchEvent, TouchEvent), |
| fractions: f32, |
| } |
| |
| impl TouchZoom { |
| pub fn new(slots: (TouchEvent, TouchEvent)) -> Self { |
| Self { slots, fractions: Default::default() } |
| } |
| |
| /// Get slot distance change since last update. |
| pub fn font_delta(&mut self, slot: TouchEvent) -> f32 { |
| let old_distance = self.distance(); |
| |
| // Update touch slots. |
| if slot.id == self.slots.0.id { |
| self.slots.0 = slot; |
| } else { |
| self.slots.1 = slot; |
| } |
| |
| // Calculate font change in `FONT_SIZE_STEP` increments. |
| let delta = (self.distance() - old_distance) * TOUCH_ZOOM_FACTOR + self.fractions; |
| let font_delta = (delta.abs() / FONT_SIZE_STEP).floor() * FONT_SIZE_STEP * delta.signum(); |
| self.fractions = delta - font_delta; |
| |
| font_delta |
| } |
| |
| /// Get active touch slots. |
| pub fn slots(&self) -> HashSet<u64, RandomState> { |
| let mut set = HashSet::default(); |
| set.insert(self.slots.0.id); |
| set.insert(self.slots.1.id); |
| set |
| } |
| |
| /// Calculate distance between slots. |
| fn distance(&self) -> f32 { |
| let delta_x = self.slots.0.location.x - self.slots.1.location.x; |
| let delta_y = self.slots.0.location.y - self.slots.1.location.y; |
| delta_x.hypot(delta_y) as f32 |
| } |
| } |
| |
| /// State of the mouse. |
| #[derive(Debug)] |
| pub struct Mouse { |
| pub left_button_state: ElementState, |
| pub middle_button_state: ElementState, |
| pub right_button_state: ElementState, |
| pub last_click_timestamp: Instant, |
| pub last_click_button: MouseButton, |
| pub click_state: ClickState, |
| pub accumulated_scroll: AccumulatedScroll, |
| pub cell_side: Side, |
| pub block_hint_launcher: bool, |
| pub hint_highlight_dirty: bool, |
| pub inside_text_area: bool, |
| pub x: usize, |
| pub y: usize, |
| } |
| |
| impl Default for Mouse { |
| fn default() -> Mouse { |
| Mouse { |
| last_click_timestamp: Instant::now(), |
| last_click_button: MouseButton::Left, |
| left_button_state: ElementState::Released, |
| middle_button_state: ElementState::Released, |
| right_button_state: ElementState::Released, |
| click_state: ClickState::None, |
| cell_side: Side::Left, |
| hint_highlight_dirty: Default::default(), |
| block_hint_launcher: Default::default(), |
| inside_text_area: Default::default(), |
| accumulated_scroll: Default::default(), |
| x: Default::default(), |
| y: Default::default(), |
| } |
| } |
| } |
| |
| impl Mouse { |
| /// Convert mouse pixel coordinates to viewport point. |
| /// |
| /// If the coordinates are outside of the terminal grid, like positions inside the padding, the |
| /// coordinates will be clamped to the closest grid coordinates. |
| #[inline] |
| pub fn point(&self, size: &SizeInfo, display_offset: usize) -> Point { |
| let col = self.x.saturating_sub(size.padding_x() as usize) / (size.cell_width() as usize); |
| let col = min(Column(col), size.last_column()); |
| |
| let line = self.y.saturating_sub(size.padding_y() as usize) / (size.cell_height() as usize); |
| let line = min(line, size.bottommost_line().0 as usize); |
| |
| term::viewport_to_point(display_offset, Point::new(line, col)) |
| } |
| } |
| |
| #[derive(Debug, Eq, PartialEq)] |
| pub enum ClickState { |
| None, |
| Click, |
| DoubleClick, |
| TripleClick, |
| } |
| |
| /// The amount of scroll accumulated from the pointer events. |
| #[derive(Default, Debug)] |
| pub struct AccumulatedScroll { |
| /// Scroll we should perform along `x` axis. |
| pub x: f64, |
| |
| /// Scroll we should perform along `y` axis. |
| pub y: f64, |
| } |
| |
| impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> { |
| /// Handle events from winit. |
| pub fn handle_event(&mut self, event: WinitEvent<Event>) { |
| match event { |
| WinitEvent::UserEvent(Event { payload, .. }) => match payload { |
| EventType::SearchNext => self.ctx.goto_match(None), |
| EventType::Scroll(scroll) => self.ctx.scroll(scroll), |
| EventType::BlinkCursor => { |
| // Only change state when timeout isn't reached, since we could get |
| // BlinkCursor and BlinkCursorTimeout events at the same time. |
| if !*self.ctx.cursor_blink_timed_out { |
| self.ctx.display.cursor_hidden ^= true; |
| *self.ctx.dirty = true; |
| } |
| }, |
| EventType::BlinkCursorTimeout => { |
| // Disable blinking after timeout reached. |
| let timer_id = TimerId::new(Topic::BlinkCursor, self.ctx.display.window.id()); |
| self.ctx.scheduler.unschedule(timer_id); |
| *self.ctx.cursor_blink_timed_out = true; |
| self.ctx.display.cursor_hidden = false; |
| *self.ctx.dirty = true; |
| }, |
| // Add message only if it's not already queued. |
| EventType::Message(message) if !self.ctx.message_buffer.is_queued(&message) => { |
| self.ctx.message_buffer.push(message); |
| self.ctx.display.pending_update.dirty = true; |
| }, |
| EventType::Terminal(event) => match event { |
| TerminalEvent::Title(title) => { |
| if !self.ctx.preserve_title && self.ctx.config.window.dynamic_title { |
| self.ctx.window().set_title(title); |
| } |
| }, |
| TerminalEvent::ResetTitle => { |
| let window_config = &self.ctx.config.window; |
| if !self.ctx.preserve_title && window_config.dynamic_title { |
| self.ctx.display.window.set_title(window_config.identity.title.clone()); |
| } |
| }, |
| TerminalEvent::Bell => { |
| // Set window urgency hint when window is not focused. |
| let focused = self.ctx.terminal.is_focused; |
| if !focused && self.ctx.terminal.mode().contains(TermMode::URGENCY_HINTS) { |
| self.ctx.window().set_urgent(true); |
| } |
| |
| // Ring visual bell. |
| self.ctx.display.visual_bell.ring(); |
| |
| // Execute bell command. |
| if let Some(bell_command) = &self.ctx.config.bell.command { |
| self.ctx.spawn_daemon(bell_command.program(), bell_command.args()); |
| } |
| }, |
| TerminalEvent::ClipboardStore(clipboard_type, content) => { |
| if self.ctx.terminal.is_focused { |
| self.ctx.clipboard.store(clipboard_type, content); |
| } |
| }, |
| TerminalEvent::ClipboardLoad(clipboard_type, format) => { |
| if self.ctx.terminal.is_focused { |
| let text = format(self.ctx.clipboard.load(clipboard_type).as_str()); |
| self.ctx.write_to_pty(text.into_bytes()); |
| } |
| }, |
| TerminalEvent::ColorRequest(index, format) => { |
| let color = self.ctx.terminal().colors()[index] |
| .map(Rgb) |
| .unwrap_or(self.ctx.display.colors[index]); |
| self.ctx.write_to_pty(format(color.0).into_bytes()); |
| }, |
| TerminalEvent::TextAreaSizeRequest(format) => { |
| let text = format(self.ctx.size_info().into()); |
| self.ctx.write_to_pty(text.into_bytes()); |
| }, |
| TerminalEvent::PtyWrite(text) => self.ctx.write_to_pty(text.into_bytes()), |
| TerminalEvent::MouseCursorDirty => self.reset_mouse_cursor(), |
| TerminalEvent::CursorBlinkingChange => self.ctx.update_cursor_blinking(), |
| TerminalEvent::Exit | TerminalEvent::ChildExit(_) | TerminalEvent::Wakeup => (), |
| }, |
| #[cfg(unix)] |
| EventType::IpcConfig(_) => (), |
| EventType::Message(_) |
| | EventType::ConfigReload(_) |
| | EventType::CreateWindow(_) |
| | EventType::Frame => (), |
| }, |
| WinitEvent::WindowEvent { event, .. } => { |
| match event { |
| WindowEvent::CloseRequested => self.ctx.terminal.exit(), |
| WindowEvent::ScaleFactorChanged { scale_factor, .. } => { |
| let old_scale_factor = |
| mem::replace(&mut self.ctx.window().scale_factor, scale_factor); |
| |
| let display_update_pending = &mut self.ctx.display.pending_update; |
| |
| // Rescale font size for the new factor. |
| let font_scale = scale_factor as f32 / old_scale_factor as f32; |
| self.ctx.display.font_size = self.ctx.display.font_size.scale(font_scale); |
| |
| let font = self.ctx.config.font.clone(); |
| display_update_pending.set_font(font.with_size(self.ctx.display.font_size)); |
| }, |
| WindowEvent::Resized(size) => { |
| // Ignore resize events to zero in any dimension, to avoid issues with Winit |
| // and the ConPTY. A 0x0 resize will also occur when the window is minimized |
| // on Windows. |
| if size.width == 0 || size.height == 0 { |
| return; |
| } |
| |
| self.ctx.display.pending_update.set_dimensions(size); |
| }, |
| WindowEvent::KeyboardInput { event, is_synthetic: false, .. } => { |
| self.key_input(event); |
| }, |
| WindowEvent::ModifiersChanged(modifiers) => self.modifiers_input(modifiers), |
| WindowEvent::MouseInput { state, button, .. } => { |
| self.ctx.window().set_mouse_visible(true); |
| self.mouse_input(state, button); |
| }, |
| WindowEvent::CursorMoved { position, .. } => { |
| self.ctx.window().set_mouse_visible(true); |
| self.mouse_moved(position); |
| }, |
| WindowEvent::MouseWheel { delta, phase, .. } => { |
| self.ctx.window().set_mouse_visible(true); |
| self.mouse_wheel_input(delta, phase); |
| }, |
| WindowEvent::Touch(touch) => self.touch(touch), |
| WindowEvent::Focused(is_focused) => { |
| self.ctx.terminal.is_focused = is_focused; |
| |
| // When the unfocused hollow is used we must redraw on focus change. |
| if self.ctx.config.cursor.unfocused_hollow { |
| *self.ctx.dirty = true; |
| } |
| |
| // Reset the urgency hint when gaining focus. |
| if is_focused { |
| self.ctx.window().set_urgent(false); |
| } |
| |
| self.ctx.update_cursor_blinking(); |
| self.on_focus_change(is_focused); |
| }, |
| WindowEvent::Occluded(occluded) => { |
| *self.ctx.occluded = occluded; |
| }, |
| WindowEvent::DroppedFile(path) => { |
| let path: String = path.to_string_lossy().into(); |
| self.ctx.paste(&(path + " "), true); |
| }, |
| WindowEvent::CursorLeft { .. } => { |
| self.ctx.mouse.inside_text_area = false; |
| |
| if self.ctx.display().highlighted_hint.is_some() { |
| *self.ctx.dirty = true; |
| } |
| }, |
| WindowEvent::Ime(ime) => match ime { |
| Ime::Commit(text) => { |
| *self.ctx.dirty = true; |
| // Don't use bracketed paste for single char input. |
| self.ctx.paste(&text, text.chars().count() > 1); |
| self.ctx.update_cursor_blinking(); |
| }, |
| Ime::Preedit(text, cursor_offset) => { |
| let preedit = if text.is_empty() { |
| None |
| } else { |
| Some(Preedit::new(text, cursor_offset.map(|offset| offset.0))) |
| }; |
| |
| if self.ctx.display.ime.preedit() != preedit.as_ref() { |
| self.ctx.display.ime.set_preedit(preedit); |
| self.ctx.update_cursor_blinking(); |
| *self.ctx.dirty = true; |
| } |
| }, |
| Ime::Enabled => { |
| self.ctx.display.ime.set_enabled(true); |
| *self.ctx.dirty = true; |
| }, |
| Ime::Disabled => { |
| self.ctx.display.ime.set_enabled(false); |
| *self.ctx.dirty = true; |
| }, |
| }, |
| WindowEvent::KeyboardInput { is_synthetic: true, .. } |
| | WindowEvent::ActivationTokenDone { .. } |
| | WindowEvent::TouchpadPressure { .. } |
| | WindowEvent::TouchpadMagnify { .. } |
| | WindowEvent::TouchpadRotate { .. } |
| | WindowEvent::SmartMagnify { .. } |
| | WindowEvent::CursorEntered { .. } |
| | WindowEvent::AxisMotion { .. } |
| | WindowEvent::HoveredFileCancelled |
| | WindowEvent::Destroyed |
| | WindowEvent::ThemeChanged(_) |
| | WindowEvent::HoveredFile(_) |
| | WindowEvent::RedrawRequested |
| | WindowEvent::Moved(_) => (), |
| } |
| }, |
| WinitEvent::Suspended { .. } |
| | WinitEvent::NewEvents { .. } |
| | WinitEvent::DeviceEvent { .. } |
| | WinitEvent::LoopExiting |
| | WinitEvent::Resumed |
| | WinitEvent::MemoryWarning |
| | WinitEvent::AboutToWait => (), |
| } |
| } |
| } |
| |
| /// The event processor. |
| /// |
| /// Stores some state from received events and dispatches actions when they are |
| /// triggered. |
| pub struct Processor { |
| windows: HashMap<WindowId, WindowContext, RandomState>, |
| gl_display: Option<GlutinDisplay>, |
| #[cfg(unix)] |
| global_ipc_options: ParsedOptions, |
| cli_options: CliOptions, |
| config: Rc<UiConfig>, |
| } |
| |
| impl Processor { |
| /// 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( |
| config: UiConfig, |
| cli_options: CliOptions, |
| _event_loop: &EventLoop<Event>, |
| ) -> Processor { |
| Processor { |
| cli_options, |
| gl_display: None, |
| config: Rc::new(config), |
| windows: Default::default(), |
| #[cfg(unix)] |
| global_ipc_options: Default::default(), |
| } |
| } |
| |
| /// Create initial window and load GL platform. |
| /// |
| /// This will initialize the OpenGL Api and pick a config that |
| /// will be used for the rest of the windows. |
| pub fn create_initial_window( |
| &mut self, |
| event_loop: &EventLoopWindowTarget<Event>, |
| proxy: EventLoopProxy<Event>, |
| options: WindowOptions, |
| ) -> Result<(), Box<dyn Error>> { |
| let window_context = |
| WindowContext::initial(event_loop, proxy, self.config.clone(), options)?; |
| |
| self.gl_display = Some(window_context.display.gl_context().display()); |
| self.windows.insert(window_context.id(), window_context); |
| |
| Ok(()) |
| } |
| |
| /// Create a new terminal window. |
| pub fn create_window( |
| &mut self, |
| event_loop: &EventLoopWindowTarget<Event>, |
| proxy: EventLoopProxy<Event>, |
| options: WindowOptions, |
| ) -> Result<(), Box<dyn Error>> { |
| let window = self.windows.iter().next().as_ref().unwrap().1; |
| |
| // Overide config with CLI/IPC options. |
| let mut config_overrides = options.config_overrides(); |
| #[cfg(unix)] |
| config_overrides.extend_from_slice(&self.global_ipc_options); |
| let mut config = self.config.clone(); |
| config = config_overrides.override_config_rc(config); |
| |
| #[allow(unused_mut)] |
| let mut window_context = |
| window.additional(event_loop, proxy, config, options, config_overrides)?; |
| |
| self.windows.insert(window_context.id(), window_context); |
| Ok(()) |
| } |
| |
| /// Run the event loop. |
| /// |
| /// The result is exit code generate from the loop. |
| pub fn run( |
| &mut self, |
| event_loop: EventLoop<Event>, |
| initial_window_options: WindowOptions, |
| ) -> Result<(), Box<dyn Error>> { |
| let proxy = event_loop.create_proxy(); |
| let mut scheduler = Scheduler::new(proxy.clone()); |
| let mut initial_window_options = Some(initial_window_options); |
| |
| // Disable all device events, since we don't care about them. |
| event_loop.listen_device_events(DeviceEvents::Never); |
| |
| let mut initial_window_error = Ok(()); |
| let initial_window_error_loop = &mut initial_window_error; |
| // SAFETY: Since this takes a pointer to the winit event loop, it MUST be dropped first, |
| // which is done by `move` into event loop. |
| let mut clipboard = unsafe { Clipboard::new(event_loop.raw_display_handle()) }; |
| let result = event_loop.run(move |event, event_loop| { |
| if self.config.debug.print_events { |
| info!("winit event: {:?}", event); |
| } |
| |
| // Ignore all events we do not care about. |
| if Self::skip_event(&event) { |
| return; |
| } |
| |
| match event { |
| // The event loop just got initialized. Create a window. |
| WinitEvent::Resumed => { |
| // Creating window inside event loop is required for platforms like macOS to |
| // properly initialize state, like tab management. Otherwise the first |
| // window won't handle tabs. |
| let initial_window_options = match initial_window_options.take() { |
| Some(initial_window_options) => initial_window_options, |
| None => return, |
| }; |
| |
| if let Err(err) = self.create_initial_window( |
| event_loop, |
| proxy.clone(), |
| initial_window_options, |
| ) { |
| *initial_window_error_loop = Err(err); |
| event_loop.exit(); |
| return; |
| } |
| |
| info!("Initialisation complete"); |
| }, |
| WinitEvent::LoopExiting => { |
| match self.gl_display.take() { |
| #[cfg(not(target_os = "macos"))] |
| Some(glutin::display::Display::Egl(display)) => { |
| // Ensure that all the windows are dropped, so the destructors for |
| // Renderer and contexts ran. |
| self.windows.clear(); |
| |
| // SAFETY: the display is being destroyed after destroying all the |
| // windows, thus no attempt to access the EGL state will be made. |
| unsafe { |
| display.terminate(); |
| } |
| }, |
| _ => (), |
| } |
| }, |
| // NOTE: This event bypasses batching to minimize input latency. |
| WinitEvent::UserEvent(Event { |
| window_id: Some(window_id), |
| payload: EventType::Terminal(TerminalEvent::Wakeup), |
| }) => { |
| if let Some(window_context) = self.windows.get_mut(&window_id) { |
| window_context.dirty = true; |
| if window_context.display.window.has_frame { |
| window_context.display.window.request_redraw(); |
| } |
| } |
| }, |
| // NOTE: This event bypasses batching to minimize input latency. |
| WinitEvent::UserEvent(Event { |
| window_id: Some(window_id), |
| payload: EventType::Frame, |
| }) => { |
| if let Some(window_context) = self.windows.get_mut(&window_id) { |
| window_context.display.window.has_frame = true; |
| if window_context.dirty { |
| window_context.display.window.request_redraw(); |
| } |
| } |
| }, |
| // Check for shutdown. |
| WinitEvent::UserEvent(Event { |
| window_id: Some(window_id), |
| payload: EventType::Terminal(TerminalEvent::Exit), |
| }) => { |
| // Remove the closed terminal. |
| let window_context = match self.windows.remove(&window_id) { |
| Some(window_context) => window_context, |
| None => return, |
| }; |
| |
| // Unschedule pending events. |
| scheduler.unschedule_window(window_context.id()); |
| |
| // Shutdown if no more terminals are open. |
| if self.windows.is_empty() { |
| // Write ref tests of last window to disk. |
| if self.config.debug.ref_test { |
| window_context.write_ref_test_results(); |
| } |
| |
| event_loop.exit(); |
| } |
| }, |
| WinitEvent::WindowEvent { window_id, event: WindowEvent::RedrawRequested } => { |
| let window_context = match self.windows.get_mut(&window_id) { |
| Some(window_context) => window_context, |
| None => return, |
| }; |
| |
| window_context.handle_event( |
| #[cfg(target_os = "macos")] |
| event_loop, |
| &proxy, |
| &mut clipboard, |
| &mut scheduler, |
| event, |
| ); |
| |
| window_context.draw(&mut scheduler); |
| }, |
| // Process all pending events. |
| WinitEvent::AboutToWait => { |
| // Dispatch event to all windows. |
| for window_context in self.windows.values_mut() { |
| window_context.handle_event( |
| #[cfg(target_os = "macos")] |
| event_loop, |
| &proxy, |
| &mut clipboard, |
| &mut scheduler, |
| WinitEvent::AboutToWait, |
| ); |
| } |
| |
| // Update the scheduler after event processing to ensure |
| // the event loop deadline is as accurate as possible. |
| let control_flow = match scheduler.update() { |
| Some(instant) => ControlFlow::WaitUntil(instant), |
| None => ControlFlow::Wait, |
| }; |
| event_loop.set_control_flow(control_flow); |
| }, |
| // Process config update. |
| WinitEvent::UserEvent(Event { payload: EventType::ConfigReload(path), .. }) => { |
| // Clear config logs from message bar for all terminals. |
| for window_context in self.windows.values_mut() { |
| if !window_context.message_buffer.is_empty() { |
| window_context.message_buffer.remove_target(LOG_TARGET_CONFIG); |
| window_context.display.pending_update.dirty = true; |
| } |
| } |
| |
| // Load config and update each terminal. |
| if let Ok(config) = config::reload(&path, &mut self.cli_options) { |
| self.config = Rc::new(config); |
| |
| for window_context in self.windows.values_mut() { |
| window_context.update_config(self.config.clone()); |
| } |
| } |
| }, |
| // Process IPC config update. |
| #[cfg(unix)] |
| WinitEvent::UserEvent(Event { |
| payload: EventType::IpcConfig(ipc_config), |
| window_id, |
| }) => { |
| // Try and parse options as toml. |
| let mut options = ParsedOptions::from_options(&ipc_config.options); |
| |
| // Override IPC config for each window with matching ID. |
| for (_, window_context) in self |
| .windows |
| .iter_mut() |
| .filter(|(id, _)| window_id.is_none() || window_id == Some(**id)) |
| { |
| if ipc_config.reset { |
| window_context.reset_window_config(self.config.clone()); |
| } else { |
| window_context.add_window_config(self.config.clone(), &options); |
| } |
| } |
| |
| // Persist global options for future windows. |
| if window_id.is_none() { |
| if ipc_config.reset { |
| self.global_ipc_options.clear(); |
| } else { |
| self.global_ipc_options.append(&mut options); |
| } |
| } |
| }, |
| // Create a new terminal window. |
| WinitEvent::UserEvent(Event { |
| payload: EventType::CreateWindow(options), .. |
| }) => { |
| // XXX Ensure that no context is current when creating a new window, |
| // otherwise it may lock the backing buffer of the |
| // surface of current context when asking |
| // e.g. EGL on Wayland to create a new context. |
| for window_context in self.windows.values_mut() { |
| window_context.display.make_not_current(); |
| } |
| |
| if let Err(err) = self.create_window(event_loop, proxy.clone(), options) { |
| error!("Could not open window: {:?}", err); |
| } |
| }, |
| // Process events affecting all windows. |
| WinitEvent::UserEvent(event @ Event { window_id: None, .. }) => { |
| for window_context in self.windows.values_mut() { |
| window_context.handle_event( |
| #[cfg(target_os = "macos")] |
| event_loop, |
| &proxy, |
| &mut clipboard, |
| &mut scheduler, |
| event.clone().into(), |
| ); |
| } |
| }, |
| // Process window-specific events. |
| WinitEvent::WindowEvent { window_id, .. } |
| | WinitEvent::UserEvent(Event { window_id: Some(window_id), .. }) => { |
| if let Some(window_context) = self.windows.get_mut(&window_id) { |
| window_context.handle_event( |
| #[cfg(target_os = "macos")] |
| event_loop, |
| &proxy, |
| &mut clipboard, |
| &mut scheduler, |
| event, |
| ); |
| } |
| }, |
| _ => (), |
| } |
| }); |
| |
| if initial_window_error.is_err() { |
| initial_window_error |
| } else { |
| result.map_err(Into::into) |
| } |
| } |
| |
| /// Check if an event is irrelevant and can be skipped. |
| fn skip_event(event: &WinitEvent<Event>) -> bool { |
| match event { |
| WinitEvent::NewEvents(StartCause::Init) => false, |
| WinitEvent::WindowEvent { event, .. } => matches!( |
| event, |
| WindowEvent::KeyboardInput { is_synthetic: true, .. } |
| | WindowEvent::TouchpadPressure { .. } |
| | WindowEvent::CursorEntered { .. } |
| | WindowEvent::AxisMotion { .. } |
| | WindowEvent::HoveredFileCancelled |
| | WindowEvent::Destroyed |
| | WindowEvent::HoveredFile(_) |
| | WindowEvent::Moved(_) |
| ), |
| WinitEvent::Suspended { .. } | WinitEvent::NewEvents { .. } => true, |
| _ => false, |
| } |
| } |
| } |
| |
| #[derive(Debug, Clone)] |
| pub struct EventProxy { |
| proxy: EventLoopProxy<Event>, |
| window_id: WindowId, |
| } |
| |
| impl EventProxy { |
| pub fn new(proxy: EventLoopProxy<Event>, window_id: WindowId) -> Self { |
| Self { proxy, window_id } |
| } |
| |
| /// Send an event to the event loop. |
| pub fn send_event(&self, event: EventType) { |
| let _ = self.proxy.send_event(Event::new(event, self.window_id)); |
| } |
| } |
| |
| impl EventListener for EventProxy { |
| fn send_event(&self, event: TerminalEvent) { |
| let _ = self.proxy.send_event(Event::new(event.into(), self.window_id)); |
| } |
| } |