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