summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--Cargo.lock7
-rw-r--r--alacritty.yml5
-rw-r--r--alacritty/Cargo.toml2
-rw-r--r--alacritty/src/cli.rs51
-rw-r--r--alacritty/src/config/bindings.rs5
-rw-r--r--alacritty/src/config/monitor.rs10
-rw-r--r--alacritty/src/config/ui_config.rs8
-rw-r--r--alacritty/src/display/mod.rs64
-rw-r--r--alacritty/src/display/window.rs68
-rw-r--r--alacritty/src/event.rs831
-rw-r--r--alacritty/src/input.rs36
-rw-r--r--alacritty/src/ipc.rs145
-rw-r--r--alacritty/src/logging.rs21
-rw-r--r--alacritty/src/main.rs210
-rw-r--r--alacritty/src/message_bar.rs35
-rw-r--r--alacritty/src/renderer/mod.rs29
-rw-r--r--alacritty/src/scheduler.rs69
-rw-r--r--alacritty/src/window_context.rs374
-rw-r--r--alacritty_terminal/src/event.rs10
-rw-r--r--alacritty_terminal/src/event_loop.rs6
-rw-r--r--alacritty_terminal/src/term/mod.rs12
-rw-r--r--alacritty_terminal/src/tty/unix.rs42
-rw-r--r--docs/features.md6
-rw-r--r--extra/alacritty-msg.man31
-rw-r--r--extra/alacritty.man9
-rw-r--r--extra/completions/_alacritty74
-rw-r--r--extra/completions/alacritty.bash9
-rw-r--r--extra/completions/alacritty.fish62
29 files changed, 1421 insertions, 811 deletions
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<PathBuf>,
/// 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<String>,
+
+ /// CLI options for config overrides.
+ #[structopt(skip)]
+ pub config_options: Value,
+
+ /// Subcommand passed to the CLI.
+ #[cfg(unix)]
+ #[structopt(subcommand)]
+ pub subcommands: Option<Subcommands>,
}
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<Class, String> {
}
}
+/// 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<PathBuf>,
+
+ /// 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<PathBuf>, event_proxy: EventProxy) {
+pub fn watch(mut paths: Vec<PathBuf>, event_proxy: EventLoopProxy<Event>) {
// 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<PathBuf>, 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<HintMatch>,
- #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
- pub wayland_event_queue: Option<EventQueue>,
-
#[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<E>(config: &Config, event_loop: &EventLoop<E>) -> Result<Display, Error> {
+ pub fn new<E>(
+ config: &Config,
+ event_loop: &EventLoopWindowTarget<E>,
+ #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+ wayland_event_queue: Option<&EventQueue>,
+ ) -> Result<Display, Error> {
#[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<Point<usize>> {
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<crossfont::Error> for Error {
fn create_gl_window<E>(
mut window: WindowBuilder,
- event_loop: &EventLoop<E>,
+ event_loop: &EventLoopWindowTarget<E>,
srgb: bool,
vsync: bool,
dimensions: Option<PhysicalSize<u32>>,
@@ -160,7 +161,7 @@ pub struct Window {
/// Cached DPR for quickly scaling pixel sizes.
pub dpr: f64,
- windowed_context: WindowedContext<PossiblyCurrent>,
+ windowed_context: Replaceable<WindowedContext<PossiblyCurrent>>,
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<E>(
- event_loop: &EventLoop<E>,
+ event_loop: &EventLoopWindowTarget<E>,
config: &Config,
size: Option<PhysicalSize<u32>>,
#[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<u32>) {
self.window().set_inner_size(size);
}
+ #[inline]
pub fn inner_size(&self) -> PhysicalSize<u32> {
self.window().inner_size()
}
@@ -261,6 +264,11 @@ impl Window {
}
#[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 {
self.current_mouse_cursor = 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<T>(Option<T>);
+
+impl<T> Replaceable<T> {
+ pub fn new(inner: T) -> Self {
+ Self(Some(inner))
+ }
+
+ /// Replace the contents of the container.
+ pub fn replace_with<F: FnMut(T) -> 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<T> Deref for Replaceable<T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ self.get()
+ }
+}
+
+impl<T> DerefMut for Replaceable<T> {
+ 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<usize> = 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<WindowId>,
+
+ /// Event payload.
+ payload: EventType,
+}
+
+impl Event {
+ pub fn new<I: Into<Option<WindowId>>>(payload: EventType, window_id: I) -> Self {
+ Self { window_id: window_id.into(), payload }
+ }
}
impl From<Event> for GlutinEvent<'_, Event> {
@@ -78,16 +81,32 @@ impl From<Event> for GlutinEvent<'_, Event> {
}
}
-impl From<TerminalEvent> 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<TerminalEvent> 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<usize>,
/// 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<String>,
- /// Current position in the search history.
- history_index: Option<usize>,
-
/// Compiled search automatons.
dfas: Option<RegexSearch>,
}
@@ -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<Event>,
+ pub event_proxy: &'a EventLoopProxy<Event>,
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<T> for ActionContext<'a, N, T> {
@@ -380,23 +395,27 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> 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<T> 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<T> 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<T> 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<N> {
- 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<GlutinEvent<'static, Event>>,
- search_state: SearchState,
- cli_options: CLIOptions,
- dirty: bool,
-}
-
-impl<N: Notify + OnResize> Processor<N> {
- /// Create a new event processor.
- ///
- /// Takes a writer which is expected to be hooked up to the write end of a PTY.
- pub fn new(
- notifier: N,
- message_buffer: MessageBuffer,
- config: Config,
- display: Display,
- cli_options: CLIOptions,
- ) -> Processor<N> {
- 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<T>(&mut self, terminal: Arc<FairMutex<Term<T>>>, mut event_loop: EventLoop<Event>)
- 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<EventProxy, ActionContext<'_, Notifier, EventProxy>> {
/// Handle events from glutin.
///
/// Doesn't take self mutably due to borrow checking.
- fn handle_event<T>(
- event: GlutinEvent<'_, Event>,
- processor: &mut input::Processor<T, ActionContext<'_, N, T>>,
- ) 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<N: Notify + OnResize> Processor<N> {
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;
-
- 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);
+ self.ctx.terminal.is_focused = is_focused;
+ *self.ctx.dirty = true;
+
+ 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<N: Notify + OnResize> Processor<N> {
| 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<EventQueue>,
+ windows: HashMap<WindowId, WindowContext>,
+ 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<Event>,
+ ) -> 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<Event>,
+ proxy: EventLoopProxy<Event>,
+ ) -> Result<(), Box<dyn Error>> {
+ 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<Event>) {
+ 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<N: Notify + OnResize> Processor<N> {
_ => false,
}
}
-
- /// Reload the configuration files from disk.
- fn reload_config<T>(path: &Path, processor: &mut input::Processor<T, ActionContext<'_, N, T>>)
- 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<T>(
- &mut self,
- terminal: &mut Term<T>,
- old_is_searching: bool,
- display_update_pending: DisplayUpdate,
- ) where
- T: EventListener,
- {
- // Compute cursor positions before resize.
- let num_lines = terminal.screen_lines();
- let cursor_at_bottom = terminal.grid().cursor.point.line + 1 == num_lines;
- let origin_at_bottom = 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<T>(&self, terminal: &Term<T>) {
- // 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<Event>);
+pub struct EventProxy {
+ proxy: EventLoopProxy<Event>,
+ window_id: WindowId,
+}
impl EventProxy {
- pub fn new(proxy: EventLoopProxy<Event>) -> Self {
- EventProxy(proxy)
+ pub fn new(proxy: EventLoopProxy<Event>, window_id: WindowId) -> Self {
+ Self { proxy, window_id }
}
/// Send an event to the event loop.
- pub fn send_event(&self, event: 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<T: EventListener> {
fn terminal(&self) -> &Term<T>;
fn terminal_mut(&mut self) -> &mut Term<T>;
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<T: EventListener> Execute<T> 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<T: EventListener, A: ActionContext<T>> Processor<T, A> {
}
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<T: EventListener, A: ActionContext<T>> Processor<T, A> {
// 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<T: EventListener, A: ActionContext<T>> Processor<T, A> {
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<T: EventListener, A: ActionContext<T>> Processor<T, A> {
} 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<Event>) -> Option<PathBuf> {
+ // 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<PathBuf>, 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<PathBuf>) -> IoResult<UnixDatagram> {
+ 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,151 +75,135 @@ fn main() {
// Load command line options.
let options = Options::new();
- // Setup glutin event loop.
- let window_event_loop = GlutinEventLoop::<Event>::with_user_event();
+ #[cfg(unix)]
+ let result = match options.subcommands {
+ Some(Subcommands::Msg(options)) => msg(options),
+ None => alacritty(options),
+ };
- // Initialize the logger as soon as possible as to capture output from other subsystems.
- let log_file = logging::initialize(&options, window_event_loop.create_proxy())
- .expect("Unable to initialize logger");
+ #[cfg(not(unix))]
+ let result = alacritty(options);
- // Load configuration file.
- let config = config::load(&options);
-
- // Update the log level from config.
- log::set_max_level(config.ui_config.debug.log_level);
+ // Handle command failure.
+ if let Err(err) = result {
+ eprintln!("Error: {}", err);
+ process::exit(1);
+ }
+}
- // 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();
+/// `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())
+}
- // Store if log file should be deleted before moving config.
- let persistent_logging = config.ui_config.debug.persistent_logging;
+/// Temporary files stored for Alacritty.
+///
+/// This stores temporary files to automate their destruction through its `Drop` implementation.
+struct TemporaryFiles {
+ #[cfg(unix)]
+ socket_path: Option<PathBuf>,
+ log_file: Option<PathBuf>,
+}
- // 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);
- }
+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) = log_file {
- if !persistent_logging && fs::remove_file(&log_file).is_ok() {
- let _ = writeln!(io::stdout(), "Deleted log file at \"{}\"", log_file.display());
+ // 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 Alacritty.
+/// 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 run(
- window_event_loop: GlutinEventLoop<Event>,
- config: Config,
- options: Options,
-) -> Result<(), Box<dyn Error>> {
+fn alacritty(options: Options) -> Result<(), String> {
info!("Welcome to Alacritty");
- // Log the configuration paths.
- log_config_path(&config);
-
- // Set environment variables.
- tty::setup_env(&config);
+ // Setup glutin event loop.
+ let window_event_loop = GlutinEventLoop::<Event>::with_user_event();
- let event_proxy = EventProxy::new(window_event_loop.create_proxy());
+ // Initialize the logger as soon as possible as to capture output from other subsystems.
+ let log_file = logging::initialize(&options, window_event_loop.create_proxy())
+ .expect("Unable to initialize logger");
- // Create a display.
- //
- // The display manages a window and can draw the terminal.
- let display = Display::new(&config, &window_event_loop)?;
+ // Load configuration file.
+ let config = config::load(&options);
+ log_config_path(&config);
- info!(
- "PTY dimensions: {:?} x {:?}",
- display.size_info.screen_lines(),
- display.size_info.columns()
- );
+ // Update the log level from config.
+ log::set_max_level(config.ui_config.debug.log_level);
- // 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));
+ // Set environment variables.
+ tty::setup_env(&config);
- // 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());
+ // Switch to home directory.
+ #[cfg(target_os = "macos")]
+ env::set_current_dir(dirs::home_dir().unwrap()).unwrap();
- // 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<ConPTY>.
+ // - 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<ConPTY>
//
- // 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<Duration>,
- id: TimerId,
}
/// Scheduler tracking all pending timers.
pub struct Scheduler {
timers: VecDeque<Timer>,
-}
-
-impl Default for Scheduler {
- fn default() -> Self {
- Self { timers: VecDeque::new() }
- }
+ event_proxy: EventLoopProxy<Event>,
}
impl Scheduler {
- pub fn new() -> Self {
- Self::default()
+ pub fn new(event_proxy: EventLoopProxy<Event>) -> 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<Event>) -> Option<Instant> {
+ pub fn update(&mut self) -> Option<Instant> {
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<Event> {
+ pub fn unschedule(&mut self, id: TimerId) -> Option<Timer> {
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<GlutinEvent<'static, Event>>,
+ terminal: Arc<FairMutex<Term<EventProxy>>>,
+ 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<Event>,
+ proxy: EventLoopProxy<Event>,
+ #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
+ wayland_event_queue: Option<&EventQueue>,
+ ) -> Result<Self, Box<dyn Error>> {
+ // 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>,
+ event_proxy: &EventLoopProxy<Event>,
+ 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<EventProxy>,
+ 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<T> Term<T> {
}
// 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<T: EventListener> Handler for Term<T> {
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<T: EventListener> Handler for Term<T> {
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<T: EventListener> Handler for Term<T> {
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<T: EventListener> Handler for Term<T> {
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<C>(config: &Config<C>, size: &SizeInfo, window_id: Option<usize>) ->
}
}
+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 <kbd>Shift</kbd> 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 <socket>
+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 <contact@christianduerr.com>
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 <option>...
Override configuration file options [example: cursor.style=Beam]
.TP
+\fB\-\-socket\fR <socket>
+Path for IPC socket creation
+.TP
\fB\-t\fR, \fB\-\-title\fR <title>
Defines the window title [default: Alacritty]
.TP
\fB\-\-working\-directory\fR <working\-directory>
Start the shell in the specified working directory
+.SH "SUBCOMMANDS"
+.TP
+\fBmsg\fR
+Available socket messages
.SH "SEE ALSO"
See the alacritty github repository at https://github.com/alacritty/alacritty for the full documentation.
.SH "BUGS"
diff --git a/extra/completions/_alacritty b/extra/completions/_alacritty
index c97d563e..1313128e 100644
--- a/extra/completions/_alacritty
+++ b/extra/completions/_alacritty
@@ -1,20 +1,62 @@
#compdef alacritty
-local ign
+# Completions available for the first parameter.
+_alacritty_first_param() {
+ # Main subcommands.
+ _describe "command" "(msg:'Available socket messages')"
-(( $#words > 2 )) && ign='!'
+ # Default options.
+ _alacritty_main
+}
+
+# Completions available for parameters after the first.
+_alacritty_following_param() {
+ case $words[2] in
+ msg)
+ _alacritty_msg;;
+ *)
+ _alacritty_main;;
+ esac
+}
+
+# Completions for the main Alacritty executable.
+_alacritty_main() {
+ # Limit some suggestions to the first option.
+ local ignore
+ (( $#words > 2 )) && ignore='!'
+
+ _arguments \
+ "$ignore(-)"{-h,--help}"[print help information]" \
+ "$ignore(-)"{-V,--version}"[print version information]" \
+ "--print-events[print all events to stdout]" \
+ '(-v)'{-q,-qq}"[reduce the level of verbosity (min is -qq)]" \
+ "--ref-test[generate ref test]" \
+ "--hold[remain open after child process exits]" \
+ '(-q)'{-v,-vv,-vvv}"[increase the level of verbosity (max is -vvv)]" \
+ "--class=[define the window class]:class" \
+ "--embed=[define the X11 window ID (as a decimal integer) to embed Alacritty within]:windowId" \
+ "(-e --command)"{-e,--command}"[execute command (must be last arg)]:program: _command_names -e:*::program arguments: _normal" \
+ "--config-file=[specify an alternative config file]:file:_files" \
+ "*"{-o=,--option=}"[override config file options]:option" \
+ "(-t --title)"{-t=,--title=}"[define the window title]:title" \
+ "--working-directory=[start shell in specified directory]:directory:_directories"\
+ "--socket=[Path for IPC socket creation]:file:_files"
+}
+
+# Completions for the `msg` subcommand.
+_alacritty_msg() {
+ # Limit some suggestions to the first option.
+ local ignore
+ (( $#words > 3 )) && ignore='!'
+
+ _arguments \
+ "$ignore(-)"{-h,--help}"[print help information]" \
+ "$ignore(-)"{-V,--version}"[print version information]" \
+ "(-s --socket)"{-s=,--socket=}"[Path for IPC socket creation]:file:_files" \
+ "*: :((create-window:'Create a new window in the same Alacritty process'))"
+}
+
+# Handle arguments based on their position.
_arguments \
- "$ign(-)"{-h,--help}"[print help information]" \
- "--print-events[print all events to stdout]" \
- '(-v)'{-q,-qq}"[reduce the level of verbosity (min is -qq)]" \
- "--ref-test[generate ref test]" \
- "--hold[remain open after child process exits]" \
- '(-q)'{-v,-vv,-vvv}"[increase the level of verbosity (max is -vvv)]" \
- "$ign(-)"{-V,--version}"[print version information]" \
- "--class=[define the window class]:class" \
- "--embed=[define the X11 window ID (as a decimal integer) to embed Alacritty within]:windowId" \
- "(-e --command)"{-e,--command}"[execute command (must be last arg)]:program: _command_names -e:*::program arguments: _normal" \
- "--config-file=[specify an alternative config file]:file:_files" \
- "*"{-o=,--option=}"[override config file options]:option" \
- "(-t --title)"{-t=,--title=}"[define the window title]:title" \
- "--working-directory=[start shell in specified directory]:directory:_directories"
+ "1: :_alacritty_first_param" \
+ "*: :_alacritty_following_param"
diff --git a/extra/completions/alacritty.bash b/extra/completions/alacritty.bash
index e514126c..464afe6e 100644
--- a/extra/completions/alacritty.bash
+++ b/extra/completions/alacritty.bash
@@ -11,7 +11,7 @@ _alacritty()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
prevprev="${COMP_WORDS[COMP_CWORD-2]}"
- opts="-h --help -V --version --print-events -q -qq -v -vv -vvv --ref-test --hold -e --command --config-file -o --option -t --title --embed --class --working-directory"
+ opts="-h --help -V --version --print-events -q -qq -v -vv -vvv --ref-test --hold -e --command --config-file -o --option -t --title --embed --class --working-directory --socket msg"
# If `--command` or `-e` is used, stop completing
for i in "${!COMP_WORDS[@]}"; do
@@ -29,8 +29,8 @@ _alacritty()
# Complete all commands in $PATH
COMPREPLY=( $(compgen -c -- "${cur}") )
return 0;;
- --config-file)
- # Path based completion
+ --config-file | --socket)
+ # File completion
local IFS=$'\n'
compopt -o filenames
COMPREPLY=( $(compgen -f -- "${cur}") )
@@ -44,6 +44,9 @@ _alacritty()
compopt -o filenames
COMPREPLY=( $(compgen -d -- "${cur}") )
return 0;;
+ msg)
+ COMPREPLY=( $(compgen -W "-h --help -V --version -s --socket" -- "${cur}") )
+ return 0;;
esac
# Show all flags if there was no previous word
diff --git a/extra/completions/alacritty.fish b/extra/completions/alacritty.fish
index 6f8da9b0..fa399ffb 100644
--- a/extra/completions/alacritty.fish
+++ b/extra/completions/alacritty.fish
@@ -1,74 +1,104 @@
+# Available subcommands
+set -l commands msg help
+
+complete -c alacritty \
+ -n "not __fish_seen_subcommand_from $commands" \
+ -a "msg help"
+
# Meta
complete -c alacritty \
+ -n "not __fish_seen_subcommand_from help" \
-s "v" \
-l "version" \
-d "Prints version information"
complete -c alacritty \
+ -n "not __fish_seen_subcommand_from help" \
-s "h" \
-l "help" \
-d "Prints help information"
# Config
complete -c alacritty \
+ -n "not __fish_seen_subcommand_from $commands" \
-f \
-l "config-file" \
-d "Specify an alternative config file"
complete -c alacritty \
+ -n "not __fish_seen_subcommand_from $commands" \
-s "t" \
-l "title" \
-d "Defines the window title"
complete -c alacritty \
+ -n "not __fish_seen_subcommand_from $commands" \
-l "class" \
-d "Defines the window class"
complete -c alacritty \
+ -n "not __fish_seen_subcommand_from $commands" \
-l "embed" \
-d "Defines the X11 window ID (as a decimal integer) to embed Alacritty within"
complete -c alacritty \
+ -n "not __fish_seen_subcommand_from $commands" \
-x \
-a '(__fish_complete_directories (commandline -ct))' \
-l "working-directory" \
-d "Start shell in specified directory"
complete -c alacritty \
+ -n "not __fish_seen_subcommand_from $commands" \
-l "hold" \
-d "Remain open after child process exits"
complete -c alacritty \
+ -n "not __fish_seen_subcommand_from $commands" \
-s "o" \
-l "option" \
-d "Override config file options"
+complete -c alacritty \
+ -n "not __fish_seen_subcommand_from $commands" \
+ -l "socket" \
+ -d "Path for IPC socket creation"
# Output
-complete \
- -c alacritty \
+complete -c alacritty \
+ -n "not __fish_seen_subcommand_from $commands" \
-l "print-events" \
-d "Print all events to stdout"
-complete \
- -c alacritty \
+complete -c alacritty \
+ -n "not __fish_seen_subcommand_from $commands" \
-s "q" \
-d "Reduces the level of verbosity (min is -qq)"
-complete \
- -c alacritty \
+complete -c alacritty \
+ -n "not __fish_seen_subcommand_from $commands" \
-s "qq" \
-d "Reduces the level of verbosity"
-complete \
- -c alacritty \
+complete -c alacritty \
+ -n "not __fish_seen_subcommand_from $commands" \
-s "v" \
-d "Increases the level of verbosity"
-complete \
- -c alacritty \
+complete -c alacritty \
+ -n "not __fish_seen_subcommand_from $commands" \
-s "vv" \
-d "Increases the level of verbosity"
-complete \
- -c alacritty \
+complete -c alacritty \
+ -n "not __fish_seen_subcommand_from $commands" \
-s "vvv" \
-d "Increases the level of verbosity"
-complete \
- -c alacritty \
+complete -c alacritty \
+ -n "not __fish_seen_subcommand_from $commands" \
-l "ref-test" \
-d "Generates ref test"
-complete \
- -c alacritty \
+complete -c alacritty \
+ -n "not __fish_seen_subcommand_from $commands" \
-s "e" \
-l "command" \
-d "Execute command (must be last arg)"
+
+# Subcommand `msg`
+complete -c alacritty \
+ -n "__fish_seen_subcommand_from msg" \
+ -s "s" \
+ -l "socket" \
+ -d "Socket path override"
+complete -c alacritty \
+ -n "__fish_seen_subcommand_from msg" \
+ -a "create-window help"