aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src/event.rs
diff options
context:
space:
mode:
Diffstat (limited to 'alacritty/src/event.rs')
-rw-r--r--alacritty/src/event.rs350
1 files changed, 302 insertions, 48 deletions
diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs
index 630e8ef0..edbc4086 100644
--- a/alacritty/src/event.rs
+++ b/alacritty/src/event.rs
@@ -1,7 +1,7 @@
//! Process window events.
use std::borrow::Cow;
-use std::cmp::max;
+use std::cmp::{max, min};
use std::env;
#[cfg(unix)]
use std::fs;
@@ -12,7 +12,7 @@ use std::path::PathBuf;
#[cfg(not(any(target_os = "macos", windows)))]
use std::sync::atomic::Ordering;
use std::sync::Arc;
-use std::time::Instant;
+use std::time::{Duration, Instant};
use glutin::dpi::PhysicalSize;
use glutin::event::{ElementState, Event as GlutinEvent, ModifiersState, MouseButton, WindowEvent};
@@ -27,12 +27,10 @@ use serde_json as json;
use font::set_font_smoothing;
use font::{self, Size};
-use alacritty_terminal::config::Font;
use alacritty_terminal::config::LOG_TARGET_CONFIG;
-use alacritty_terminal::event::OnResize;
-use alacritty_terminal::event::{Event as TerminalEvent, EventListener, Notify};
-use alacritty_terminal::grid::Scroll;
-use alacritty_terminal::index::{Column, Line, Point, Side};
+use alacritty_terminal::event::{Event as TerminalEvent, EventListener, Notify, OnResize};
+use alacritty_terminal::grid::{Dimensions, Scroll};
+use alacritty_terminal::index::{Column, Direction, Line, Point, Side};
use alacritty_terminal::message_bar::{Message, MessageBuffer};
use alacritty_terminal::selection::{Selection, SelectionType};
use alacritty_terminal::sync::FairMutex;
@@ -46,12 +44,18 @@ use crate::cli::Options;
use crate::clipboard::Clipboard;
use crate::config;
use crate::config::Config;
-use crate::display::Display;
+use crate::display::{Display, DisplayUpdate};
use crate::input::{self, ActionContext as _, FONT_SIZE_STEP};
-use crate::scheduler::Scheduler;
+use crate::scheduler::{Scheduler, TimerId};
use crate::url::{Url, Urls};
use crate::window::Window;
+/// 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);
+
/// Events dispatched through the UI event loop.
#[derive(Debug, Clone)]
pub enum Event {
@@ -60,6 +64,7 @@ pub enum Event {
Scroll(Scroll),
ConfigReload(PathBuf),
Message(Message),
+ SearchNext,
}
impl From<Event> for GlutinEvent<'_, Event> {
@@ -74,17 +79,35 @@ impl From<TerminalEvent> for Event {
}
}
-#[derive(Default, Clone, Debug, PartialEq)]
-pub struct DisplayUpdate {
- pub dimensions: Option<PhysicalSize<u32>>,
- pub message_buffer: bool,
- pub font: Option<Font>,
- pub cursor: bool,
+/// Regex search state.
+pub struct SearchState {
+ /// Search string regex.
+ regex: Option<String>,
+
+ /// Search direction.
+ direction: Direction,
+
+ /// Change in display offset since the beginning of the search.
+ display_offset_delta: isize,
+
+ /// Vi cursor position before search.
+ vi_cursor_point: Point,
}
-impl DisplayUpdate {
- fn is_empty(&self) -> bool {
- self.dimensions.is_none() && self.font.is_none() && !self.message_buffer && !self.cursor
+impl SearchState {
+ fn new() -> Self {
+ Self::default()
+ }
+}
+
+impl Default for SearchState {
+ fn default() -> Self {
+ Self {
+ direction: Direction::Right,
+ display_offset_delta: 0,
+ vi_cursor_point: Point::default(),
+ regex: None,
+ }
}
}
@@ -104,6 +127,7 @@ pub struct ActionContext<'a, N, T> {
pub event_loop: &'a EventLoopWindowTarget<Event>,
pub urls: &'a Urls,
pub scheduler: &'a mut Scheduler,
+ pub search_state: &'a mut SearchState,
font_size: &'a mut Size,
}
@@ -294,19 +318,148 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
fn change_font_size(&mut self, delta: f32) {
*self.font_size = max(*self.font_size + delta, Size::new(FONT_SIZE_STEP));
let font = self.config.font.clone().with_size(*self.font_size);
- self.display_update_pending.font = Some(font);
+ self.display_update_pending.set_font(font);
self.terminal.dirty = true;
}
fn reset_font_size(&mut self) {
*self.font_size = self.config.font.size;
- self.display_update_pending.font = Some(self.config.font.clone());
+ self.display_update_pending.set_font(self.config.font.clone());
self.terminal.dirty = true;
}
+ #[inline]
fn pop_message(&mut self) {
- self.display_update_pending.message_buffer = true;
- self.message_buffer.pop();
+ if !self.message_buffer.is_empty() {
+ self.display_update_pending.dirty = true;
+ self.message_buffer.pop();
+ }
+ }
+
+ #[inline]
+ fn start_search(&mut self, direction: Direction) {
+ let num_lines = self.terminal.screen_lines();
+ let num_cols = self.terminal.cols();
+
+ self.search_state.regex = Some(String::new());
+ self.search_state.direction = direction;
+
+ // Store original vi cursor position as search origin and for resetting.
+ self.search_state.vi_cursor_point = if self.terminal.mode().contains(TermMode::VI) {
+ self.terminal.vi_mode_cursor.point
+ } else {
+ match direction {
+ Direction::Right => Point::new(Line(0), Column(0)),
+ Direction::Left => Point::new(num_lines - 2, num_cols - 1),
+ }
+ };
+
+ self.display_update_pending.dirty = true;
+ self.terminal.dirty = true;
+ }
+
+ #[inline]
+ fn confirm_search(&mut self) {
+ // Enter vi mode once search is confirmed.
+ self.terminal.set_vi_mode();
+
+ // Force unlimited search if the previous one was interrupted.
+ if self.scheduler.scheduled(TimerId::DelayedSearch) {
+ self.goto_match(None);
+ }
+
+ // Move vi cursor down if resize will pull content from history.
+ if self.terminal.history_size() != 0 && self.terminal.grid().display_offset() == 0 {
+ self.terminal.vi_mode_cursor.point.line += 1;
+ }
+
+ // Clear reset state.
+ self.search_state.display_offset_delta = 0;
+
+ self.display_update_pending.dirty = true;
+ self.search_state.regex = None;
+ self.terminal.dirty = true;
+ }
+
+ #[inline]
+ fn cancel_search(&mut self) {
+ self.terminal.cancel_search();
+
+ // Recover pre-search state.
+ self.search_reset_state();
+
+ // Move vi cursor down if resize will pull from history.
+ if self.terminal.history_size() != 0 && self.terminal.grid().display_offset() == 0 {
+ self.terminal.vi_mode_cursor.point.line += 1;
+ }
+
+ self.display_update_pending.dirty = true;
+ self.search_state.regex = None;
+ self.terminal.dirty = true;
+ }
+
+ #[inline]
+ fn push_search(&mut self, c: char) {
+ let regex = match self.search_state.regex.as_mut() {
+ Some(regex) => regex,
+ None => return,
+ };
+
+ // Hide cursor while typing into the search bar.
+ if self.config.ui_config.mouse.hide_when_typing {
+ self.window.set_mouse_visible(false);
+ }
+
+ // Add new char to search string.
+ regex.push(c);
+
+ // Create terminal search from the new regex string.
+ self.terminal.start_search(&regex);
+
+ // Update search highlighting.
+ self.goto_match(MAX_SEARCH_WHILE_TYPING);
+
+ self.terminal.dirty = true;
+ }
+
+ #[inline]
+ fn pop_search(&mut self) {
+ let regex = match self.search_state.regex.as_mut() {
+ Some(regex) => regex,
+ None => return,
+ };
+
+ // Hide cursor while typing into the search bar.
+ if self.config.ui_config.mouse.hide_when_typing {
+ self.window.set_mouse_visible(false);
+ }
+
+ // Remove last char from search string.
+ regex.pop();
+
+ if regex.is_empty() {
+ // Stop search if there's nothing to search for.
+ self.search_reset_state();
+ self.terminal.cancel_search();
+ } else {
+ // Create terminal search from the new regex string.
+ self.terminal.start_search(&regex);
+
+ // Update search highlighting.
+ self.goto_match(MAX_SEARCH_WHILE_TYPING);
+ }
+
+ self.terminal.dirty = true;
+ }
+
+ #[inline]
+ fn search_direction(&self) -> Direction {
+ self.search_state.direction
+ }
+
+ #[inline]
+ fn search_active(&self) -> bool {
+ self.search_state.regex.is_some()
}
fn message(&self) -> Option<&Message> {
@@ -334,6 +487,73 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
}
}
+impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
+ /// Reset terminal to the state before search was started.
+ fn search_reset_state(&mut self) {
+ // Reset display offset.
+ self.terminal.scroll_display(Scroll::Delta(self.search_state.display_offset_delta));
+ self.search_state.display_offset_delta = 0;
+
+ // Reset vi mode cursor.
+ let mut vi_cursor_point = self.search_state.vi_cursor_point;
+ vi_cursor_point.line = min(vi_cursor_point.line, self.terminal.screen_lines() - 1);
+ vi_cursor_point.col = min(vi_cursor_point.col, self.terminal.cols() - 1);
+ self.terminal.vi_mode_cursor.point = vi_cursor_point;
+
+ // Unschedule pending timers.
+ self.scheduler.unschedule(TimerId::DelayedSearch);
+ }
+
+ /// Jump to the first regex match from the search origin.
+ fn goto_match(&mut self, mut limit: Option<usize>) {
+ let regex = match self.search_state.regex.take() {
+ Some(regex) => regex,
+ None => return,
+ };
+
+ // Limit search only when enough lines are available to run into the limit.
+ limit = limit.filter(|&limit| limit <= self.terminal.total_lines());
+
+ // Use original position as search origin.
+ let mut vi_cursor_point = self.search_state.vi_cursor_point;
+ vi_cursor_point.line = min(vi_cursor_point.line, self.terminal.screen_lines() - 1);
+ let mut origin = self.terminal.visible_to_buffer(vi_cursor_point);
+ origin.line = (origin.line as isize + self.search_state.display_offset_delta) as usize;
+
+ // Jump to the next match.
+ let direction = self.search_state.direction;
+ match self.terminal.search_next(origin, direction, Side::Left, limit) {
+ Some(regex_match) => {
+ let old_offset = self.terminal.grid().display_offset() as isize;
+
+ self.terminal.vi_goto_point(*regex_match.start());
+
+ // 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 isize;
+
+ // Since we found a result, we require no delayed re-search.
+ self.scheduler.unschedule(TimerId::DelayedSearch);
+ },
+ // 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.
+ if !self.scheduler.scheduled(TimerId::DelayedSearch) {
+ self.scheduler.schedule(
+ Event::SearchNext.into(),
+ TYPING_SEARCH_DELAY,
+ false,
+ TimerId::DelayedSearch,
+ );
+ }
+ },
+ }
+
+ self.search_state.regex = Some(regex);
+ }
+}
+
#[derive(Debug, Eq, PartialEq)]
pub enum ClickState {
None,
@@ -400,6 +620,7 @@ pub struct Processor<N> {
display: Display,
font_size: Size,
event_queue: Vec<GlutinEvent<'static, Event>>,
+ search_state: SearchState,
}
impl<N: Notify + OnResize> Processor<N> {
@@ -429,6 +650,7 @@ impl<N: Notify + OnResize> Processor<N> {
display,
event_queue: Vec::new(),
clipboard,
+ search_state: SearchState::new(),
}
}
@@ -513,6 +735,7 @@ impl<N: Notify + OnResize> Processor<N> {
let mut terminal = terminal.lock();
let mut display_update_pending = DisplayUpdate::default();
+ let old_is_searching = self.search_state.regex.is_some();
let context = ActionContext {
terminal: &mut terminal,
@@ -530,6 +753,7 @@ impl<N: Notify + OnResize> Processor<N> {
config: &mut self.config,
urls: &self.display.urls,
scheduler: &mut scheduler,
+ search_state: &mut self.search_state,
event_loop,
};
let mut processor = input::Processor::new(context, &self.display.highlighted_url);
@@ -539,14 +763,8 @@ impl<N: Notify + OnResize> Processor<N> {
}
// Process DisplayUpdate events.
- if !display_update_pending.is_empty() {
- self.display.handle_update(
- &mut terminal,
- &mut self.notifier,
- &self.message_buffer,
- &self.config,
- display_update_pending,
- );
+ if display_update_pending.dirty {
+ self.submit_display_update(&mut terminal, old_is_searching, display_update_pending);
}
#[cfg(not(any(target_os = "macos", windows)))]
@@ -575,12 +793,15 @@ impl<N: Notify + OnResize> Processor<N> {
&self.config,
&self.mouse,
self.modifiers,
+ self.search_state.regex.as_ref(),
);
}
});
// Write ref tests to disk.
- self.write_ref_test_results(&terminal.lock());
+ if self.config.debug.ref_test {
+ self.write_ref_test_results(&terminal.lock());
+ }
}
/// Handle events from glutin.
@@ -598,20 +819,21 @@ impl<N: Notify + OnResize> Processor<N> {
let display_update_pending = &mut processor.ctx.display_update_pending;
// Push current font to update its DPR.
- display_update_pending.font =
- Some(processor.ctx.config.font.clone().with_size(*processor.ctx.font_size));
+ let font = processor.ctx.config.font.clone();
+ display_update_pending.set_font(font.with_size(*processor.ctx.font_size));
// Resize to event's dimensions, since no resize event is emitted on Wayland.
- display_update_pending.dimensions = Some(PhysicalSize::new(width, height));
+ display_update_pending.set_dimensions(PhysicalSize::new(width, height));
processor.ctx.size_info.dpr = scale_factor;
processor.ctx.terminal.dirty = true;
},
Event::Message(message) => {
processor.ctx.message_buffer.push(message);
- processor.ctx.display_update_pending.message_buffer = true;
+ processor.ctx.display_update_pending.dirty = true;
processor.ctx.terminal.dirty = true;
},
+ Event::SearchNext => processor.ctx.goto_match(None),
Event::ConfigReload(path) => Self::reload_config(&path, processor),
Event::Scroll(scroll) => processor.ctx.scroll(scroll),
Event::TerminalEvent(event) => match event {
@@ -647,7 +869,7 @@ impl<N: Notify + OnResize> Processor<N> {
}
}
- processor.ctx.display_update_pending.dimensions = Some(size);
+ processor.ctx.display_update_pending.set_dimensions(size);
processor.ctx.terminal.dirty = true;
},
WindowEvent::KeyboardInput { input, is_synthetic: false, .. } => {
@@ -741,14 +963,14 @@ impl<N: Notify + OnResize> Processor<N> {
}
}
- pub fn reload_config<T>(
- path: &PathBuf,
- processor: &mut input::Processor<T, ActionContext<N, T>>,
- ) where
+ fn reload_config<T>(path: &PathBuf, processor: &mut input::Processor<T, ActionContext<N, T>>)
+ where
T: EventListener,
{
- processor.ctx.message_buffer.remove_target(LOG_TARGET_CONFIG);
- processor.ctx.display_update_pending.message_buffer = true;
+ if !processor.ctx.message_buffer.is_empty() {
+ processor.ctx.message_buffer.remove_target(LOG_TARGET_CONFIG);
+ processor.ctx.display_update_pending.dirty = true;
+ }
let config = match config::reload_from(&path) {
Ok(config) => config,
@@ -764,7 +986,7 @@ impl<N: Notify + OnResize> Processor<N> {
if (processor.ctx.config.cursor.thickness() - config.cursor.thickness()).abs()
> std::f64::EPSILON
{
- processor.ctx.display_update_pending.cursor = true;
+ processor.ctx.display_update_pending.set_cursor_dirty();
}
if processor.ctx.config.font != config.font {
@@ -774,7 +996,7 @@ impl<N: Notify + OnResize> Processor<N> {
}
let font = config.font.clone().with_size(*processor.ctx.font_size);
- processor.ctx.display_update_pending.font = Some(font);
+ processor.ctx.display_update_pending.set_font(font);
}
#[cfg(not(any(target_os = "macos", windows)))]
@@ -793,12 +1015,44 @@ impl<N: Notify + OnResize> Processor<N> {
processor.ctx.terminal.dirty = true;
}
- // 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;
+ /// Submit the pending changes to the `Display`.
+ fn submit_display_update<T>(
+ &mut self,
+ terminal: &mut Term<T>,
+ old_is_searching: bool,
+ display_update_pending: DisplayUpdate,
+ ) where
+ T: EventListener,
+ {
+ // Compute cursor positions before resize.
+ let num_lines = terminal.screen_lines();
+ let cursor_at_bottom = terminal.grid().cursor.point.line + 1 == num_lines;
+ let origin_at_bottom = (!terminal.mode().contains(TermMode::VI)
+ && self.search_state.direction == Direction::Left)
+ || terminal.vi_mode_cursor.point.line == num_lines - 1;
+
+ self.display.handle_update(
+ terminal,
+ &mut self.notifier,
+ &self.message_buffer,
+ self.search_state.regex.is_some(),
+ &self.config,
+ display_update_pending,
+ );
+
+ // Scroll to make sure search origin is visible and content moves as little as possible.
+ if !old_is_searching && self.search_state.regex.is_some() {
+ let display_offset = terminal.grid().display_offset();
+ if display_offset == 0 && cursor_at_bottom && !origin_at_bottom {
+ terminal.scroll_display(Scroll::Delta(1));
+ } else if display_offset != 0 && origin_at_bottom {
+ terminal.scroll_display(Scroll::Delta(-1));
+ }
}
+ }
+ /// Write the ref test results to the disk.
+ fn write_ref_test_results<T>(&self, terminal: &Term<T>) {
// Dump grid state.
let mut grid = terminal.grid().clone();
grid.initialize_all(Cell::default());