From 1df7dc5171abfe1eab3e95be964f61c5876198f1 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Sat, 23 Oct 2021 07:16:47 +0000 Subject: [PATCH] Add multi-window support Previously Alacritty would always initialize only a single terminal emulator window feeding into the winit event loop, however some platforms like macOS expect all windows to be spawned by the same process and this "daemon-mode" can also come with the advantage of increased memory efficiency. The event loop has been restructured to handle all window-specific events only by the event processing context with the associated window id. This makes it possible to add new terminal windows at any time using the WindowContext::new function call. Some preliminary tests have shown that for empty terminals, this reduces the cost of additional terminal emulators from ~100M to ~6M. However at this point the robustness of the daemon against issues with individual terminals has not been refined, making the reliability of this system questionable. New windows can be created either by using the new `CreateNewWindow` action, or with the `alacritty msg create-window` subcommand. The subcommand sends a message to an IPC socket which Alacritty listens on, its location can be found in the `ALACRITTY_SOCKET` environment variable. Fixes #607. --- CHANGELOG.md | 1 + Cargo.lock | 7 +- alacritty.yml | 5 + alacritty/Cargo.toml | 2 +- alacritty/src/cli.rs | 51 +- alacritty/src/config/bindings.rs | 5 +- alacritty/src/config/monitor.rs | 10 +- alacritty/src/config/ui_config.rs | 8 +- alacritty/src/display/mod.rs | 64 ++- alacritty/src/display/window.rs | 68 ++- alacritty/src/event.rs | 829 +++++++++++---------------- alacritty/src/input.rs | 36 +- alacritty/src/ipc.rs | 145 +++++ alacritty/src/logging.rs | 21 +- alacritty/src/main.rs | 218 ++++--- alacritty/src/message_bar.rs | 35 +- alacritty/src/renderer/mod.rs | 29 +- alacritty/src/scheduler.rs | 69 +-- alacritty/src/window_context.rs | 374 ++++++++++++ alacritty_terminal/src/event.rs | 10 +- alacritty_terminal/src/event_loop.rs | 6 +- alacritty_terminal/src/term/mod.rs | 12 +- alacritty_terminal/src/tty/unix.rs | 42 +- docs/features.md | 6 + extra/alacritty-msg.man | 31 + extra/alacritty.man | 9 +- extra/completions/_alacritty | 74 ++- extra/completions/alacritty.bash | 9 +- extra/completions/alacritty.fish | 62 +- 29 files changed, 1424 insertions(+), 814 deletions(-) create mode 100644 alacritty/src/ipc.rs create mode 100644 alacritty/src/window_context.rs create mode 100644 extra/alacritty-msg.man diff --git a/CHANGELOG.md b/CHANGELOG.md index f337f601..4f464608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Option `colors.transparent_background_colors` to allow applying opacity to all background colors +- Support for running multiple windows from a single Alacritty instance (see docs/features.md) ### Changed diff --git a/Cargo.lock b/Cargo.lock index 38a44a21..fee60d82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1989,9 +1989,12 @@ dependencies = [ [[package]] name = "xdg" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" +checksum = "3a23fe958c70412687039c86f578938b4a0bb50ec788e96bce4d6ab00ddd5803" +dependencies = [ + "dirs", +] [[package]] name = "xml-rs" diff --git a/alacritty.yml b/alacritty.yml index 04654e56..09abce3c 100644 --- a/alacritty.yml +++ b/alacritty.yml @@ -440,6 +440,9 @@ # Send ESC (\x1b) before characters when alt is pressed. #alt_send_esc: true +# Offer IPC using `alacritty msg` (unix only) +#ipc_socket: true + #mouse: # Click settings # @@ -595,6 +598,8 @@ # - ToggleFullscreen # - SpawnNewInstance # Spawn a new instance of Alacritty. +# - CreateNewWindow +# Create a new Alacritty window from the current process. # - ClearLogNotice # Clear Alacritty's UI warning and error notice. # - ClearSelection diff --git a/alacritty/Cargo.toml b/alacritty/Cargo.toml index b1fee0c2..5d02d19c 100644 --- a/alacritty/Cargo.toml +++ b/alacritty/Cargo.toml @@ -39,7 +39,7 @@ dirs = "3.0.1" gl_generator = "0.14.0" [target.'cfg(not(windows))'.dependencies] -xdg = "2" +xdg = "2.4.0" [target.'cfg(not(target_os = "macos"))'.dependencies] png = { version = "0.16.8", default-features = false, optional = true } diff --git a/alacritty/src/cli.rs b/alacritty/src/cli.rs index b1e12007..ce0563ff 100644 --- a/alacritty/src/cli.rs +++ b/alacritty/src/cli.rs @@ -10,7 +10,7 @@ use alacritty_terminal::config::Program; use crate::config::window::{Class, DEFAULT_NAME}; use crate::config::{serde_utils, Config}; -/// Options specified on the command line. +/// CLI options for the main Alacritty executable. #[derive(StructOpt, Debug)] #[structopt(author, about, version = env!("VERSION"))] pub struct Options { @@ -57,9 +57,10 @@ pub struct Options { #[structopt(long)] pub hold: bool, - /// CLI options for config overrides. - #[structopt(skip)] - pub config_options: Value, + /// Path for IPC socket creation. + #[cfg(unix)] + #[structopt(long)] + pub socket: Option, /// Reduces the level of verbosity (the min level is -qq). #[structopt(short, conflicts_with("verbose"), parse(from_occurrences))] @@ -76,6 +77,15 @@ pub struct Options { /// Override configuration file options [example: cursor.style=Beam]. #[structopt(short = "o", long)] option: Vec, + + /// CLI options for config overrides. + #[structopt(skip)] + pub config_options: Value, + + /// Subcommand passed to the CLI. + #[cfg(unix)] + #[structopt(subcommand)] + pub subcommands: Option, } impl Options { @@ -118,6 +128,11 @@ impl Options { config.ui_config.window.class = class.clone(); } + #[cfg(unix)] + { + config.ui_config.ipc_socket |= self.socket.is_some(); + } + config.ui_config.window.dynamic_title &= self.title.is_none(); config.ui_config.window.embed = self.embed.as_ref().and_then(|embed| embed.parse().ok()); config.ui_config.debug.print_events |= self.print_events; @@ -199,6 +214,34 @@ fn parse_class(input: &str) -> Result { } } +/// Available CLI subcommands. +#[cfg(unix)] +#[derive(StructOpt, Debug)] +pub enum Subcommands { + Msg(MessageOptions), +} + +/// Send a message to the Alacritty socket. +#[cfg(unix)] +#[derive(StructOpt, Debug)] +pub struct MessageOptions { + /// IPC socket connection path override. + #[structopt(long, short)] + pub socket: Option, + + /// Message which should be sent. + #[structopt(subcommand)] + pub message: SocketMessage, +} + +/// Available socket messages. +#[cfg(unix)] +#[derive(StructOpt, Debug)] +pub enum SocketMessage { + /// Create a new window in the same Alacritty process. + CreateWindow, +} + #[cfg(test)] mod tests { use super::*; diff --git a/alacritty/src/config/bindings.rs b/alacritty/src/config/bindings.rs index 8289fc20..533573c8 100644 --- a/alacritty/src/config/bindings.rs +++ b/alacritty/src/config/bindings.rs @@ -180,6 +180,9 @@ pub enum Action { /// Spawn a new instance of Alacritty. SpawnNewInstance, + /// Create a new Alacritty window. + CreateNewWindow, + /// Toggle fullscreen. ToggleFullscreen, @@ -1099,7 +1102,7 @@ impl<'a> Deserialize<'a> for RawBinding { let mode = mode.unwrap_or_else(BindingMode::empty); let not_mode = not_mode.unwrap_or_else(BindingMode::empty); - let mods = mods.unwrap_or_else(ModifiersState::default); + let mods = mods.unwrap_or_default(); let action = match (action, chars, command) { (Some(action @ Action::ViMotion(_)), None, None) diff --git a/alacritty/src/config/monitor.rs b/alacritty/src/config/monitor.rs index e3dd0556..9d37172e 100644 --- a/alacritty/src/config/monitor.rs +++ b/alacritty/src/config/monitor.rs @@ -2,19 +2,20 @@ use std::path::PathBuf; use std::sync::mpsc; use std::time::Duration; +use glutin::event_loop::EventLoopProxy; use log::{debug, error}; use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; use alacritty_terminal::thread; -use crate::event::{Event, EventProxy}; +use crate::event::{Event, EventType}; #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] const DEBOUNCE_DELAY: Duration = Duration::from_millis(10); #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] const DEBOUNCE_DELAY: Duration = Duration::from_millis(1000); -pub fn watch(mut paths: Vec, event_proxy: EventProxy) { +pub fn watch(mut paths: Vec, event_proxy: EventLoopProxy) { // Don't monitor config if there is no path to watch. if paths.is_empty() { return; @@ -77,9 +78,10 @@ pub fn watch(mut paths: Vec, event_proxy: EventProxy) { if paths.contains(&path) => { // Always reload the primary configuration file. - event_proxy.send_event(Event::ConfigReload(paths[0].clone())); + let event = Event::new(EventType::ConfigReload(paths[0].clone()), None); + let _ = event_proxy.send_event(event); } - _ => {}, + _ => (), } } }); diff --git a/alacritty/src/config/ui_config.rs b/alacritty/src/config/ui_config.rs index 3ce02161..3ba59ea8 100644 --- a/alacritty/src/config/ui_config.rs +++ b/alacritty/src/config/ui_config.rs @@ -62,6 +62,10 @@ pub struct UiConfig { /// Regex hints for interacting with terminal content. pub hints: Hints, + /// Offer IPC through a unix socket. + #[cfg(unix)] + pub ipc_socket: bool, + /// Keybindings. key_bindings: KeyBindings, @@ -76,8 +80,10 @@ pub struct UiConfig { impl Default for UiConfig { fn default() -> Self { Self { - alt_send_esc: true, live_config_reload: true, + alt_send_esc: true, + #[cfg(unix)] + ipc_socket: true, font: Default::default(), window: Default::default(), mouse: Default::default(), diff --git a/alacritty/src/display/mod.rs b/alacritty/src/display/mod.rs index 946c27f9..a942a88d 100644 --- a/alacritty/src/display/mod.rs +++ b/alacritty/src/display/mod.rs @@ -3,15 +3,15 @@ use std::cmp::min; use std::convert::TryFrom; -use std::f64; use std::fmt::{self, Formatter}; #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] use std::sync::atomic::Ordering; use std::time::Instant; +use std::{f64, mem}; use glutin::dpi::{PhysicalPosition, PhysicalSize}; use glutin::event::ModifiersState; -use glutin::event_loop::EventLoop; +use glutin::event_loop::EventLoopWindowTarget; #[cfg(not(any(target_os = "macos", windows)))] use glutin::platform::unix::EventLoopWindowTargetExtUnix; use glutin::window::CursorIcon; @@ -19,7 +19,7 @@ use log::{debug, info}; use parking_lot::MutexGuard; use unicode_width::UnicodeWidthChar; #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] -use wayland_client::{Display as WaylandDisplay, EventQueue}; +use wayland_client::EventQueue; use crossfont::{self, Rasterize, Rasterizer}; @@ -178,9 +178,6 @@ pub struct Display { /// Hint highlighted by the vi mode cursor. pub vi_highlighted_hint: Option, - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - pub wayland_event_queue: Option, - #[cfg(not(any(target_os = "macos", windows)))] pub is_x11: bool, @@ -195,13 +192,21 @@ pub struct Display { /// State of the keyboard hints. pub hint_state: HintState, + /// Unprocessed display updates. + pub pending_update: DisplayUpdate, + renderer: QuadRenderer, glyph_cache: GlyphCache, meter: Meter, } impl Display { - pub fn new(config: &Config, event_loop: &EventLoop) -> Result { + pub fn new( + config: &Config, + event_loop: &EventLoopWindowTarget, + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + wayland_event_queue: Option<&EventQueue>, + ) -> Result { #[cfg(any(not(feature = "x11"), target_os = "macos", windows))] let is_x11 = false; #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] @@ -229,23 +234,13 @@ impl Display { debug!("Estimated window size: {:?}", estimated_size); debug!("Estimated cell size: {} x {}", cell_width, cell_height); - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - let mut wayland_event_queue = None; - - // Initialize Wayland event queue, to handle Wayland callbacks. - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - if let Some(display) = event_loop.wayland_display() { - let display = unsafe { WaylandDisplay::from_external_display(display as _) }; - wayland_event_queue = Some(display.create_event_queue()); - } - // Spawn the Alacritty window. let mut window = Window::new( event_loop, config, estimated_size, #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - wayland_event_queue.as_ref(), + wayland_event_queue, )?; info!("Device pixel ratio: {}", window.dpr); @@ -344,11 +339,10 @@ impl Display { vi_highlighted_hint: None, #[cfg(not(any(target_os = "macos", windows)))] is_x11, - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - wayland_event_queue, cursor_hidden: false, visual_bell: VisualBell::from(&config.ui_config.bell), colors: List::from(&config.ui_config.colors), + pending_update: Default::default(), }) } @@ -414,26 +408,30 @@ impl Display { message_buffer: &MessageBuffer, search_active: bool, config: &Config, - update_pending: DisplayUpdate, ) where T: EventListener, { + let pending_update = mem::take(&mut self.pending_update); + let (mut cell_width, mut cell_height) = (self.size_info.cell_width(), self.size_info.cell_height()); + // Ensure we're modifying the correct OpenGL context. + self.window.make_current(); + // Update font size and cell dimensions. - if let Some(font) = update_pending.font() { + if let Some(font) = pending_update.font() { let cell_dimensions = self.update_glyph_cache(config, font); cell_width = cell_dimensions.0; cell_height = cell_dimensions.1; info!("Cell size: {} x {}", cell_width, cell_height); - } else if update_pending.cursor_dirty() { + } else if pending_update.cursor_dirty() { self.clear_glyph_cache(); } let (mut width, mut height) = (self.size_info.width(), self.size_info.height()); - if let Some(dimensions) = update_pending.dimensions() { + if let Some(dimensions) = pending_update.dimensions() { width = dimensions.width as f32; height = dimensions.height as f32; } @@ -463,8 +461,7 @@ impl Display { terminal.resize(self.size_info); // Resize renderer. - let physical = - PhysicalSize::new(self.size_info.width() as u32, self.size_info.height() as u32); + let physical = PhysicalSize::new(self.size_info.width() as _, self.size_info.height() as _); self.window.resize(physical); self.renderer.resize(&self.size_info); @@ -505,6 +502,9 @@ impl Display { // Drop terminal as early as possible to free lock. drop(terminal); + // Make sure this window's OpenGL context is active. + self.window.make_current(); + self.renderer.with_api(&config.ui_config, &size_info, |api| { api.clear(background_color); }); @@ -515,6 +515,10 @@ impl Display { { let _sampler = self.meter.sampler(); + // Ensure macOS hasn't reset our viewport. + #[cfg(target_os = "macos")] + self.renderer.set_viewport(&size_info); + let glyph_cache = &mut self.glyph_cache; let highlighted_hint = &self.highlighted_hint; let vi_highlighted_hint = &self.vi_highlighted_hint; @@ -819,6 +823,14 @@ impl Display { } } +impl Drop for Display { + fn drop(&mut self) { + // Switch OpenGL context before dropping, otherwise objects (like programs) from other + // contexts might be deleted. + self.window.make_current() + } +} + /// Convert a terminal point to a viewport relative point. pub fn point_to_viewport(display_offset: usize, point: Point) -> Option> { let viewport_line = point.line.0 + display_offset as i32; diff --git a/alacritty/src/display/window.rs b/alacritty/src/display/window.rs index 12416700..16932dc4 100644 --- a/alacritty/src/display/window.rs +++ b/alacritty/src/display/window.rs @@ -29,11 +29,12 @@ use { }; use std::fmt::{self, Display, Formatter}; +use std::ops::{Deref, DerefMut}; #[cfg(target_os = "macos")] use cocoa::base::{id, NO, YES}; use glutin::dpi::{PhysicalPosition, PhysicalSize}; -use glutin::event_loop::EventLoop; +use glutin::event_loop::EventLoopWindowTarget; #[cfg(target_os = "macos")] use glutin::platform::macos::{WindowBuilderExtMacOS, WindowExtMacOS}; #[cfg(windows)] @@ -124,7 +125,7 @@ impl From for Error { fn create_gl_window( mut window: WindowBuilder, - event_loop: &EventLoop, + event_loop: &EventLoopWindowTarget, srgb: bool, vsync: bool, dimensions: Option>, @@ -160,7 +161,7 @@ pub struct Window { /// Cached DPR for quickly scaling pixel sizes. pub dpr: f64, - windowed_context: WindowedContext, + windowed_context: Replaceable>, current_mouse_cursor: CursorIcon, mouse_visible: bool, } @@ -170,7 +171,7 @@ impl Window { /// /// This creates a window and fully initializes a window. pub fn new( - event_loop: &EventLoop, + event_loop: &EventLoopWindowTarget, config: &Config, size: Option>, #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] @@ -232,7 +233,7 @@ impl Window { Ok(Self { current_mouse_cursor, mouse_visible: true, - windowed_context, + windowed_context: Replaceable::new(windowed_context), #[cfg(not(any(target_os = "macos", windows)))] should_draw: Arc::new(AtomicBool::new(true)), #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] @@ -241,10 +242,12 @@ impl Window { }) } + #[inline] pub fn set_inner_size(&mut self, size: PhysicalSize) { self.window().set_inner_size(size); } + #[inline] pub fn inner_size(&self) -> PhysicalSize { self.window().inner_size() } @@ -260,6 +263,11 @@ impl Window { self.window().set_title(title); } + #[inline] + pub fn request_redraw(&self) { + self.window().request_redraw(); + } + #[inline] pub fn set_mouse_cursor(&mut self, cursor: CursorIcon) { if cursor != self.current_mouse_cursor { @@ -374,7 +382,7 @@ impl Window { None } - pub fn window_id(&self) -> WindowId { + pub fn id(&self) -> WindowId { self.window().id() } @@ -436,6 +444,13 @@ impl Window { self.windowed_context.resize(size); } + pub fn make_current(&mut self) { + if !self.windowed_context.is_current() { + self.windowed_context + .replace_with(|context| unsafe { context.make_current().expect("context swap") }); + } + } + /// Disable macOS window shadows. /// /// This prevents rendering artifacts from showing up when the window is transparent. @@ -496,3 +511,44 @@ unsafe extern "C" fn xembed_error_handler(_: *mut XDisplay, _: *mut XErrorEvent) log::error!("Could not embed into specified window."); std::process::exit(1); } + +/// Struct for safe in-place replacement. +/// +/// This struct allows easily replacing struct fields that provide `self -> Self` methods in-place, +/// without having to deal with constantly unwrapping the underlying [`Option`]. +struct Replaceable(Option); + +impl Replaceable { + pub fn new(inner: T) -> Self { + Self(Some(inner)) + } + + /// Replace the contents of the container. + pub fn replace_with T>(&mut self, f: F) { + self.0 = self.0.take().map(f); + } + + /// Get immutable access to the wrapped value. + pub fn get(&self) -> &T { + self.0.as_ref().unwrap() + } + + /// Get mutable access to the wrapped value. + pub fn get_mut(&mut self) -> &mut T { + self.0.as_mut().unwrap() + } +} + +impl Deref for Replaceable { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.get() + } +} + +impl DerefMut for Replaceable { + fn deref_mut(&mut self) -> &mut Self::Target { + self.get_mut() + } +} diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index 8e8fac08..09e74a9d 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -2,16 +2,12 @@ use std::borrow::Cow; use std::cmp::{max, min}; -use std::collections::VecDeque; +use std::collections::{HashMap, VecDeque}; +use std::error::Error; use std::fmt::Debug; #[cfg(not(any(target_os = "macos", windows)))] use std::fs; -use std::fs::File; -use std::io::Write; -use std::path::{Path, PathBuf}; -#[cfg(not(any(target_os = "macos", windows)))] -use std::sync::atomic::Ordering; -use std::sync::Arc; +use std::path::PathBuf; use std::time::{Duration, Instant}; use std::{env, f32, mem}; @@ -21,35 +17,38 @@ use glutin::event_loop::{ControlFlow, EventLoop, EventLoopProxy, EventLoopWindow use glutin::platform::run_return::EventLoopExtRunReturn; #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] use glutin::platform::unix::EventLoopWindowTargetExtUnix; -use log::info; -use serde_json as json; +use glutin::window::WindowId; +use log::{error, info}; +#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] +use wayland_client::{Display as WaylandDisplay, EventQueue}; use crossfont::{self, Size}; use alacritty_terminal::config::LOG_TARGET_CONFIG; -use alacritty_terminal::event::{Event as TerminalEvent, EventListener, Notify, OnResize}; +use alacritty_terminal::event::{Event as TerminalEvent, EventListener, Notify}; +use alacritty_terminal::event_loop::Notifier; use alacritty_terminal::grid::{Dimensions, Scroll}; use alacritty_terminal::index::{Boundary, Column, Direction, Line, Point, Side}; use alacritty_terminal::selection::{Selection, SelectionType}; -use alacritty_terminal::sync::FairMutex; use alacritty_terminal::term::search::{Match, RegexSearch}; use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode}; #[cfg(not(windows))] use alacritty_terminal::tty; -use crate::cli::Options as CLIOptions; +use crate::cli::Options as CliOptions; use crate::clipboard::Clipboard; use crate::config::ui_config::{HintAction, HintInternalAction}; use crate::config::{self, Config}; use crate::daemon::start_daemon; use crate::display::hint::HintMatch; use crate::display::window::Window; -use crate::display::{self, Display, DisplayUpdate}; +use crate::display::{self, Display}; use crate::input::{self, ActionContext as _, FONT_SIZE_STEP}; #[cfg(target_os = "macos")] use crate::macos; use crate::message_bar::{Message, MessageBuffer}; -use crate::scheduler::{Scheduler, TimerId}; +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); @@ -60,16 +59,20 @@ const MAX_SEARCH_WHILE_TYPING: Option = Some(1000); /// Maximum number of search terms stored in the history. const MAX_SEARCH_HISTORY_SIZE: usize = 255; -/// Events dispatched through the UI event loop. +/// Alacritty events. #[derive(Debug, Clone)] -pub enum Event { - Terminal(TerminalEvent), - DprChanged(f64, (u32, u32)), - Scroll(Scroll), - ConfigReload(PathBuf), - Message(Message), - BlinkCursor, - SearchNext, +pub struct Event { + /// Limit event to a specific window. + window_id: Option, + + /// Event payload. + payload: EventType, +} + +impl Event { + pub fn new>>(payload: EventType, window_id: I) -> Self { + Self { window_id: window_id.into(), payload } + } } impl From for GlutinEvent<'_, Event> { @@ -78,16 +81,32 @@ impl From for GlutinEvent<'_, Event> { } } -impl From for Event { +/// Alacritty events. +#[derive(Debug, Clone)] +pub enum EventType { + DprChanged(f64, (u32, u32)), + Terminal(TerminalEvent), + ConfigReload(PathBuf), + Message(Message), + Scroll(Scroll), + CreateWindow, + BlinkCursor, + SearchNext, +} + +impl From for EventType { fn from(event: TerminalEvent) -> Self { - Event::Terminal(event) + Self::Terminal(event) } } /// Regex search state. pub struct SearchState { /// Search direction. - direction: Direction, + pub direction: Direction, + + /// Current position in the search history. + pub history_index: Option, /// Change in display offset since the beginning of the search. display_offset_delta: i32, @@ -106,9 +125,6 @@ pub struct SearchState { /// in history which is currently being previewed. history: VecDeque, - /// Current position in the search history. - history_index: Option, - /// Compiled search automatons. dfas: Option, } @@ -164,14 +180,13 @@ pub struct ActionContext<'a, N, T> { pub modifiers: &'a mut ModifiersState, pub display: &'a mut Display, pub message_buffer: &'a mut MessageBuffer, - pub display_update_pending: &'a mut DisplayUpdate, pub config: &'a mut Config, pub event_loop: &'a EventLoopWindowTarget, + pub event_proxy: &'a EventLoopProxy, pub scheduler: &'a mut Scheduler, pub search_state: &'a mut SearchState, - cli_options: &'a CLIOptions, - font_size: &'a mut Size, - dirty: &'a mut bool, + pub font_size: &'a mut Size, + pub dirty: &'a mut bool, } impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionContext<'a, N, T> { @@ -380,23 +395,27 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon start_daemon(&alacritty, &args); } + fn create_new_window(&mut self) { + let _ = self.event_proxy.send_event(Event::new(EventType::CreateWindow, None)); + } + 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.ui_config.font.clone().with_size(*self.font_size); - self.display_update_pending.set_font(font); + self.display.pending_update.set_font(font); *self.dirty = true; } fn reset_font_size(&mut self) { *self.font_size = self.config.ui_config.font.size(); - self.display_update_pending.set_font(self.config.ui_config.font.clone()); + self.display.pending_update.set_font(self.config.ui_config.font.clone()); *self.dirty = true; } #[inline] fn pop_message(&mut self) { if !self.message_buffer.is_empty() { - self.display_update_pending.dirty = true; + self.display.pending_update.dirty = true; self.message_buffer.pop(); *self.dirty = true; } @@ -433,7 +452,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon }; } - self.display_update_pending.dirty = true; + self.display.pending_update.dirty = true; *self.dirty = true; } @@ -446,7 +465,8 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon } // Force unlimited search if the previous one was interrupted. - if self.scheduler.scheduled(TimerId::DelayedSearch) { + let timer_id = TimerId::new(Topic::DelayedSearch, self.display.window.id()); + if self.scheduler.scheduled(timer_id) { self.goto_match(None); } @@ -610,9 +630,10 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon #[inline] fn on_typing_start(&mut self) { // Disable cursor blinking. - let blink_interval = self.config.cursor.blink_interval(); - if let Some(timer) = self.scheduler.get_mut(TimerId::BlinkCursor) { - timer.deadline = Instant::now() + Duration::from_millis(blink_interval); + let timer_id = TimerId::new(Topic::BlinkCursor, self.display.window.id()); + if let Some(timer) = self.scheduler.unschedule(timer_id) { + let interval = Duration::from_millis(self.config.cursor.blink_interval()); + self.scheduler.schedule(timer.event, interval, true, timer.id); self.display.cursor_hidden = false; *self.dirty = true; } @@ -795,7 +816,8 @@ 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) { // Unschedule pending timers. - self.scheduler.unschedule(TimerId::DelayedSearch); + 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; @@ -849,19 +871,17 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { self.search_state.display_offset_delta += old_offset - display_offset as i32; // Since we found a result, we require no delayed re-search. - self.scheduler.unschedule(TimerId::DelayedSearch); + 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. - if !self.scheduler.scheduled(TimerId::DelayedSearch) { - self.scheduler.schedule( - Event::SearchNext.into(), - TYPING_SEARCH_DELAY, - false, - TimerId::DelayedSearch, - ); + 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. @@ -874,7 +894,7 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { /// Cleanup the search state. fn exit_search(&mut self) { - self.display_update_pending.dirty = true; + self.display.pending_update.dirty = true; self.search_state.history_index = None; *self.dirty = true; @@ -895,14 +915,12 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { let blinking = cursor_style.blinking_override().unwrap_or(terminal_blinking); // Update cursor blinking state. - self.scheduler.unschedule(TimerId::BlinkCursor); + let timer_id = TimerId::new(Topic::BlinkCursor, self.display.window.id()); + self.scheduler.unschedule(timer_id); if blinking && self.terminal.is_focused { - self.scheduler.schedule( - GlutinEvent::UserEvent(Event::BlinkCursor), - Duration::from_millis(self.config.cursor.blink_interval()), - true, - TimerId::BlinkCursor, - ) + let event = Event::new(EventType::BlinkCursor, self.display.window.id()); + let interval = Duration::from_millis(self.config.cursor.blink_interval()); + self.scheduler.schedule(event, interval, true, timer_id); } else { self.display.cursor_hidden = false; *self.dirty = true; @@ -975,305 +993,88 @@ impl Mouse { } } -/// The event processor. -/// -/// Stores some state from received events and dispatches actions when they are -/// triggered. -pub struct Processor { - notifier: N, - mouse: Mouse, - received_count: usize, - suppress_chars: bool, - modifiers: ModifiersState, - config: Config, - message_buffer: MessageBuffer, - display: Display, - font_size: Size, - event_queue: Vec>, - search_state: SearchState, - cli_options: CLIOptions, - dirty: bool, -} - -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( - notifier: N, - message_buffer: MessageBuffer, - config: Config, - display: Display, - cli_options: CLIOptions, - ) -> Processor { - Processor { - font_size: config.ui_config.font.size(), - message_buffer, - cli_options, - notifier, - display, - config, - received_count: Default::default(), - suppress_chars: Default::default(), - search_state: Default::default(), - event_queue: Default::default(), - modifiers: Default::default(), - mouse: Default::default(), - dirty: Default::default(), - } - } - - /// Return `true` if `event_queue` is empty, `false` otherwise. - #[inline] - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - fn event_queue_empty(&mut self) -> bool { - let wayland_event_queue = match self.display.wayland_event_queue.as_mut() { - Some(wayland_event_queue) => wayland_event_queue, - // Since frame callbacks do not exist on X11, just check for event queue. - None => return self.event_queue.is_empty(), - }; - - // Check for pending frame callbacks on Wayland. - let events_dispatched = wayland_event_queue - .dispatch_pending(&mut (), |_, _, _| {}) - .expect("failed to dispatch event queue"); - - self.event_queue.is_empty() && events_dispatched == 0 - } - - /// Return `true` if `event_queue` is empty, `false` otherwise. - #[inline] - #[cfg(any(not(feature = "wayland"), target_os = "macos", windows))] - fn event_queue_empty(&mut self) -> bool { - self.event_queue.is_empty() - } - - /// Run the event loop. - pub fn run(&mut self, terminal: Arc>>, mut event_loop: EventLoop) - where - T: EventListener, - { - let mut scheduler = Scheduler::new(); - - // Start the initial cursor blinking timer. - if self.config.cursor.style().blinking { - let event: Event = TerminalEvent::CursorBlinkingChange(true).into(); - self.event_queue.push(event.into()); - } - - // NOTE: Since this takes a pointer to the winit event loop, it MUST be dropped first. - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - let mut clipboard = unsafe { Clipboard::new(event_loop.wayland_display()) }; - #[cfg(any(not(feature = "wayland"), target_os = "macos", windows))] - let mut clipboard = Clipboard::new(); - - event_loop.run_return(|event, event_loop, control_flow| { - if self.config.ui_config.debug.print_events { - info!("glutin event: {:?}", event); - } - - // Ignore all events we do not care about. - if Self::skip_event(&event) { - return; - } - - match event { - // Check for shutdown. - GlutinEvent::UserEvent(Event::Terminal(TerminalEvent::Exit)) => { - *control_flow = ControlFlow::Exit; - return; - }, - // Process events. - GlutinEvent::RedrawEventsCleared => { - *control_flow = match scheduler.update(&mut self.event_queue) { - Some(instant) => ControlFlow::WaitUntil(instant), - None => ControlFlow::Wait, - }; - - if self.event_queue_empty() { - return; - } - }, - // Remap DPR change event to remove lifetime. - GlutinEvent::WindowEvent { - event: WindowEvent::ScaleFactorChanged { scale_factor, new_inner_size }, - .. - } => { - *control_flow = ControlFlow::Poll; - let size = (new_inner_size.width, new_inner_size.height); - self.event_queue.push(Event::DprChanged(scale_factor, size).into()); - return; - }, - // Transmute to extend lifetime, which exists only for `ScaleFactorChanged` event. - // Since we remap that event to remove the lifetime, this is safe. - event => unsafe { - *control_flow = ControlFlow::Poll; - self.event_queue.push(mem::transmute(event)); - return; - }, - } - - let mut terminal = terminal.lock(); - - let mut display_update_pending = DisplayUpdate::default(); - let old_is_searching = self.search_state.history_index.is_some(); - - let context = ActionContext { - terminal: &mut terminal, - notifier: &mut self.notifier, - mouse: &mut self.mouse, - clipboard: &mut clipboard, - received_count: &mut self.received_count, - suppress_chars: &mut self.suppress_chars, - modifiers: &mut self.modifiers, - message_buffer: &mut self.message_buffer, - display_update_pending: &mut display_update_pending, - display: &mut self.display, - font_size: &mut self.font_size, - config: &mut self.config, - scheduler: &mut scheduler, - search_state: &mut self.search_state, - cli_options: &self.cli_options, - dirty: &mut self.dirty, - event_loop, - }; - let mut processor = input::Processor::new(context); - - for event in self.event_queue.drain(..) { - Processor::handle_event(event, &mut processor); - } - - // Process DisplayUpdate events. - if display_update_pending.dirty { - self.submit_display_update(&mut terminal, old_is_searching, display_update_pending); - } - - // Skip rendering on Wayland until we get frame event from compositor. - #[cfg(not(any(target_os = "macos", windows)))] - if !self.display.is_x11 && !self.display.window.should_draw.load(Ordering::Relaxed) { - return; - } - - if self.dirty || self.mouse.hint_highlight_dirty { - self.dirty |= self.display.update_highlighted_hints( - &terminal, - &self.config, - &self.mouse, - self.modifiers, - ); - self.mouse.hint_highlight_dirty = false; - } - - if self.dirty { - self.dirty = false; - - // Request immediate re-draw if visual bell animation is not finished yet. - if !self.display.visual_bell.completed() { - let event: Event = TerminalEvent::Wakeup.into(); - self.event_queue.push(event.into()); - - *control_flow = ControlFlow::Poll; - } - - // Redraw screen. - self.display.draw(terminal, &self.message_buffer, &self.config, &self.search_state); - } - }); - - // Write ref tests to disk. - if self.config.ui_config.debug.ref_test { - self.write_ref_test_results(&terminal.lock()); - } - } - +impl input::Processor> { /// Handle events from glutin. /// /// Doesn't take self mutably due to borrow checking. - fn handle_event( - event: GlutinEvent<'_, Event>, - processor: &mut input::Processor>, - ) where - T: EventListener, - { + pub fn handle_event(&mut self, event: GlutinEvent<'_, Event>) { match event { - GlutinEvent::UserEvent(event) => match event { - Event::DprChanged(scale_factor, (width, height)) => { - let display_update_pending = &mut processor.ctx.display_update_pending; + GlutinEvent::UserEvent(Event { payload, .. }) => match payload { + EventType::DprChanged(scale_factor, (width, height)) => { + let display_update_pending = &mut self.ctx.display.pending_update; // Push current font to update its DPR. - let font = processor.ctx.config.ui_config.font.clone(); - display_update_pending.set_font(font.with_size(*processor.ctx.font_size)); + let font = self.ctx.config.ui_config.font.clone(); + display_update_pending.set_font(font.with_size(*self.ctx.font_size)); // Resize to event's dimensions, since no resize event is emitted on Wayland. display_update_pending.set_dimensions(PhysicalSize::new(width, height)); - processor.ctx.window().dpr = scale_factor; - *processor.ctx.dirty = true; + self.ctx.window().dpr = scale_factor; + *self.ctx.dirty = true; }, - Event::Message(message) => { - processor.ctx.message_buffer.push(message); - processor.ctx.display_update_pending.dirty = true; - *processor.ctx.dirty = true; + EventType::SearchNext => self.ctx.goto_match(None), + EventType::Scroll(scroll) => self.ctx.scroll(scroll), + EventType::BlinkCursor => { + self.ctx.display.cursor_hidden ^= true; + *self.ctx.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::BlinkCursor => { - processor.ctx.display.cursor_hidden ^= true; - *processor.ctx.dirty = true; + EventType::Message(message) => { + self.ctx.message_buffer.push(message); + self.ctx.display.pending_update.dirty = true; + *self.ctx.dirty = true; }, - Event::Terminal(event) => match event { + EventType::Terminal(event) => match event { TerminalEvent::Title(title) => { - let ui_config = &processor.ctx.config.ui_config; + let ui_config = &self.ctx.config.ui_config; if ui_config.window.dynamic_title { - processor.ctx.window().set_title(&title); + self.ctx.window().set_title(&title); } }, TerminalEvent::ResetTitle => { - let ui_config = &processor.ctx.config.ui_config; + let ui_config = &self.ctx.config.ui_config; if ui_config.window.dynamic_title { - processor.ctx.display.window.set_title(&ui_config.window.title); + self.ctx.display.window.set_title(&ui_config.window.title); } }, - TerminalEvent::Wakeup => *processor.ctx.dirty = true, + TerminalEvent::Wakeup => *self.ctx.dirty = true, TerminalEvent::Bell => { // Set window urgency. - if processor.ctx.terminal.mode().contains(TermMode::URGENCY_HINTS) { - let focused = processor.ctx.terminal.is_focused; - processor.ctx.window().set_urgent(!focused); + if self.ctx.terminal.mode().contains(TermMode::URGENCY_HINTS) { + let focused = self.ctx.terminal.is_focused; + self.ctx.window().set_urgent(!focused); } // Ring visual bell. - processor.ctx.display.visual_bell.ring(); + self.ctx.display.visual_bell.ring(); // Execute bell command. - if let Some(bell_command) = &processor.ctx.config.ui_config.bell.command { + if let Some(bell_command) = &self.ctx.config.ui_config.bell.command { start_daemon(bell_command.program(), bell_command.args()); } }, TerminalEvent::ClipboardStore(clipboard_type, content) => { - processor.ctx.clipboard.store(clipboard_type, content); + self.ctx.clipboard.store(clipboard_type, content); }, TerminalEvent::ClipboardLoad(clipboard_type, format) => { - let text = format(processor.ctx.clipboard.load(clipboard_type).as_str()); - processor.ctx.write_to_pty(text.into_bytes()); + let text = format(self.ctx.clipboard.load(clipboard_type).as_str()); + self.ctx.write_to_pty(text.into_bytes()); }, TerminalEvent::ColorRequest(index, format) => { - let text = format(processor.ctx.display.colors[index]); - processor.ctx.write_to_pty(text.into_bytes()); + let text = format(self.ctx.display.colors[index]); + self.ctx.write_to_pty(text.into_bytes()); }, - TerminalEvent::PtyWrite(text) => processor.ctx.write_to_pty(text.into_bytes()), - TerminalEvent::MouseCursorDirty => processor.reset_mouse_cursor(), + TerminalEvent::PtyWrite(text) => self.ctx.write_to_pty(text.into_bytes()), + TerminalEvent::MouseCursorDirty => self.reset_mouse_cursor(), TerminalEvent::Exit => (), - TerminalEvent::CursorBlinkingChange(_) => { - processor.ctx.update_cursor_blinking(); - }, + TerminalEvent::CursorBlinkingChange => self.ctx.update_cursor_blinking(), }, + EventType::ConfigReload(_) | EventType::CreateWindow => (), }, - GlutinEvent::RedrawRequested(_) => *processor.ctx.dirty = true, - GlutinEvent::WindowEvent { event, window_id, .. } => { + GlutinEvent::RedrawRequested(_) => *self.ctx.dirty = true, + GlutinEvent::WindowEvent { event, .. } => { match event { - WindowEvent::CloseRequested => processor.ctx.terminal.exit(), + WindowEvent::CloseRequested => self.ctx.terminal.exit(), WindowEvent::Resized(size) => { // Minimizing the window sends a Resize event with zero width and // height. But there's no need to ever actually resize to this. @@ -1283,53 +1084,49 @@ impl Processor { return; } - processor.ctx.display_update_pending.set_dimensions(size); - *processor.ctx.dirty = true; + self.ctx.display.pending_update.set_dimensions(size); + *self.ctx.dirty = true; }, WindowEvent::KeyboardInput { input, is_synthetic: false, .. } => { - processor.key_input(input); + self.key_input(input); }, - WindowEvent::ModifiersChanged(modifiers) => { - processor.modifiers_input(modifiers) - }, - WindowEvent::ReceivedCharacter(c) => processor.received_char(c), + WindowEvent::ModifiersChanged(modifiers) => self.modifiers_input(modifiers), + WindowEvent::ReceivedCharacter(c) => self.received_char(c), WindowEvent::MouseInput { state, button, .. } => { - processor.ctx.window().set_mouse_visible(true); - processor.mouse_input(state, button); - *processor.ctx.dirty = true; + self.ctx.window().set_mouse_visible(true); + self.mouse_input(state, button); + *self.ctx.dirty = true; }, WindowEvent::CursorMoved { position, .. } => { - processor.ctx.window().set_mouse_visible(true); - processor.mouse_moved(position); + self.ctx.window().set_mouse_visible(true); + self.mouse_moved(position); }, WindowEvent::MouseWheel { delta, phase, .. } => { - processor.ctx.window().set_mouse_visible(true); - processor.mouse_wheel_input(delta, phase); + self.ctx.window().set_mouse_visible(true); + self.mouse_wheel_input(delta, phase); }, WindowEvent::Focused(is_focused) => { - if window_id == processor.ctx.window().window_id() { - processor.ctx.terminal.is_focused = is_focused; - *processor.ctx.dirty = true; + self.ctx.terminal.is_focused = is_focused; + *self.ctx.dirty = true; - if is_focused { - processor.ctx.window().set_urgent(false); - } else { - processor.ctx.window().set_mouse_visible(true); - } - - processor.ctx.update_cursor_blinking(); - processor.on_focus_change(is_focused); + if is_focused { + self.ctx.window().set_urgent(false); + } else { + self.ctx.window().set_mouse_visible(true); } + + self.ctx.update_cursor_blinking(); + self.on_focus_change(is_focused); }, WindowEvent::DroppedFile(path) => { let path: String = path.to_string_lossy().into(); - processor.ctx.write_to_pty((path + " ").into_bytes()); + self.ctx.write_to_pty((path + " ").into_bytes()); }, WindowEvent::CursorLeft { .. } => { - processor.ctx.mouse.inside_text_area = false; + self.ctx.mouse.inside_text_area = false; - if processor.ctx.display().highlighted_hint.is_some() { - *processor.ctx.dirty = true; + if self.ctx.display().highlighted_hint.is_some() { + *self.ctx.dirty = true; } }, WindowEvent::KeyboardInput { is_synthetic: true, .. } @@ -1354,6 +1151,194 @@ impl Processor { | GlutinEvent::LoopDestroyed => (), } } +} + +/// The event processor. +/// +/// Stores some state from received events and dispatches actions when they are +/// triggered. +pub struct Processor { + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + wayland_event_queue: Option, + windows: HashMap, + cli_options: CliOptions, + config: Config, +} + +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: Config, + cli_options: CliOptions, + _event_loop: &EventLoop, + ) -> Processor { + // Initialize Wayland event queue, to handle Wayland callbacks. + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + let wayland_event_queue = _event_loop.wayland_display().map(|display| { + let display = unsafe { WaylandDisplay::from_external_display(display as _) }; + display.create_event_queue() + }); + + Processor { + windows: HashMap::new(), + cli_options, + config, + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + wayland_event_queue, + } + } + + /// Create a new terminal window. + pub fn create_window( + &mut self, + event_loop: &EventLoopWindowTarget, + proxy: EventLoopProxy, + ) -> Result<(), Box> { + let window_context = WindowContext::new( + &self.config, + event_loop, + proxy, + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + self.wayland_event_queue.as_ref(), + )?; + self.windows.insert(window_context.id(), window_context); + Ok(()) + } + + /// Run the event loop. + pub fn run(&mut self, mut event_loop: EventLoop) { + let proxy = event_loop.create_proxy(); + let mut scheduler = Scheduler::new(proxy.clone()); + + // NOTE: Since this takes a pointer to the winit event loop, it MUST be dropped first. + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + let mut clipboard = unsafe { Clipboard::new(event_loop.wayland_display()) }; + #[cfg(any(not(feature = "wayland"), target_os = "macos", windows))] + let mut clipboard = Clipboard::new(); + + event_loop.run_return(|event, event_loop, control_flow| { + if self.config.ui_config.debug.print_events { + info!("glutin event: {:?}", event); + } + + // Ignore all events we do not care about. + if Self::skip_event(&event) { + return; + } + + match event { + // Check for shutdown. + GlutinEvent::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.ui_config.debug.ref_test { + window_context.write_ref_test_results(); + } + + *control_flow = ControlFlow::Exit; + } + }, + // Process all pending events. + GlutinEvent::RedrawEventsCleared => { + *control_flow = match scheduler.update() { + Some(instant) => ControlFlow::WaitUntil(instant), + None => ControlFlow::Wait, + }; + + // Check for pending frame callbacks on Wayland. + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + if let Some(wayland_event_queue) = self.wayland_event_queue.as_mut() { + wayland_event_queue + .dispatch_pending(&mut (), |_, _, _| {}) + .expect("failed to dispatch wayland event queue"); + } + + // Dispatch event to all windows. + for window_context in self.windows.values_mut() { + window_context.handle_event( + event_loop, + &proxy, + &mut self.config, + &mut clipboard, + &mut scheduler, + GlutinEvent::RedrawEventsCleared, + ); + } + }, + // Process config update. + GlutinEvent::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, &self.cli_options) { + let old_config = mem::replace(&mut self.config, config); + + for window_context in self.windows.values_mut() { + window_context.update_config(&old_config, &self.config); + } + } + }, + // Create a new terminal window. + GlutinEvent::UserEvent(Event { payload: EventType::CreateWindow, .. }) => { + if let Err(err) = self.create_window(event_loop, proxy.clone()) { + error!("Could not open window: {:?}", err); + } + }, + // Process events affecting all windows. + GlutinEvent::UserEvent(event @ Event { window_id: None, .. }) => { + for window_context in self.windows.values_mut() { + window_context.handle_event( + event_loop, + &proxy, + &mut self.config, + &mut clipboard, + &mut scheduler, + event.clone().into(), + ); + } + }, + // Process window-specific events. + GlutinEvent::WindowEvent { window_id, .. } + | GlutinEvent::UserEvent(Event { window_id: Some(window_id), .. }) + | GlutinEvent::RedrawRequested(window_id) => { + if let Some(window_context) = self.windows.get_mut(&window_id) { + window_context.handle_event( + event_loop, + &proxy, + &mut self.config, + &mut clipboard, + &mut scheduler, + event, + ); + } + }, + _ => (), + } + }); + } /// Check if an event is irrelevant and can be skipped. fn skip_event(event: &GlutinEvent<'_, Event>) -> bool { @@ -1377,163 +1362,27 @@ impl Processor { _ => false, } } - - /// Reload the configuration files from disk. - fn reload_config(path: &Path, processor: &mut input::Processor>) - where - T: EventListener, - { - 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(path, processor.ctx.cli_options) { - Ok(config) => config, - Err(_) => return, - }; - - processor.ctx.display.update_config(&config); - processor.ctx.terminal.update_config(&config); - - // Reload cursor if its thickness has changed. - if (processor.ctx.config.cursor.thickness() - config.cursor.thickness()).abs() - > f32::EPSILON - { - processor.ctx.display_update_pending.set_cursor_dirty(); - } - - if processor.ctx.config.ui_config.font != config.ui_config.font { - // Do not update font size if it has been changed at runtime. - if *processor.ctx.font_size == processor.ctx.config.ui_config.font.size() { - *processor.ctx.font_size = config.ui_config.font.size(); - } - - let font = config.ui_config.font.clone().with_size(*processor.ctx.font_size); - processor.ctx.display_update_pending.set_font(font); - } - - // Update display if padding options were changed. - let window_config = &processor.ctx.config.ui_config.window; - if window_config.padding(1.) != config.ui_config.window.padding(1.) - || window_config.dynamic_padding != config.ui_config.window.dynamic_padding - { - processor.ctx.display_update_pending.dirty = true; - } - - // Live title reload. - if !config.ui_config.window.dynamic_title - || processor.ctx.config.ui_config.window.title != config.ui_config.window.title - { - processor.ctx.window().set_title(&config.ui_config.window.title); - } - - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - if processor.ctx.event_loop.is_wayland() { - processor.ctx.window().set_wayland_theme(&config.ui_config.colors); - } - - // Set subpixel anti-aliasing. - #[cfg(target_os = "macos")] - crossfont::set_font_smoothing(config.ui_config.font.use_thin_strokes); - - // Disable shadows for transparent windows on macOS. - #[cfg(target_os = "macos")] - processor.ctx.window().set_has_shadow(config.ui_config.window_opacity() >= 1.0); - - // Update hint keys. - processor.ctx.display.hint_state.update_alphabet(config.ui_config.hints.alphabet()); - - *processor.ctx.config = config; - - // Update cursor blinking. - processor.ctx.update_cursor_blinking(); - - *processor.ctx.dirty = true; - } - - /// Submit the pending changes to the `Display`. - fn submit_display_update( - &mut self, - terminal: &mut Term, - 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 = if terminal.mode().contains(TermMode::VI) { - terminal.vi_mode_cursor.point.line == num_lines - 1 - } else { - self.search_state.direction == Direction::Left - }; - - self.display.handle_update( - terminal, - &mut self.notifier, - &self.message_buffer, - self.search_state.history_index.is_some(), - &self.config, - display_update_pending, - ); - - let new_is_searching = self.search_state.history_index.is_some(); - if !old_is_searching && new_is_searching { - // Scroll on search start to make sure origin is visible with minimal viewport motion. - 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(&self, terminal: &Term) { - // Dump grid state. - let mut grid = terminal.grid().clone(); - grid.initialize_all(); - grid.truncate(); - - let serialized_grid = json::to_string(&grid).expect("serialize grid"); - - let serialized_size = json::to_string(&self.display.size_info).expect("serialize size"); - - let serialized_config = format!("{{\"history_size\":{}}}", grid.history_size()); - - File::create("./grid.json") - .and_then(|mut f| f.write_all(serialized_grid.as_bytes())) - .expect("write grid.json"); - - File::create("./size.json") - .and_then(|mut f| f.write_all(serialized_size.as_bytes())) - .expect("write size.json"); - - File::create("./config.json") - .and_then(|mut f| f.write_all(serialized_config.as_bytes())) - .expect("write config.json"); - } } #[derive(Debug, Clone)] -pub struct EventProxy(EventLoopProxy); +pub struct EventProxy { + proxy: EventLoopProxy, + window_id: WindowId, +} impl EventProxy { - pub fn new(proxy: EventLoopProxy) -> Self { - EventProxy(proxy) + pub fn new(proxy: EventLoopProxy, window_id: WindowId) -> Self { + Self { proxy, window_id } } /// Send an event to the event loop. - pub fn send_event(&self, event: Event) { - let _ = self.0.send_event(event); + 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.0.send_event(Event::Terminal(event)); + let _ = self.proxy.send_event(Event::new(event.into(), self.window_id)); } } diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs index 98a1b723..ca5742ee 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -34,9 +34,9 @@ use crate::daemon::start_daemon; use crate::display::hint::HintMatch; use crate::display::window::Window; use crate::display::Display; -use crate::event::{ClickState, Event, Mouse, TYPING_SEARCH_DELAY}; +use crate::event::{ClickState, Event, EventType, Mouse, TYPING_SEARCH_DELAY}; use crate::message_bar::{self, Message}; -use crate::scheduler::{Scheduler, TimerId}; +use crate::scheduler::{Scheduler, TimerId, Topic}; /// Font size change interval. pub const FONT_SIZE_STEP: f32 = 0.5; @@ -80,6 +80,7 @@ pub trait ActionContext { fn terminal(&self) -> &Term; fn terminal_mut(&mut self) -> &mut Term; fn spawn_new_instance(&mut self) {} + fn create_new_window(&mut self) {} fn change_font_size(&mut self, _delta: f32) {} fn reset_font_size(&mut self) {} fn pop_message(&mut self) {} @@ -319,6 +320,7 @@ impl Execute for Action { Action::ClearHistory => ctx.terminal_mut().clear_screen(ClearMode::Saved), Action::ClearLogNotice => ctx.pop_message(), Action::SpawnNewInstance => ctx.spawn_new_instance(), + Action::CreateNewWindow => ctx.create_new_window(), Action::ReceiveChar | Action::None => (), } } @@ -594,7 +596,8 @@ impl> Processor { } self.ctx.display().highlighted_hint = hint; - self.ctx.scheduler_mut().unschedule(TimerId::SelectionScrolling); + let timer_id = TimerId::new(Topic::SelectionScrolling, self.ctx.window().id()); + self.ctx.scheduler_mut().unschedule(timer_id); // Copy selection on release, to prevent flooding the display server. self.ctx.copy_selection(ClipboardType::Selection); @@ -731,8 +734,10 @@ impl> Processor { // Reset search delay when the user is still typing. if self.ctx.search_active() { - if let Some(timer) = self.ctx.scheduler_mut().get_mut(TimerId::DelayedSearch) { - timer.deadline = Instant::now() + TYPING_SEARCH_DELAY; + let timer_id = TimerId::new(Topic::DelayedSearch, self.ctx.window().id()); + let scheduler = self.ctx.scheduler_mut(); + if let Some(timer) = scheduler.unschedule(timer_id) { + scheduler.schedule(timer.event, TYPING_SEARCH_DELAY, false, timer.id); } } @@ -911,6 +916,7 @@ impl> Processor { fn update_selection_scrolling(&mut self, mouse_y: i32) { let dpr = self.ctx.window().dpr; let size = self.ctx.size_info(); + let window_id = self.ctx.window().id(); let scheduler = self.ctx.scheduler_mut(); // Scale constants by DPI. @@ -928,26 +934,18 @@ impl> Processor { } else if mouse_y >= start_bottom { start_bottom - mouse_y - step } else { - scheduler.unschedule(TimerId::SelectionScrolling); + scheduler.unschedule(TimerId::new(Topic::SelectionScrolling, window_id)); return; }; // Scale number of lines scrolled based on distance to boundary. let delta = delta as i32 / step as i32; - let event = Event::Scroll(Scroll::Delta(delta)); + let event = Event::new(EventType::Scroll(Scroll::Delta(delta)), Some(window_id)); // Schedule event. - match scheduler.get_mut(TimerId::SelectionScrolling) { - Some(timer) => timer.event = event.into(), - None => { - scheduler.schedule( - event.into(), - SELECTION_SCROLLING_INTERVAL, - true, - TimerId::SelectionScrolling, - ); - }, - } + let timer_id = TimerId::new(Topic::SelectionScrolling, window_id); + scheduler.unschedule(timer_id); + scheduler.schedule(event, SELECTION_SCROLLING_INTERVAL, true, timer_id); } } @@ -1106,7 +1104,7 @@ mod tests { ..Mouse::default() }; - let mut message_buffer = MessageBuffer::new(); + let mut message_buffer = MessageBuffer::default(); let context = ActionContext { terminal: &mut terminal, diff --git a/alacritty/src/ipc.rs b/alacritty/src/ipc.rs new file mode 100644 index 00000000..02aaf85f --- /dev/null +++ b/alacritty/src/ipc.rs @@ -0,0 +1,145 @@ +//! Alacritty socket IPC. + +use std::ffi::OsStr; +use std::io::{Error as IoError, ErrorKind, Result as IoResult}; +use std::os::unix::net::UnixDatagram; +use std::path::PathBuf; +use std::{env, fs, process}; + +use glutin::event_loop::EventLoopProxy; +use log::warn; + +use alacritty_terminal::thread; + +use crate::cli::Options; +use crate::event::{Event, EventType}; + +/// IPC socket message for creating a new window. +pub const SOCKET_MESSAGE_CREATE_WINDOW: [u8; 1] = [1]; + +/// Environment variable name for the IPC socket path. +const ALACRITTY_SOCKET_ENV: &str = "ALACRITTY_SOCKET"; + +/// Create an IPC socket. +pub fn spawn_ipc_socket(options: &Options, event_proxy: EventLoopProxy) -> Option { + // Create the IPC socket and export its path as env variable if necessary. + let socket_path = options.socket.clone().unwrap_or_else(|| { + let mut path = socket_dir(); + path.push(format!("{}-{}.sock", socket_prefix(), process::id())); + path + }); + env::set_var(ALACRITTY_SOCKET_ENV, socket_path.as_os_str()); + + let socket = match UnixDatagram::bind(&socket_path) { + Ok(socket) => socket, + Err(err) => { + warn!("Unable to create socket: {:?}", err); + return None; + }, + }; + + // Spawn a thread to listen on the IPC socket. + thread::spawn_named("socket listener", move || { + // Accept up to 2 bytes to ensure only one byte is received. + // This ensures forward-compatibility. + let mut buf = [0; 2]; + + while let Ok(received) = socket.recv(&mut buf) { + if buf[..received] == SOCKET_MESSAGE_CREATE_WINDOW { + let _ = event_proxy.send_event(Event::new(EventType::CreateWindow, None)); + } + } + }); + + Some(socket_path) +} + +/// Send a message to the active Alacritty socket. +pub fn send_message(socket: Option, message: &[u8]) -> IoResult<()> { + let socket = find_socket(socket)?; + socket.send(message)?; + Ok(()) +} + +/// Directory for the IPC socket file. +#[cfg(not(target_os = "macos"))] +fn socket_dir() -> PathBuf { + xdg::BaseDirectories::with_prefix("alacritty") + .ok() + .and_then(|xdg| xdg.get_runtime_directory().map(ToOwned::to_owned).ok()) + .and_then(|path| fs::create_dir_all(&path).map(|_| path).ok()) + .unwrap_or_else(env::temp_dir) +} + +/// Directory for the IPC socket file. +#[cfg(target_os = "macos")] +fn socket_dir() -> PathBuf { + env::temp_dir() +} + +/// Find the IPC socket path. +fn find_socket(socket_path: Option) -> IoResult { + let socket = UnixDatagram::unbound()?; + + // Handle --socket CLI override. + if let Some(socket_path) = socket_path { + // Ensure we inform the user about an invalid path. + socket.connect(&socket_path).map_err(|err| { + let message = format!("invalid socket path {:?}", socket_path); + IoError::new(err.kind(), message) + })?; + } + + // Handle environment variable. + if let Ok(path) = env::var(ALACRITTY_SOCKET_ENV) { + let socket_path = PathBuf::from(path); + if socket.connect(&socket_path).is_ok() { + return Ok(socket); + } + } + + // Search for sockets files. + for entry in fs::read_dir(socket_dir())?.filter_map(|entry| entry.ok()) { + let path = entry.path(); + + // Skip files that aren't Alacritty sockets. + let socket_prefix = socket_prefix(); + if path + .file_name() + .and_then(OsStr::to_str) + .filter(|file| file.starts_with(&socket_prefix) && file.ends_with(".sock")) + .is_none() + { + continue; + } + + // Attempt to connect to the socket. + match socket.connect(&path) { + Ok(_) => return Ok(socket), + // Delete orphan sockets. + Err(error) if error.kind() == ErrorKind::ConnectionRefused => { + let _ = fs::remove_file(&path); + }, + // Ignore other errors like permission issues. + Err(_) => (), + } + } + + Err(IoError::new(ErrorKind::NotFound, "no socket found")) +} + +/// File prefix matching all available sockets. +/// +/// This prefix will include display server information to allow for environments with multiple +/// display servers running for the same user. +#[cfg(not(target_os = "macos"))] +fn socket_prefix() -> String { + let display = env::var("WAYLAND_DISPLAY").or_else(|_| env::var("DISPLAY")).unwrap_or_default(); + format!("Alacritty-{}", display) +} + +/// File prefix matching all available sockets. +#[cfg(target_os = "macos")] +fn socket_prefix() -> String { + String::from("Alacritty") +} diff --git a/alacritty/src/logging.rs b/alacritty/src/logging.rs index 8751c91e..56ed4ab5 100644 --- a/alacritty/src/logging.rs +++ b/alacritty/src/logging.rs @@ -15,7 +15,7 @@ use glutin::event_loop::EventLoopProxy; use log::{self, Level, LevelFilter}; use crate::cli::Options; -use crate::event::Event; +use crate::event::{Event, EventType}; use crate::message_bar::{Message, MessageType}; /// Name for the environment variable containing the log file's path. @@ -61,6 +61,12 @@ impl Logger { /// Log a record to the message bar. fn message_bar_log(&self, record: &log::Record<'_>, logfile_path: &str) { + let message_type = match record.level() { + Level::Error => MessageType::Error, + Level::Warn => MessageType::Warning, + _ => return, + }; + let event_proxy = match self.event_proxy.lock() { Ok(event_proxy) => event_proxy, Err(_) => return, @@ -78,16 +84,11 @@ impl Logger { env_var, record.args(), ); - let message_type = match record.level() { - Level::Error => MessageType::Error, - Level::Warn => MessageType::Warning, - _ => unreachable!(), - }; let mut message = Message::new(message, message_type); message.set_target(record.target().to_owned()); - let _ = event_proxy.send_event(Event::Message(message)); + let _ = event_proxy.send_event(Event::new(EventType::Message(message), None)); } } @@ -113,10 +114,8 @@ impl log::Log for Logger { // Write to logfile. let _ = logfile.write_all(message.as_ref()); - // Write to message bar. - if record.level() <= Level::Warn { - self.message_bar_log(record, &logfile.path.to_string_lossy()); - } + // Log relevant entries to message bar. + self.message_bar_log(record, &logfile.path.to_string_lossy()); } // Write to stdout. diff --git a/alacritty/src/main.rs b/alacritty/src/main.rs index 488a67bc..74e27b84 100644 --- a/alacritty/src/main.rs +++ b/alacritty/src/main.rs @@ -14,20 +14,16 @@ compile_error!(r#"at least one of the "x11"/"wayland" features must be enabled"# #[cfg(target_os = "macos")] use std::env; -use std::error::Error; -use std::fs; use std::io::{self, Write}; -use std::sync::Arc; +use std::path::PathBuf; +use std::string::ToString; +use std::{fs, process}; use glutin::event_loop::EventLoop as GlutinEventLoop; -use log::{error, info}; +use log::info; #[cfg(windows)] use winapi::um::wincon::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS}; -use alacritty_terminal::event_loop::{self, EventLoop, Msg}; -use alacritty_terminal::grid::Dimensions; -use alacritty_terminal::sync::FairMutex; -use alacritty_terminal::term::Term; use alacritty_terminal::tty; mod cli; @@ -37,6 +33,8 @@ mod daemon; mod display; mod event; mod input; +#[cfg(unix)] +mod ipc; mod logging; #[cfg(target_os = "macos")] mod macos; @@ -45,6 +43,7 @@ mod message_bar; mod panic; mod renderer; mod scheduler; +mod window_context; mod gl { #![allow(clippy::all)] @@ -52,12 +51,14 @@ mod gl { } use crate::cli::Options; +#[cfg(unix)] +use crate::cli::{MessageOptions, Subcommands}; use crate::config::{monitor, Config}; -use crate::display::Display; -use crate::event::{Event, EventProxy, Processor}; +use crate::event::{Event, Processor}; +#[cfg(unix)] +use crate::ipc::SOCKET_MESSAGE_CREATE_WINDOW; #[cfg(target_os = "macos")] use crate::macos::locale; -use crate::message_bar::MessageBuffer; fn main() { #[cfg(windows)] @@ -74,6 +75,61 @@ fn main() { // Load command line options. let options = Options::new(); + #[cfg(unix)] + let result = match options.subcommands { + Some(Subcommands::Msg(options)) => msg(options), + None => alacritty(options), + }; + + #[cfg(not(unix))] + let result = alacritty(options); + + // Handle command failure. + if let Err(err) = result { + eprintln!("Error: {}", err); + process::exit(1); + } +} + +/// `msg` subcommand entrypoint. +#[cfg(unix)] +fn msg(options: MessageOptions) -> Result<(), String> { + ipc::send_message(options.socket, &SOCKET_MESSAGE_CREATE_WINDOW).map_err(|err| err.to_string()) +} + +/// Temporary files stored for Alacritty. +/// +/// This stores temporary files to automate their destruction through its `Drop` implementation. +struct TemporaryFiles { + #[cfg(unix)] + socket_path: Option, + log_file: Option, +} + +impl Drop for TemporaryFiles { + fn drop(&mut self) { + // Clean up the IPC socket file. + #[cfg(unix)] + if let Some(socket_path) = &self.socket_path { + let _ = fs::remove_file(socket_path); + } + + // Clean up logfile. + if let Some(log_file) = &self.log_file { + if fs::remove_file(log_file).is_ok() { + let _ = writeln!(io::stdout(), "Deleted log file at \"{}\"", log_file.display()); + } + } + } +} + +/// Run main Alacritty entrypoint. +/// +/// Creates a window, the terminal state, PTY, I/O event loop, input processor, +/// config change monitor, and runs the main display loop. +fn alacritty(options: Options) -> Result<(), String> { + info!("Welcome to Alacritty"); + // Setup glutin event loop. let window_event_loop = GlutinEventLoop::::with_user_event(); @@ -83,142 +139,71 @@ fn main() { // Load configuration file. let config = config::load(&options); + log_config_path(&config); // Update the log level from config. log::set_max_level(config.ui_config.debug.log_level); - // Switch to home directory. - #[cfg(target_os = "macos")] - env::set_current_dir(dirs::home_dir().unwrap()).unwrap(); - // Set locale. - #[cfg(target_os = "macos")] - locale::set_locale_environment(); - - // Store if log file should be deleted before moving config. - let persistent_logging = config.ui_config.debug.persistent_logging; - - // Run Alacritty. - if let Err(err) = run(window_event_loop, config, options) { - error!("Alacritty encountered an unrecoverable error:\n\n\t{}\n", err); - std::process::exit(1); - } - - // Clean up logfile. - if let Some(log_file) = log_file { - if !persistent_logging && fs::remove_file(&log_file).is_ok() { - let _ = writeln!(io::stdout(), "Deleted log file at \"{}\"", log_file.display()); - } - } -} - -/// Run Alacritty. -/// -/// Creates a window, the terminal state, PTY, I/O event loop, input processor, -/// config change monitor, and runs the main display loop. -fn run( - window_event_loop: GlutinEventLoop, - config: Config, - options: Options, -) -> Result<(), Box> { - info!("Welcome to Alacritty"); - - // Log the configuration paths. - log_config_path(&config); - // Set environment variables. tty::setup_env(&config); - let event_proxy = EventProxy::new(window_event_loop.create_proxy()); + // Switch to home directory. + #[cfg(target_os = "macos")] + env::set_current_dir(dirs::home_dir().unwrap()).unwrap(); - // Create a display. - // - // The display manages a window and can draw the terminal. - let display = Display::new(&config, &window_event_loop)?; - - info!( - "PTY dimensions: {:?} x {:?}", - display.size_info.screen_lines(), - display.size_info.columns() - ); - - // Create the terminal. - // - // This object contains all of the state about what's being displayed. It's - // wrapped in a clonable mutex since both the I/O loop and display need to - // access it. - let terminal = Term::new(&config, display.size_info, event_proxy.clone()); - let terminal = Arc::new(FairMutex::new(terminal)); - - // Create the PTY. - // - // The PTY forks a process to run the shell on the slave side of the - // pseudoterminal. A file descriptor for the master side is retained for - // reading/writing to the shell. - let pty = tty::new(&config, &display.size_info, display.window.x11_window_id()); - - // Create the pseudoterminal I/O loop. - // - // PTY I/O is ran on another thread as to not occupy cycles used by the - // renderer and input processing. Note that access to the terminal state is - // synchronized since the I/O loop updates the state, and the display - // consumes it periodically. - let event_loop = EventLoop::new( - Arc::clone(&terminal), - event_proxy.clone(), - pty, - config.hold, - config.ui_config.debug.ref_test, - ); - - // The event loop channel allows write requests from the event processor - // to be sent to the pty loop and ultimately written to the pty. - let loop_tx = event_loop.channel(); + // Set macOS locale. + #[cfg(target_os = "macos")] + locale::set_locale_environment(); // Create a config monitor when config was loaded from path. // // The monitor watches the config file for changes and reloads it. Pending // config changes are processed in the main loop. if config.ui_config.live_config_reload { - monitor::watch(config.ui_config.config_paths.clone(), event_proxy); + monitor::watch(config.ui_config.config_paths.clone(), window_event_loop.create_proxy()); } - // Setup storage for message UI. - let message_buffer = MessageBuffer::new(); + // Create the IPC socket listener. + #[cfg(unix)] + let socket_path = if config.ui_config.ipc_socket { + ipc::spawn_ipc_socket(&options, window_event_loop.create_proxy()) + } else { + None + }; + + // Setup automatic RAII cleanup for our files. + let log_cleanup = log_file.filter(|_| !config.ui_config.debug.persistent_logging); + let _files = TemporaryFiles { + #[cfg(unix)] + socket_path, + log_file: log_cleanup, + }; // Event processor. - let mut processor = Processor::new( - event_loop::Notifier(loop_tx.clone()), - message_buffer, - config, - display, - options, - ); + let mut processor = Processor::new(config, options, &window_event_loop); - // Kick off the I/O thread. - let io_thread = event_loop.spawn(); + // Create the first Alacritty window. + let proxy = window_event_loop.create_proxy(); + processor.create_window(&window_event_loop, proxy).map_err(|err| err.to_string())?; info!("Initialisation complete"); // Start event loop and block until shutdown. - processor.run(terminal, window_event_loop); + processor.run(window_event_loop); // This explicit drop is needed for Windows, ConPTY backend. Otherwise a deadlock can occur. // The cause: - // - Drop for ConPTY will deadlock if the conout pipe has already been dropped. - // - The conout pipe is dropped when the io_thread is joined below (io_thread owns PTY). - // - ConPTY is dropped when the last of processor and io_thread are dropped, because both of - // them own an Arc. + // - Drop for ConPTY will deadlock if the conout pipe has already been dropped + // - ConPTY is dropped when the last of processor and window context are dropped, because both + // of them own an Arc // - // The fix is to ensure that processor is dropped first. That way, when io_thread (i.e. PTY) - // is dropped, it can ensure ConPTY is dropped before the conout pipe in the PTY drop order. + // The fix is to ensure that processor is dropped first. That way, when window context (i.e. + // PTY) is dropped, it can ensure ConPTY is dropped before the conout pipe in the PTY drop + // order. // // FIXME: Change PTY API to enforce the correct drop order with the typesystem. drop(processor); - // Shutdown PTY parser event loop. - loop_tx.send(Msg::Shutdown).expect("Error sending shutdown to PTY event loop"); - io_thread.join().expect("join io thread"); - // FIXME patch notify library to have a shutdown method. // config_reloader.join().ok(); @@ -229,7 +214,6 @@ fn run( } info!("Goodbye"); - Ok(()) } diff --git a/alacritty/src/message_bar.rs b/alacritty/src/message_bar.rs index 72e6f354..a0c821ae 100644 --- a/alacritty/src/message_bar.rs +++ b/alacritty/src/message_bar.rs @@ -141,11 +141,6 @@ pub struct MessageBuffer { } impl MessageBuffer { - /// Create new message buffer. - pub fn new() -> MessageBuffer { - MessageBuffer { messages: VecDeque::new() } - } - /// Check if there are any messages queued. #[inline] pub fn is_empty(&self) -> bool { @@ -196,7 +191,7 @@ mod tests { #[test] fn appends_close_button() { let input = "a"; - let mut message_buffer = MessageBuffer::new(); + let mut message_buffer = MessageBuffer::default(); message_buffer.push(Message::new(input.into(), MessageType::Error)); let size = SizeInfo::new(7., 10., 1., 1., 0., 0., false); @@ -208,7 +203,7 @@ mod tests { #[test] fn multiline_close_button_first_line() { let input = "fo\nbar"; - let mut message_buffer = MessageBuffer::new(); + let mut message_buffer = MessageBuffer::default(); message_buffer.push(Message::new(input.into(), MessageType::Error)); let size = SizeInfo::new(6., 10., 1., 1., 0., 0., false); @@ -220,7 +215,7 @@ mod tests { #[test] fn splits_on_newline() { let input = "a\nb"; - let mut message_buffer = MessageBuffer::new(); + let mut message_buffer = MessageBuffer::default(); message_buffer.push(Message::new(input.into(), MessageType::Error)); let size = SizeInfo::new(6., 10., 1., 1., 0., 0., false); @@ -232,7 +227,7 @@ mod tests { #[test] fn splits_on_length() { let input = "foobar1"; - let mut message_buffer = MessageBuffer::new(); + let mut message_buffer = MessageBuffer::default(); message_buffer.push(Message::new(input.into(), MessageType::Error)); let size = SizeInfo::new(6., 10., 1., 1., 0., 0., false); @@ -244,7 +239,7 @@ mod tests { #[test] fn empty_with_shortterm() { let input = "foobar"; - let mut message_buffer = MessageBuffer::new(); + let mut message_buffer = MessageBuffer::default(); message_buffer.push(Message::new(input.into(), MessageType::Error)); let size = SizeInfo::new(6., 0., 1., 1., 0., 0., false); @@ -256,7 +251,7 @@ mod tests { #[test] fn truncates_long_messages() { let input = "hahahahahahahahahahaha truncate this because it's too long for the term"; - let mut message_buffer = MessageBuffer::new(); + let mut message_buffer = MessageBuffer::default(); message_buffer.push(Message::new(input.into(), MessageType::Error)); let size = SizeInfo::new(22., (MIN_FREE_LINES + 2) as f32, 1., 1., 0., 0., false); @@ -271,7 +266,7 @@ mod tests { #[test] fn hide_button_when_too_narrow() { let input = "ha"; - let mut message_buffer = MessageBuffer::new(); + let mut message_buffer = MessageBuffer::default(); message_buffer.push(Message::new(input.into(), MessageType::Error)); let size = SizeInfo::new(2., 10., 1., 1., 0., 0., false); @@ -283,7 +278,7 @@ mod tests { #[test] fn hide_truncated_when_too_narrow() { let input = "hahahahahahahahaha"; - let mut message_buffer = MessageBuffer::new(); + let mut message_buffer = MessageBuffer::default(); message_buffer.push(Message::new(input.into(), MessageType::Error)); let size = SizeInfo::new(2., (MIN_FREE_LINES + 2) as f32, 1., 1., 0., 0., false); @@ -295,7 +290,7 @@ mod tests { #[test] fn add_newline_for_button() { let input = "test"; - let mut message_buffer = MessageBuffer::new(); + let mut message_buffer = MessageBuffer::default(); message_buffer.push(Message::new(input.into(), MessageType::Error)); let size = SizeInfo::new(5., 10., 1., 1., 0., 0., false); @@ -306,7 +301,7 @@ mod tests { #[test] fn remove_target() { - let mut message_buffer = MessageBuffer::new(); + let mut message_buffer = MessageBuffer::default(); for i in 0..10 { let mut msg = Message::new(i.to_string(), MessageType::Error); if i % 2 == 0 && i < 5 { @@ -329,7 +324,7 @@ mod tests { #[test] fn pop() { - let mut message_buffer = MessageBuffer::new(); + let mut message_buffer = MessageBuffer::default(); let one = Message::new(String::from("one"), MessageType::Error); message_buffer.push(one.clone()); let two = Message::new(String::from("two"), MessageType::Warning); @@ -345,7 +340,7 @@ mod tests { #[test] fn wrap_on_words() { let input = "a\nbc defg"; - let mut message_buffer = MessageBuffer::new(); + let mut message_buffer = MessageBuffer::default(); message_buffer.push(Message::new(input.into(), MessageType::Error)); let size = SizeInfo::new(5., 10., 1., 1., 0., 0., false); @@ -361,7 +356,7 @@ mod tests { #[test] fn wrap_with_unicode() { let input = "ab\nc 👩d fgh"; - let mut message_buffer = MessageBuffer::new(); + let mut message_buffer = MessageBuffer::default(); message_buffer.push(Message::new(input.into(), MessageType::Error)); let size = SizeInfo::new(7., 10., 1., 1., 0., 0., false); @@ -377,7 +372,7 @@ mod tests { #[test] fn strip_whitespace_at_linebreak() { let input = "\n0 1 2 3"; - let mut message_buffer = MessageBuffer::new(); + let mut message_buffer = MessageBuffer::default(); message_buffer.push(Message::new(input.into(), MessageType::Error)); let size = SizeInfo::new(3., 10., 1., 1., 0., 0., false); @@ -388,7 +383,7 @@ mod tests { #[test] fn remove_duplicates() { - let mut message_buffer = MessageBuffer::new(); + let mut message_buffer = MessageBuffer::default(); for _ in 0..10 { let msg = Message::new(String::from("test"), MessageType::Error); message_buffer.push(msg); diff --git a/alacritty/src/renderer/mod.rs b/alacritty/src/renderer/mod.rs index 23be70be..b9ec728c 100644 --- a/alacritty/src/renderer/mod.rs +++ b/alacritty/src/renderer/mod.rs @@ -673,11 +673,7 @@ impl QuadRenderer { gl::BlendFunc(gl::SRC1_COLOR, gl::ONE_MINUS_SRC1_COLOR); // Restore viewport with padding. - let padding_x = size_info.padding_x() as i32; - let padding_y = size_info.padding_y() as i32; - let width = size_info.width() as i32; - let height = size_info.height() as i32; - gl::Viewport(padding_x, padding_y, width - 2 * padding_x, height - 2 * padding_y); + self.set_viewport(&size_info); } } @@ -730,15 +726,9 @@ impl QuadRenderer { }) } - pub fn resize(&mut self, size: &SizeInfo) { - // Viewport. + pub fn resize(&self, size: &SizeInfo) { unsafe { - gl::Viewport( - size.padding_x() as i32, - size.padding_y() as i32, - size.width() as i32 - 2 * size.padding_x() as i32, - size.height() as i32 - 2 * size.padding_y() as i32, - ); + self.set_viewport(size); // Update projection. gl::UseProgram(self.program.id); @@ -751,6 +741,19 @@ impl QuadRenderer { gl::UseProgram(0); } } + + /// Set the viewport for cell rendering. + #[inline] + pub fn set_viewport(&self, size: &SizeInfo) { + unsafe { + gl::Viewport( + size.padding_x() as i32, + size.padding_y() as i32, + size.width() as i32 - 2 * size.padding_x() as i32, + size.height() as i32 - 2 * size.padding_y() as i32, + ); + } + } } impl Drop for QuadRenderer { diff --git a/alacritty/src/scheduler.rs b/alacritty/src/scheduler.rs index 5e454141..924f5904 100644 --- a/alacritty/src/scheduler.rs +++ b/alacritty/src/scheduler.rs @@ -3,15 +3,27 @@ use std::collections::VecDeque; use std::time::{Duration, Instant}; -use glutin::event::Event as GlutinEvent; +use glutin::event_loop::EventLoopProxy; +use glutin::window::WindowId; -use crate::event::Event as AlacrittyEvent; - -type Event = GlutinEvent<'static, AlacrittyEvent>; +use crate::event::Event; /// ID uniquely identifying a timer. #[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum TimerId { +pub struct TimerId { + topic: Topic, + window_id: WindowId, +} + +impl TimerId { + pub fn new(topic: Topic, window_id: WindowId) -> Self { + Self { topic, window_id } + } +} + +/// Available timer topics. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Topic { SelectionScrolling, DelayedSearch, BlinkCursor, @@ -21,33 +33,29 @@ pub enum TimerId { pub struct Timer { pub deadline: Instant, pub event: Event, + pub id: TimerId, interval: Option, - id: TimerId, } /// Scheduler tracking all pending timers. pub struct Scheduler { timers: VecDeque, -} - -impl Default for Scheduler { - fn default() -> Self { - Self { timers: VecDeque::new() } - } + event_proxy: EventLoopProxy, } impl Scheduler { - pub fn new() -> Self { - Self::default() + pub fn new(event_proxy: EventLoopProxy) -> Self { + Self { timers: VecDeque::new(), event_proxy } } /// Process all pending timers. /// /// If there are still timers pending after all ready events have been processed, the closest /// pending deadline will be returned. - pub fn update(&mut self, event_queue: &mut Vec) -> Option { + pub fn update(&mut self) -> Option { let now = Instant::now(); + while !self.timers.is_empty() && self.timers[0].deadline <= now { if let Some(timer) = self.timers.pop_front() { // Automatically repeat the event. @@ -55,7 +63,7 @@ impl Scheduler { self.schedule(timer.event.clone(), interval, true, timer.id); } - event_queue.push(timer.event); + let _ = self.event_proxy.send_event(timer.event); } } @@ -67,17 +75,11 @@ impl Scheduler { let deadline = Instant::now() + interval; // Get insert position in the schedule. - let mut index = self.timers.len(); - loop { - if index == 0 { - break; - } - index -= 1; - - if self.timers[index].deadline < deadline { - break; - } - } + let index = self + .timers + .iter() + .position(|timer| timer.deadline > deadline) + .unwrap_or_else(|| self.timers.len()); // Set the automatic event repeat rate. let interval = if repeat { Some(interval) } else { None }; @@ -86,9 +88,9 @@ impl Scheduler { } /// Cancel a scheduled event. - pub fn unschedule(&mut self, id: TimerId) -> Option { + pub fn unschedule(&mut self, id: TimerId) -> Option { let index = self.timers.iter().position(|timer| timer.id == id)?; - self.timers.remove(index).map(|timer| timer.event) + self.timers.remove(index) } /// Check if a timer is already scheduled. @@ -96,8 +98,11 @@ impl Scheduler { self.timers.iter().any(|timer| timer.id == id) } - /// Access a staged event by ID. - pub fn get_mut(&mut self, id: TimerId) -> Option<&mut Timer> { - self.timers.iter_mut().find(|timer| timer.id == id) + /// Remove all timers scheduled for a window. + /// + /// This must be called when a window is removed to ensure that timers on intervals do not + /// stick around forever and cause a memory leak. + pub fn unschedule_window(&mut self, window_id: WindowId) { + self.timers.retain(|timer| timer.id.window_id != window_id); } } diff --git a/alacritty/src/window_context.rs b/alacritty/src/window_context.rs new file mode 100644 index 00000000..caa69851 --- /dev/null +++ b/alacritty/src/window_context.rs @@ -0,0 +1,374 @@ +//! Terminal window context. + +use std::error::Error; +use std::fs::File; +use std::io::Write; +use std::mem; +#[cfg(not(any(target_os = "macos", windows)))] +use std::sync::atomic::Ordering; +use std::sync::Arc; + +use crossfont::Size; +use glutin::event::{Event as GlutinEvent, ModifiersState, WindowEvent}; +use glutin::event_loop::{EventLoopProxy, EventLoopWindowTarget}; +use glutin::window::WindowId; +use log::info; +use serde_json as json; +#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] +use wayland_client::EventQueue; + +use alacritty_terminal::event::Event as TerminalEvent; +use alacritty_terminal::event_loop::{EventLoop as PtyEventLoop, Msg, Notifier}; +use alacritty_terminal::grid::{Dimensions, Scroll}; +use alacritty_terminal::index::Direction; +use alacritty_terminal::sync::FairMutex; +use alacritty_terminal::term::{Term, TermMode}; +use alacritty_terminal::tty; + +use crate::clipboard::Clipboard; +use crate::config::Config; +use crate::display::Display; +use crate::event::{ActionContext, Event, EventProxy, EventType, Mouse, SearchState}; +use crate::input; +use crate::message_bar::MessageBuffer; +use crate::scheduler::Scheduler; + +/// Event context for one individual Alacritty window. +pub struct WindowContext { + pub message_buffer: MessageBuffer, + pub display: Display, + event_queue: Vec>, + terminal: Arc>>, + modifiers: ModifiersState, + search_state: SearchState, + received_count: usize, + suppress_chars: bool, + notifier: Notifier, + font_size: Size, + mouse: Mouse, + dirty: bool, +} + +impl WindowContext { + /// Create a new terminal window context. + pub fn new( + config: &Config, + window_event_loop: &EventLoopWindowTarget, + proxy: EventLoopProxy, + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + wayland_event_queue: Option<&EventQueue>, + ) -> Result> { + // Create a display. + // + // The display manages a window and can draw the terminal. + let display = Display::new( + config, + window_event_loop, + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + wayland_event_queue, + )?; + + info!( + "PTY dimensions: {:?} x {:?}", + display.size_info.screen_lines(), + display.size_info.columns() + ); + + let event_proxy = EventProxy::new(proxy, display.window.id()); + + // Create the terminal. + // + // This object contains all of the state about what's being displayed. It's + // wrapped in a clonable mutex since both the I/O loop and display need to + // access it. + let terminal = Term::new(config, display.size_info, event_proxy.clone()); + let terminal = Arc::new(FairMutex::new(terminal)); + + // Create the PTY. + // + // The PTY forks a process to run the shell on the slave side of the + // pseudoterminal. A file descriptor for the master side is retained for + // reading/writing to the shell. + let pty = tty::new(config, &display.size_info, display.window.x11_window_id()); + + // Create the pseudoterminal I/O loop. + // + // PTY I/O is ran on another thread as to not occupy cycles used by the + // renderer and input processing. Note that access to the terminal state is + // synchronized since the I/O loop updates the state, and the display + // consumes it periodically. + let event_loop = PtyEventLoop::new( + Arc::clone(&terminal), + event_proxy.clone(), + pty, + config.hold, + config.ui_config.debug.ref_test, + ); + + // The event loop channel allows write requests from the event processor + // to be sent to the pty loop and ultimately written to the pty. + let loop_tx = event_loop.channel(); + + // Kick off the I/O thread. + let _io_thread = event_loop.spawn(); + + // Start cursor blinking, in case `Focused` isn't sent on startup. + if config.cursor.style().blinking { + event_proxy.send_event(TerminalEvent::CursorBlinkingChange.into()); + } + + // Create context for the Alacritty window. + Ok(WindowContext { + font_size: config.ui_config.font.size(), + notifier: Notifier(loop_tx), + terminal, + display, + suppress_chars: Default::default(), + message_buffer: Default::default(), + received_count: Default::default(), + search_state: Default::default(), + event_queue: Default::default(), + modifiers: Default::default(), + mouse: Default::default(), + dirty: Default::default(), + }) + } + + /// Update the terminal window to the latest config. + pub fn update_config(&mut self, old_config: &Config, config: &Config) { + self.display.update_config(config); + self.terminal.lock().update_config(config); + + // Reload cursor if its thickness has changed. + if (old_config.cursor.thickness() - config.cursor.thickness()).abs() > f32::EPSILON { + self.display.pending_update.set_cursor_dirty(); + } + + if old_config.ui_config.font != config.ui_config.font { + // Do not update font size if it has been changed at runtime. + if self.font_size == old_config.ui_config.font.size() { + self.font_size = config.ui_config.font.size(); + } + + let font = config.ui_config.font.clone().with_size(self.font_size); + self.display.pending_update.set_font(font); + } + + // Update display if padding options were changed. + let window_config = &old_config.ui_config.window; + if window_config.padding(1.) != config.ui_config.window.padding(1.) + || window_config.dynamic_padding != config.ui_config.window.dynamic_padding + { + self.display.pending_update.dirty = true; + } + + // Live title reload. + if !config.ui_config.window.dynamic_title + || old_config.ui_config.window.title != config.ui_config.window.title + { + self.display.window.set_title(&config.ui_config.window.title); + } + + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + self.display.window.set_wayland_theme(&config.ui_config.colors); + + // Set subpixel anti-aliasing. + #[cfg(target_os = "macos")] + crossfont::set_font_smoothing(config.ui_config.font.use_thin_strokes); + + // Disable shadows for transparent windows on macOS. + #[cfg(target_os = "macos")] + self.display.window.set_has_shadow(config.ui_config.window_opacity() >= 1.0); + + // Update hint keys. + self.display.hint_state.update_alphabet(config.ui_config.hints.alphabet()); + + // Update cursor blinking. + let event = Event::new(TerminalEvent::CursorBlinkingChange.into(), None); + self.event_queue.push(event.into()); + + self.dirty = true; + } + + /// Process events for this terminal window. + pub fn handle_event( + &mut self, + event_loop: &EventLoopWindowTarget, + event_proxy: &EventLoopProxy, + config: &mut Config, + clipboard: &mut Clipboard, + scheduler: &mut Scheduler, + event: GlutinEvent<'_, Event>, + ) { + match event { + // Skip further event handling with no staged updates. + GlutinEvent::RedrawEventsCleared if self.event_queue.is_empty() && !self.dirty => { + return; + }, + // Continue to process all pending events. + GlutinEvent::RedrawEventsCleared => (), + // Remap DPR change event to remove the lifetime. + GlutinEvent::WindowEvent { + event: WindowEvent::ScaleFactorChanged { scale_factor, new_inner_size }, + window_id, + } => { + let size = (new_inner_size.width, new_inner_size.height); + let event = Event::new(EventType::DprChanged(scale_factor, size), window_id); + self.event_queue.push(event.into()); + return; + }, + // Transmute to extend lifetime, which exists only for `ScaleFactorChanged` event. + // Since we remap that event to remove the lifetime, this is safe. + event => unsafe { + self.event_queue.push(mem::transmute(event)); + return; + }, + } + + let mut terminal = self.terminal.lock(); + + let old_is_searching = self.search_state.history_index.is_some(); + + let context = ActionContext { + message_buffer: &mut self.message_buffer, + received_count: &mut self.received_count, + suppress_chars: &mut self.suppress_chars, + search_state: &mut self.search_state, + modifiers: &mut self.modifiers, + font_size: &mut self.font_size, + notifier: &mut self.notifier, + display: &mut self.display, + mouse: &mut self.mouse, + dirty: &mut self.dirty, + terminal: &mut terminal, + event_proxy, + event_loop, + clipboard, + scheduler, + config, + }; + let mut processor = input::Processor::new(context); + + for event in self.event_queue.drain(..) { + processor.handle_event(event); + } + + // Process DisplayUpdate events. + if self.display.pending_update.dirty { + Self::submit_display_update( + &mut terminal, + &mut self.display, + &mut self.notifier, + &self.message_buffer, + &self.search_state, + old_is_searching, + config, + ); + } + + if self.dirty || self.mouse.hint_highlight_dirty { + self.dirty |= self.display.update_highlighted_hints( + &terminal, + config, + &self.mouse, + self.modifiers, + ); + self.mouse.hint_highlight_dirty = false; + } + + // Skip rendering on Wayland until we get frame event from compositor. + #[cfg(not(any(target_os = "macos", windows)))] + if !self.display.is_x11 && !self.display.window.should_draw.load(Ordering::Relaxed) { + return; + } + + if self.dirty { + self.dirty = false; + + // Request immediate re-draw if visual bell animation is not finished yet. + if !self.display.visual_bell.completed() { + self.display.window.request_redraw(); + } + + // Redraw screen. + self.display.draw(terminal, &self.message_buffer, config, &self.search_state); + } + } + + /// ID of this terminal context. + pub fn id(&self) -> WindowId { + self.display.window.id() + } + + /// Write the ref test results to the disk. + pub fn write_ref_test_results(&self) { + // Dump grid state. + let mut grid = self.terminal.lock().grid().clone(); + grid.initialize_all(); + grid.truncate(); + + let serialized_grid = json::to_string(&grid).expect("serialize grid"); + + let serialized_size = json::to_string(&self.display.size_info).expect("serialize size"); + + let serialized_config = format!("{{\"history_size\":{}}}", grid.history_size()); + + File::create("./grid.json") + .and_then(|mut f| f.write_all(serialized_grid.as_bytes())) + .expect("write grid.json"); + + File::create("./size.json") + .and_then(|mut f| f.write_all(serialized_size.as_bytes())) + .expect("write size.json"); + + File::create("./config.json") + .and_then(|mut f| f.write_all(serialized_config.as_bytes())) + .expect("write config.json"); + } + + /// Submit the pending changes to the `Display`. + fn submit_display_update( + terminal: &mut Term, + display: &mut Display, + notifier: &mut Notifier, + message_buffer: &MessageBuffer, + search_state: &SearchState, + old_is_searching: bool, + config: &Config, + ) { + // 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 = if terminal.mode().contains(TermMode::VI) { + terminal.vi_mode_cursor.point.line == num_lines - 1 + } else { + search_state.direction == Direction::Left + }; + + display.handle_update( + terminal, + notifier, + message_buffer, + search_state.history_index.is_some(), + config, + ); + + let new_is_searching = search_state.history_index.is_some(); + if !old_is_searching && new_is_searching { + // Scroll on search start to make sure origin is visible with minimal viewport motion. + 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)); + } + } + } +} + +impl Drop for WindowContext { + fn drop(&mut self) { + // Shutdown the terminal's PTY. + let _ = self.notifier.0.send(Msg::Shutdown); + } +} diff --git a/alacritty_terminal/src/event.rs b/alacritty_terminal/src/event.rs index fac7a56a..1ddf820b 100644 --- a/alacritty_terminal/src/event.rs +++ b/alacritty_terminal/src/event.rs @@ -39,7 +39,7 @@ pub enum Event { PtyWrite(String), /// Cursor blinking state has changed. - CursorBlinkingChange(bool), + CursorBlinkingChange, /// New terminal content available. Wakeup, @@ -54,17 +54,17 @@ pub enum Event { impl Debug for Event { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - Event::MouseCursorDirty => write!(f, "MouseCursorDirty"), - Event::Title(title) => write!(f, "Title({})", title), - Event::ResetTitle => write!(f, "ResetTitle"), Event::ClipboardStore(ty, text) => write!(f, "ClipboardStore({:?}, {})", ty, text), Event::ClipboardLoad(ty, _) => write!(f, "ClipboardLoad({:?})", ty), Event::ColorRequest(index, _) => write!(f, "ColorRequest({})", index), Event::PtyWrite(text) => write!(f, "PtyWrite({})", text), + Event::Title(title) => write!(f, "Title({})", title), + Event::CursorBlinkingChange => write!(f, "CursorBlinkingChange"), + Event::MouseCursorDirty => write!(f, "MouseCursorDirty"), + Event::ResetTitle => write!(f, "ResetTitle"), Event::Wakeup => write!(f, "Wakeup"), Event::Bell => write!(f, "Bell"), Event::Exit => write!(f, "Exit"), - Event::CursorBlinkingChange(blinking) => write!(f, "CursorBlinking({})", blinking), } } } diff --git a/alacritty_terminal/src/event_loop.rs b/alacritty_terminal/src/event_loop.rs index fbd882ad..36392581 100644 --- a/alacritty_terminal/src/event_loop.rs +++ b/alacritty_terminal/src/event_loop.rs @@ -73,13 +73,13 @@ impl event::Notify for Notifier { return; } - self.0.send(Msg::Input(bytes)).expect("send event loop msg"); + let _ = self.0.send(Msg::Input(bytes)); } } impl event::OnResize for Notifier { fn on_resize(&mut self, size: &SizeInfo) { - self.0.send(Msg::Resize(*size)).expect("expected send event loop msg"); + let _ = self.0.send(Msg::Resize(*size)); } } @@ -182,8 +182,8 @@ where while let Ok(msg) = self.rx.try_recv() { match msg { Msg::Input(input) => state.write_list.push_back(input), - Msg::Shutdown => return false, Msg::Resize(size) => self.pty.on_resize(&size), + Msg::Shutdown => return false, } } diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index 1808f3aa..894bd763 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -640,7 +640,7 @@ impl Term { } // Update UI about cursor blinking state changes. - self.event_proxy.send_event(Event::CursorBlinkingChange(self.cursor_style().blinking)); + self.event_proxy.send_event(Event::CursorBlinkingChange); } /// Move vi mode cursor. @@ -1471,8 +1471,7 @@ impl Handler for Term { self.mode &= TermMode::VI; self.mode.insert(TermMode::default()); - let blinking = self.cursor_style().blinking; - self.event_proxy.send_event(Event::CursorBlinkingChange(blinking)); + self.event_proxy.send_event(Event::CursorBlinkingChange); } #[inline] @@ -1576,7 +1575,7 @@ impl Handler for Term { ansi::Mode::BlinkingCursor => { let style = self.cursor_style.get_or_insert(self.default_cursor_style); style.blinking = true; - self.event_proxy.send_event(Event::CursorBlinkingChange(true)); + self.event_proxy.send_event(Event::CursorBlinkingChange); }, } } @@ -1618,7 +1617,7 @@ impl Handler for Term { ansi::Mode::BlinkingCursor => { let style = self.cursor_style.get_or_insert(self.default_cursor_style); style.blinking = false; - self.event_proxy.send_event(Event::CursorBlinkingChange(false)); + self.event_proxy.send_event(Event::CursorBlinkingChange); }, } } @@ -1678,8 +1677,7 @@ impl Handler for Term { self.cursor_style = style; // Notify UI about blinking changes. - let blinking = style.unwrap_or(self.default_cursor_style).blinking; - self.event_proxy.send_event(Event::CursorBlinkingChange(blinking)); + self.event_proxy.send_event(Event::CursorBlinkingChange); } #[inline] diff --git a/alacritty_terminal/src/tty/unix.rs b/alacritty_terminal/src/tty/unix.rs index 483333e7..a52f8329 100644 --- a/alacritty_terminal/src/tty/unix.rs +++ b/alacritty_terminal/src/tty/unix.rs @@ -246,6 +246,16 @@ pub fn new(config: &Config, size: &SizeInfo, window_id: Option) -> } } +impl Drop for Pty { + fn drop(&mut self) { + // Make sure the PTY is terminated properly. + unsafe { + libc::kill(self.child.id() as i32, libc::SIGHUP); + } + let _ = self.child.wait(); + } +} + impl EventedReadWrite for Pty { type Reader = File; type Writer = File; @@ -339,6 +349,22 @@ impl EventedPty for Pty { } } +impl OnResize for Pty { + /// Resize the PTY. + /// + /// Tells the kernel that the window size changed with the new pixel + /// dimensions and line/column counts. + fn on_resize(&mut self, size: &SizeInfo) { + let win = size.to_winsize(); + + let res = unsafe { libc::ioctl(self.fd.as_raw_fd(), libc::TIOCSWINSZ, &win as *const _) }; + + if res < 0 { + die!("ioctl TIOCSWINSZ failed: {}", io::Error::last_os_error()); + } + } +} + /// Types that can produce a `libc::winsize`. pub trait ToWinsize { /// Get a `libc::winsize`. @@ -356,22 +382,6 @@ impl<'a> ToWinsize for &'a SizeInfo { } } -impl OnResize for Pty { - /// Resize the PTY. - /// - /// Tells the kernel that the window size changed with the new pixel - /// dimensions and line/column counts. - fn on_resize(&mut self, size: &SizeInfo) { - let win = size.to_winsize(); - - let res = unsafe { libc::ioctl(self.fd.as_raw_fd(), libc::TIOCSWINSZ, &win as *const _) }; - - if res < 0 { - die!("ioctl TIOCSWINSZ failed: {}", io::Error::last_os_error()); - } - } -} - unsafe fn set_nonblocking(fd: c_int) { use libc::{fcntl, F_GETFL, F_SETFL, O_NONBLOCK}; diff --git a/docs/features.md b/docs/features.md index fd9a9ad2..a97c143d 100644 --- a/docs/features.md +++ b/docs/features.md @@ -78,3 +78,9 @@ change in mouse cursor shape, you're required to hold Shift to bypass that. [configuration file]: ../alacritty.yml + +## Multi-Window + +Alacritty supports running multiple terminal emulators from the same Alacritty +instance. New windows can be created either by using the `CreateNewWindow` +keybinding action, or by executing the `alacritty msg create-window` subcommand. diff --git a/extra/alacritty-msg.man b/extra/alacritty-msg.man new file mode 100644 index 00000000..818169d0 --- /dev/null +++ b/extra/alacritty-msg.man @@ -0,0 +1,31 @@ +.TH ALACRITTY-MSG "1" "October 2021" "alacritty 0.10.0-dev" "User Commands" +.SH NAME +alacritty-msg \- Send messages to Alacritty +.SH "SYNOPSIS" +alacritty msg [OPTIONS] [MESSAGES] +.SH DESCRIPTION +This command communicates with running Alacritty instances through a socket, +making it possible to control Alacritty without directly accessing it. +.SH "OPTIONS" +.TP +\fB\-s\fR, \fB\-\-socket\fR +Path for IPC socket creation +.SH "MESSAGES" +.TP +\fBcreate-window\fR +Create a new window in the same Alacritty process +.SH "SEE ALSO" +See the alacritty github repository at https://github.com/alacritty/alacritty for the full documentation. +.SH "BUGS" +Found a bug? Please report it at https://github.com/alacritty/alacritty/issues. +.SH "MAINTAINERS" +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +Christian Duerr diff --git a/extra/alacritty.man b/extra/alacritty.man index fbc28c25..5ff0852e 100644 --- a/extra/alacritty.man +++ b/extra/alacritty.man @@ -2,7 +2,7 @@ .SH NAME Alacritty \- A fast, cross-platform, OpenGL terminal emulator .SH "SYNOPSIS" -alacritty [FLAGS] [OPTIONS] +alacritty [SUBCOMMANDS] [FLAGS] [OPTIONS] .SH DESCRIPTION Alacritty is a modern terminal emulator that comes with sensible defaults, but allows for extensive configuration. By integrating with other applications, @@ -57,11 +57,18 @@ Defines the X11 window ID (as a decimal integer) to embed Alacritty within \fB\-o\fR, \fB\-\-option\fR