Add multi-window support

Previously Alacritty would always initialize only a single terminal
emulator window feeding into the winit event loop, however some
platforms like macOS expect all windows to be spawned by the same
process and this "daemon-mode" can also come with the advantage of
increased memory efficiency.

The event loop has been restructured to handle all window-specific
events only by the event processing context with the associated window
id. This makes it possible to add new terminal windows at any time using
the WindowContext::new function call.

Some preliminary tests have shown that for empty terminals, this reduces
the cost of additional terminal emulators from ~100M to ~6M. However at
this point the robustness of the daemon against issues with individual
terminals has not been refined, making the reliability of this system
questionable.

New windows can be created either by using the new `CreateNewWindow`
action, or with the `alacritty msg create-window` subcommand. The
subcommand sends a message to an IPC socket which Alacritty listens on,
its location can be found in the `ALACRITTY_SOCKET` environment
variable.

Fixes #607.
This commit is contained in:
Christian Duerr 2021-10-23 07:16:47 +00:00 committed by GitHub
parent d8a98f8829
commit 1df7dc5171
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1424 additions and 814 deletions

View File

@ -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

7
Cargo.lock generated
View File

@ -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"

View File

@ -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

View File

@ -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 }

View File

@ -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::*;

View File

@ -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)

View File

@ -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);
}
_ => {},
_ => (),
}
}
});

View File

@ -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(),

View File

@ -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;

View File

@ -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()
}
@ -260,6 +263,11 @@ impl Window {
self.window().set_title(title);
}
#[inline]
pub fn request_redraw(&self) {
self.window().request_redraw();
}
#[inline]
pub fn set_mouse_cursor(&mut self, cursor: CursorIcon) {
if cursor != self.current_mouse_cursor {
@ -374,7 +382,7 @@ impl Window {
None
}
pub fn window_id(&self) -> WindowId {
pub fn id(&self) -> WindowId {
self.window().id()
}
@ -436,6 +444,13 @@ impl Window {
self.windowed_context.resize(size);
}
pub fn make_current(&mut self) {
if !self.windowed_context.is_current() {
self.windowed_context
.replace_with(|context| unsafe { context.make_current().expect("context swap") });
}
}
/// Disable macOS window shadows.
///
/// This prevents rendering artifacts from showing up when the window is transparent.
@ -496,3 +511,44 @@ unsafe extern "C" fn xembed_error_handler(_: *mut XDisplay, _: *mut XErrorEvent)
log::error!("Could not embed into specified window.");
std::process::exit(1);
}
/// Struct for safe in-place replacement.
///
/// This struct allows easily replacing struct fields that provide `self -> Self` methods in-place,
/// without having to deal with constantly unwrapping the underlying [`Option`].
struct Replaceable<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()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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,

145
alacritty/src/ipc.rs Normal file
View File

@ -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")
}

View File

@ -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.

View File

@ -14,20 +14,16 @@ compile_error!(r#"at least one of the "x11"/"wayland" features must be enabled"#
#[cfg(target_os = "macos")]
use std::env;
use std::error::Error;
use std::fs;
use std::io::{self, Write};
use std::sync::Arc;
use std::path::PathBuf;
use std::string::ToString;
use std::{fs, process};
use glutin::event_loop::EventLoop as GlutinEventLoop;
use log::{error, info};
use log::info;
#[cfg(windows)]
use winapi::um::wincon::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS};
use alacritty_terminal::event_loop::{self, EventLoop, Msg};
use alacritty_terminal::grid::Dimensions;
use alacritty_terminal::sync::FairMutex;
use alacritty_terminal::term::Term;
use alacritty_terminal::tty;
mod cli;
@ -37,6 +33,8 @@ mod daemon;
mod display;
mod event;
mod input;
#[cfg(unix)]
mod ipc;
mod logging;
#[cfg(target_os = "macos")]
mod macos;
@ -45,6 +43,7 @@ mod message_bar;
mod panic;
mod renderer;
mod scheduler;
mod window_context;
mod gl {
#![allow(clippy::all)]
@ -52,12 +51,14 @@ mod gl {
}
use crate::cli::Options;
#[cfg(unix)]
use crate::cli::{MessageOptions, Subcommands};
use crate::config::{monitor, Config};
use crate::display::Display;
use crate::event::{Event, EventProxy, Processor};
use crate::event::{Event, Processor};
#[cfg(unix)]
use crate::ipc::SOCKET_MESSAGE_CREATE_WINDOW;
#[cfg(target_os = "macos")]
use crate::macos::locale;
use crate::message_bar::MessageBuffer;
fn main() {
#[cfg(windows)]
@ -74,6 +75,61 @@ fn main() {
// Load command line options.
let options = Options::new();
#[cfg(unix)]
let result = match options.subcommands {
Some(Subcommands::Msg(options)) => msg(options),
None => alacritty(options),
};
#[cfg(not(unix))]
let result = alacritty(options);
// Handle command failure.
if let Err(err) = result {
eprintln!("Error: {}", err);
process::exit(1);
}
}
/// `msg` subcommand entrypoint.
#[cfg(unix)]
fn msg(options: MessageOptions) -> Result<(), String> {
ipc::send_message(options.socket, &SOCKET_MESSAGE_CREATE_WINDOW).map_err(|err| err.to_string())
}
/// Temporary files stored for Alacritty.
///
/// This stores temporary files to automate their destruction through its `Drop` implementation.
struct TemporaryFiles {
#[cfg(unix)]
socket_path: Option<PathBuf>,
log_file: Option<PathBuf>,
}
impl Drop for TemporaryFiles {
fn drop(&mut self) {
// Clean up the IPC socket file.
#[cfg(unix)]
if let Some(socket_path) = &self.socket_path {
let _ = fs::remove_file(socket_path);
}
// Clean up logfile.
if let Some(log_file) = &self.log_file {
if fs::remove_file(log_file).is_ok() {
let _ = writeln!(io::stdout(), "Deleted log file at \"{}\"", log_file.display());
}
}
}
}
/// Run main Alacritty entrypoint.
///
/// Creates a window, the terminal state, PTY, I/O event loop, input processor,
/// config change monitor, and runs the main display loop.
fn alacritty(options: Options) -> Result<(), String> {
info!("Welcome to Alacritty");
// Setup glutin event loop.
let window_event_loop = GlutinEventLoop::<Event>::with_user_event();
@ -83,142 +139,71 @@ fn main() {
// Load configuration file.
let config = config::load(&options);
log_config_path(&config);
// Update the log level from config.
log::set_max_level(config.ui_config.debug.log_level);
// Switch to home directory.
#[cfg(target_os = "macos")]
env::set_current_dir(dirs::home_dir().unwrap()).unwrap();
// Set locale.
#[cfg(target_os = "macos")]
locale::set_locale_environment();
// Store if log file should be deleted before moving config.
let persistent_logging = config.ui_config.debug.persistent_logging;
// Run Alacritty.
if let Err(err) = run(window_event_loop, config, options) {
error!("Alacritty encountered an unrecoverable error:\n\n\t{}\n", err);
std::process::exit(1);
}
// Clean up logfile.
if let Some(log_file) = log_file {
if !persistent_logging && fs::remove_file(&log_file).is_ok() {
let _ = writeln!(io::stdout(), "Deleted log file at \"{}\"", log_file.display());
}
}
}
/// Run Alacritty.
///
/// Creates a window, the terminal state, PTY, I/O event loop, input processor,
/// config change monitor, and runs the main display loop.
fn run(
window_event_loop: GlutinEventLoop<Event>,
config: Config,
options: Options,
) -> Result<(), Box<dyn Error>> {
info!("Welcome to Alacritty");
// Log the configuration paths.
log_config_path(&config);
// Set environment variables.
tty::setup_env(&config);
let event_proxy = EventProxy::new(window_event_loop.create_proxy());
// Switch to home directory.
#[cfg(target_os = "macos")]
env::set_current_dir(dirs::home_dir().unwrap()).unwrap();
// Create a display.
//
// The display manages a window and can draw the terminal.
let display = Display::new(&config, &window_event_loop)?;
info!(
"PTY dimensions: {:?} x {:?}",
display.size_info.screen_lines(),
display.size_info.columns()
);
// Create the terminal.
//
// This object contains all of the state about what's being displayed. It's
// wrapped in a clonable mutex since both the I/O loop and display need to
// access it.
let terminal = Term::new(&config, display.size_info, event_proxy.clone());
let terminal = Arc::new(FairMutex::new(terminal));
// Create the PTY.
//
// The PTY forks a process to run the shell on the slave side of the
// pseudoterminal. A file descriptor for the master side is retained for
// reading/writing to the shell.
let pty = tty::new(&config, &display.size_info, display.window.x11_window_id());
// Create the pseudoterminal I/O loop.
//
// PTY I/O is ran on another thread as to not occupy cycles used by the
// renderer and input processing. Note that access to the terminal state is
// synchronized since the I/O loop updates the state, and the display
// consumes it periodically.
let event_loop = EventLoop::new(
Arc::clone(&terminal),
event_proxy.clone(),
pty,
config.hold,
config.ui_config.debug.ref_test,
);
// The event loop channel allows write requests from the event processor
// to be sent to the pty loop and ultimately written to the pty.
let loop_tx = event_loop.channel();
// Set macOS locale.
#[cfg(target_os = "macos")]
locale::set_locale_environment();
// Create a config monitor when config was loaded from path.
//
// The monitor watches the config file for changes and reloads it. Pending
// config changes are processed in the main loop.
if config.ui_config.live_config_reload {
monitor::watch(config.ui_config.config_paths.clone(), event_proxy);
monitor::watch(config.ui_config.config_paths.clone(), window_event_loop.create_proxy());
}
// Setup storage for message UI.
let message_buffer = MessageBuffer::new();
// Create the IPC socket listener.
#[cfg(unix)]
let socket_path = if config.ui_config.ipc_socket {
ipc::spawn_ipc_socket(&options, window_event_loop.create_proxy())
} else {
None
};
// Setup automatic RAII cleanup for our files.
let log_cleanup = log_file.filter(|_| !config.ui_config.debug.persistent_logging);
let _files = TemporaryFiles {
#[cfg(unix)]
socket_path,
log_file: log_cleanup,
};
// Event processor.
let mut processor = Processor::new(
event_loop::Notifier(loop_tx.clone()),
message_buffer,
config,
display,
options,
);
let mut processor = Processor::new(config, options, &window_event_loop);
// Kick off the I/O thread.
let io_thread = event_loop.spawn();
// Create the first Alacritty window.
let proxy = window_event_loop.create_proxy();
processor.create_window(&window_event_loop, proxy).map_err(|err| err.to_string())?;
info!("Initialisation complete");
// Start event loop and block until shutdown.
processor.run(terminal, window_event_loop);
processor.run(window_event_loop);
// This explicit drop is needed for Windows, ConPTY backend. Otherwise a deadlock can occur.
// The cause:
// - Drop for ConPTY will deadlock if the conout pipe has already been dropped.
// - The conout pipe is dropped when the io_thread is joined below (io_thread owns PTY).
// - ConPTY is dropped when the last of processor and io_thread are dropped, because both of
// them own an Arc<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(())
}

View File

@ -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);

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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),
}
}
}

View File

@ -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,
}
}

View File

@ -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]

View File

@ -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};

View File

@ -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.

31
extra/alacritty-msg.man Normal file
View File

@ -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>

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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"