From 4ddb608563d985060d69594d1004550a680ae3bd Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Wed, 31 Aug 2022 22:48:38 +0000 Subject: [PATCH] Add IPC config subcommand This patch adds a new mechanism for changing configuration options without editing the configuration file, by sending options to running instances through `alacritty msg`. Each window will load Alacritty's configuration file by default and then accept IPC messages for config updates using the `alacritty msg config` subcommand. By default all windows will be updated, individual windows can be addressed using `alacritty msg config --window-id "$ALACRITTY_WINDOW_ID"`. Each option will replace the config's current value and cannot be reset until Alacritty is restarted or the option is overwritten with a new value. Configuration options are passed in the format `field.subfield=value`, where `value` is interpreted as yaml. Closes #472. --- Cargo.lock | 12 ++ Cargo.toml | 1 + alacritty/Cargo.toml | 6 +- alacritty/src/cli.rs | 48 ++++++-- alacritty/src/config/bindings.rs | 4 +- alacritty/src/config/font.rs | 4 +- alacritty/src/config/ui_config.rs | 12 +- alacritty/src/config/window.rs | 4 +- alacritty/src/display/window.rs | 10 -- alacritty/src/event.rs | 36 ++++-- alacritty/src/ipc.rs | 9 ++ alacritty/src/window_context.rs | 103 ++++++++++++---- alacritty_config/Cargo.toml | 14 +++ alacritty_config/src/lib.rs | 64 ++++++++++ alacritty_config_derive/Cargo.toml | 6 +- .../src/{ => config_deserialize}/de_enum.rs | 11 +- .../src/{ => config_deserialize}/de_struct.rs | 69 ++--------- .../src/config_deserialize/mod.rs | 22 ++++ alacritty_config_derive/src/lib.rs | 75 ++++++++++-- alacritty_config_derive/src/serde_replace.rs | 113 ++++++++++++++++++ alacritty_config_derive/tests/config.rs | 38 +++++- alacritty_terminal/Cargo.toml | 4 + alacritty_terminal/src/config/mod.rs | 8 +- alacritty_terminal/src/config/scrolling.rs | 4 +- alacritty_terminal/src/index.rs | 16 ++- alacritty_terminal/src/term/color.rs | 6 +- alacritty_terminal/src/tty/unix.rs | 9 +- alacritty_terminal/src/tty/windows/mod.rs | 2 +- extra/alacritty-msg.man | 27 ++++- extra/completions/_alacritty | 17 +++ extra/completions/alacritty.bash | 27 ++++- extra/completions/alacritty.fish | 12 +- 32 files changed, 629 insertions(+), 164 deletions(-) create mode 100644 alacritty_config/Cargo.toml create mode 100644 alacritty_config/src/lib.rs rename alacritty_config_derive/src/{ => config_deserialize}/de_enum.rs (86%) rename alacritty_config_derive/src/{ => config_deserialize}/de_struct.rs (74%) create mode 100644 alacritty_config_derive/src/config_deserialize/mod.rs create mode 100644 alacritty_config_derive/src/serde_replace.rs diff --git a/Cargo.lock b/Cargo.lock index 06c70622..c786c9a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,7 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" name = "alacritty" version = "0.11.0-dev" dependencies = [ + "alacritty_config", "alacritty_config_derive", "alacritty_terminal", "bitflags", @@ -49,10 +50,20 @@ dependencies = [ "xdg", ] +[[package]] +name = "alacritty_config" +version = "0.1.0" +dependencies = [ + "log", + "serde", + "serde_yaml", +] + [[package]] name = "alacritty_config_derive" version = "0.1.0" dependencies = [ + "alacritty_config", "log", "proc-macro2", "quote", @@ -65,6 +76,7 @@ dependencies = [ name = "alacritty_terminal" version = "0.17.0-dev" dependencies = [ + "alacritty_config", "alacritty_config_derive", "base64", "bitflags", diff --git a/Cargo.toml b/Cargo.toml index 7a6dec80..aad21557 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "alacritty", "alacritty_terminal", + "alacritty_config", "alacritty_config_derive", ] diff --git a/alacritty/Cargo.toml b/alacritty/Cargo.toml index 989f0a83..9e858d70 100644 --- a/alacritty/Cargo.toml +++ b/alacritty/Cargo.toml @@ -18,8 +18,12 @@ default-features = false path = "../alacritty_config_derive" version = "0.1.0" +[dependencies.alacritty_config] +path = "../alacritty_config" +version = "0.1.0" + [dependencies] -clap = { version = "3.0.0", features = ["derive"] } +clap = { version = "3.0.0", features = ["derive", "env"] } log = { version = "0.4", features = ["std", "serde"] } fnv = "1" serde = { version = "1", features = ["derive"] } diff --git a/alacritty/src/cli.rs b/alacritty/src/cli.rs index e9c563d4..e7aae207 100644 --- a/alacritty/src/cli.rs +++ b/alacritty/src/cli.rs @@ -81,14 +81,7 @@ impl Options { let mut options = Self::parse(); // Convert `--option` flags into serde `Value`. - for option in &options.option { - match option_as_value(option) { - Ok(value) => { - options.config_options = serde_utils::merge(options.config_options, value); - }, - Err(_) => eprintln!("Invalid CLI config option: {:?}", option), - } - } + options.config_options = options_as_value(&options.option); options } @@ -132,7 +125,18 @@ impl Options { } } -/// Format an option in the format of `parent.field=value` to a serde Value. +/// Combine multiple options into a [`serde_yaml::Value`]. +pub fn options_as_value(options: &[String]) -> Value { + options.iter().fold(Value::default(), |value, option| match option_as_value(option) { + Ok(new_value) => serde_utils::merge(value, new_value), + Err(_) => { + eprintln!("Ignoring invalid option: {:?}", option); + value + }, + }) +} + +/// Parse an option in the format of `parent.field=value` as a serde Value. fn option_as_value(option: &str) -> Result { let mut yaml_text = String::with_capacity(option.len()); let mut closing_brackets = String::new(); @@ -266,7 +270,7 @@ pub enum Subcommands { #[derive(Args, Debug)] pub struct MessageOptions { /// IPC socket connection path override. - #[clap(long, short, value_hint = ValueHint::FilePath)] + #[clap(short, long, value_hint = ValueHint::FilePath)] pub socket: Option, /// Message which should be sent. @@ -280,9 +284,12 @@ pub struct MessageOptions { pub enum SocketMessage { /// Create a new window in the same Alacritty process. CreateWindow(WindowOptions), + + /// Update the Alacritty configuration. + Config(IpcConfig), } -/// Subset of options that we pass to a 'create-window' subcommand. +/// Subset of options that we pass to 'create-window' IPC subcommand. #[derive(Serialize, Deserialize, Args, Default, Clone, Debug, PartialEq, Eq)] pub struct WindowOptions { /// Terminal options which can be passed via IPC. @@ -294,6 +301,25 @@ pub struct WindowOptions { pub window_identity: WindowIdentity, } +/// Parameters to the `config` IPC subcommand. +#[cfg(unix)] +#[derive(Args, Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +pub struct IpcConfig { + /// Configuration file options [example: cursor.style=Beam]. + #[clap(required = true, value_name = "CONFIG_OPTIONS")] + pub options: Vec, + + /// Window ID for the new config. + /// + /// Use `-1` to apply this change to all windows. + #[clap(short, long, allow_hyphen_values = true, env = "ALACRITTY_WINDOW_ID")] + pub window_id: Option, + + /// Clear all runtime configuration changes. + #[clap(short, long, conflicts_with = "options")] + pub reset: bool, +} + #[cfg(test)] mod tests { use super::*; diff --git a/alacritty/src/config/bindings.rs b/alacritty/src/config/bindings.rs index c48b2d75..72ea88b2 100644 --- a/alacritty/src/config/bindings.rs +++ b/alacritty/src/config/bindings.rs @@ -9,7 +9,7 @@ use serde::de::{self, Error as SerdeError, MapAccess, Unexpected, Visitor}; use serde::{Deserialize, Deserializer}; use serde_yaml::Value as SerdeValue; -use alacritty_config_derive::ConfigDeserialize; +use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; use alacritty_terminal::config::Program; use alacritty_terminal::term::TermMode; @@ -1191,7 +1191,7 @@ impl<'a> Deserialize<'a> for KeyBinding { /// /// Our deserialize impl wouldn't be covered by a derive(Deserialize); see the /// impl below. -#[derive(Debug, Copy, Clone, Hash, Default, Eq, PartialEq)] +#[derive(SerdeReplace, Debug, Copy, Clone, Hash, Default, Eq, PartialEq)] pub struct ModsWrapper(pub ModifiersState); impl ModsWrapper { diff --git a/alacritty/src/config/font.rs b/alacritty/src/config/font.rs index d3431171..9c431b15 100644 --- a/alacritty/src/config/font.rs +++ b/alacritty/src/config/font.rs @@ -4,7 +4,7 @@ use crossfont::Size as FontSize; use serde::de::{self, Visitor}; use serde::{Deserialize, Deserializer}; -use alacritty_config_derive::ConfigDeserialize; +use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; use crate::config::ui_config::Delta; @@ -129,7 +129,7 @@ impl SecondaryFontDescription { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(SerdeReplace, Debug, Clone, PartialEq, Eq)] struct Size(FontSize); impl Default for Size { diff --git a/alacritty/src/config/ui_config.rs b/alacritty/src/config/ui_config.rs index 28139d27..a332b737 100644 --- a/alacritty/src/config/ui_config.rs +++ b/alacritty/src/config/ui_config.rs @@ -9,7 +9,7 @@ use serde::de::{Error as SerdeError, MapAccess, Visitor}; use serde::{self, Deserialize, Deserializer}; use unicode_width::UnicodeWidthChar; -use alacritty_config_derive::ConfigDeserialize; +use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; use alacritty_terminal::config::{ Config as TerminalConfig, Percentage, Program, LOG_TARGET_CONFIG, }; @@ -30,7 +30,7 @@ use crate::config::window::WindowConfig; const URL_REGEX: &str = "(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)\ [^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+"; -#[derive(ConfigDeserialize, Debug, PartialEq)] +#[derive(ConfigDeserialize, Clone, Debug, PartialEq)] pub struct UiConfig { /// Font configuration. pub font: Font, @@ -145,7 +145,7 @@ impl UiConfig { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(SerdeReplace, Clone, Debug, PartialEq, Eq)] struct KeyBindings(Vec); impl Default for KeyBindings { @@ -163,7 +163,7 @@ impl<'de> Deserialize<'de> for KeyBindings { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(SerdeReplace, Clone, Debug, PartialEq, Eq)] struct MouseBindings(Vec); impl Default for MouseBindings { @@ -223,7 +223,7 @@ pub struct Delta { } /// Regex terminal hints. -#[derive(ConfigDeserialize, Debug, PartialEq, Eq)] +#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] pub struct Hints { /// Characters for the hint labels. alphabet: HintsAlphabet, @@ -273,7 +273,7 @@ impl Hints { } } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(SerdeReplace, Clone, Debug, PartialEq, Eq)] struct HintsAlphabet(String); impl Default for HintsAlphabet { diff --git a/alacritty/src/config/window.rs b/alacritty/src/config/window.rs index 80df87b7..5d63d60f 100644 --- a/alacritty/src/config/window.rs +++ b/alacritty/src/config/window.rs @@ -6,7 +6,7 @@ use log::{error, warn}; use serde::de::{self, MapAccess, Visitor}; use serde::{Deserialize, Deserializer, Serialize}; -use alacritty_config_derive::ConfigDeserialize; +use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; use alacritty_terminal::config::{Percentage, LOG_TARGET_CONFIG}; use alacritty_terminal::index::Column; @@ -201,7 +201,7 @@ pub struct Dimensions { } /// Window class hint. -#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +#[derive(SerdeReplace, Serialize, Debug, Clone, PartialEq, Eq)] pub struct Class { pub general: String, pub instance: String, diff --git a/alacritty/src/display/window.rs b/alacritty/src/display/window.rs index eac12a22..95b42345 100644 --- a/alacritty/src/display/window.rs +++ b/alacritty/src/display/window.rs @@ -405,16 +405,6 @@ impl Window { self.window().request_user_attention(attention); } - #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] - pub fn x11_window_id(&self) -> Option { - self.window().xlib_window().map(|xlib_window| xlib_window as usize) - } - - #[cfg(any(not(feature = "x11"), target_os = "macos", windows))] - pub fn x11_window_id(&self) -> Option { - None - } - pub fn id(&self) -> WindowId { self.window().id() } diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index b4258a03..bd4b60b2 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -9,6 +9,7 @@ use std::fmt::Debug; #[cfg(not(windows))] use std::os::unix::io::RawFd; use std::path::PathBuf; +use std::rc::Rc; use std::time::{Duration, Instant}; use std::{env, f32, mem}; @@ -38,6 +39,8 @@ use alacritty_terminal::selection::{Selection, SelectionType}; use alacritty_terminal::term::search::{Match, RegexSearch}; use alacritty_terminal::term::{self, ClipboardType, Term, TermMode}; +#[cfg(unix)] +use crate::cli::IpcConfig; use crate::cli::{Options as CliOptions, WindowOptions}; use crate::clipboard::Clipboard; use crate::config::ui_config::{HintAction, HintInternalAction}; @@ -93,6 +96,8 @@ pub enum EventType { Message(Message), Scroll(Scroll), CreateWindow(WindowOptions), + #[cfg(unix)] + IpcConfig(IpcConfig), BlinkCursor, BlinkCursorTimeout, SearchNext, @@ -184,7 +189,7 @@ pub struct ActionContext<'a, N, T> { pub modifiers: &'a mut ModifiersState, pub display: &'a mut Display, pub message_buffer: &'a mut MessageBuffer, - pub config: &'a mut UiConfig, + pub config: &'a UiConfig, pub cursor_blink_timed_out: &'a mut bool, pub event_loop: &'a EventLoopWindowTarget, pub event_proxy: &'a EventLoopProxy, @@ -1162,6 +1167,8 @@ impl input::Processor> { TerminalEvent::Exit => (), TerminalEvent::CursorBlinkingChange => self.ctx.update_cursor_blinking(), }, + #[cfg(unix)] + EventType::IpcConfig(_) => (), EventType::ConfigReload(_) | EventType::CreateWindow(_) => (), }, GlutinEvent::RedrawRequested(_) => *self.ctx.dirty = true, @@ -1292,7 +1299,7 @@ pub struct Processor { wayland_event_queue: Option, windows: HashMap, cli_options: CliOptions, - config: UiConfig, + config: Rc, } impl Processor { @@ -1313,8 +1320,8 @@ impl Processor { Processor { windows: HashMap::new(), + config: Rc::new(config), cli_options, - config, #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] wayland_event_queue, } @@ -1328,7 +1335,7 @@ impl Processor { options: WindowOptions, ) -> Result<(), Box> { let window_context = WindowContext::new( - &self.config, + self.config.clone(), &options, event_loop, proxy, @@ -1436,7 +1443,6 @@ impl Processor { window_context.handle_event( event_loop, &proxy, - &mut self.config, &mut clipboard, &mut scheduler, GlutinEvent::RedrawEventsCleared, @@ -1457,13 +1463,27 @@ impl Processor { // 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); + self.config = Rc::new(config); for window_context in self.windows.values_mut() { - window_context.update_config(&old_config, &self.config); + window_context.update_config(self.config.clone()); } } }, + // Process IPC config update. + #[cfg(unix)] + GlutinEvent::UserEvent(Event { + payload: EventType::IpcConfig(ipc_config), + window_id, + }) => { + for (_, window_context) in self + .windows + .iter_mut() + .filter(|(id, _)| window_id.is_none() || window_id == Some(**id)) + { + window_context.update_ipc_config(self.config.clone(), ipc_config.clone()); + } + }, // Create a new terminal window. GlutinEvent::UserEvent(Event { payload: EventType::CreateWindow(options), .. @@ -1485,7 +1505,6 @@ impl Processor { window_context.handle_event( event_loop, &proxy, - &mut self.config, &mut clipboard, &mut scheduler, event.clone().into(), @@ -1500,7 +1519,6 @@ impl Processor { window_context.handle_event( event_loop, &proxy, - &mut self.config, &mut clipboard, &mut scheduler, event, diff --git a/alacritty/src/ipc.rs b/alacritty/src/ipc.rs index d4c807ba..e229a048 100644 --- a/alacritty/src/ipc.rs +++ b/alacritty/src/ipc.rs @@ -7,6 +7,7 @@ use std::path::PathBuf; use std::{env, fs, process}; use glutin::event_loop::EventLoopProxy; +use glutin::window::WindowId; use log::warn; use alacritty_terminal::thread; @@ -62,6 +63,14 @@ pub fn spawn_ipc_socket(options: &Options, event_proxy: EventLoopProxy) - let event = Event::new(EventType::CreateWindow(options), None); let _ = event_proxy.send_event(event); }, + SocketMessage::Config(ipc_config) => { + let window_id = ipc_config + .window_id + .and_then(|id| u64::try_from(id).ok()) + .map(WindowId::from); + let event = Event::new(EventType::IpcConfig(ipc_config), window_id); + let _ = event_proxy.send_event(event); + }, } } }); diff --git a/alacritty/src/window_context.rs b/alacritty/src/window_context.rs index e75f118d..9a2a8730 100644 --- a/alacritty/src/window_context.rs +++ b/alacritty/src/window_context.rs @@ -6,6 +6,7 @@ use std::io::Write; use std::mem; #[cfg(not(windows))] use std::os::unix::io::{AsRawFd, RawFd}; +use std::rc::Rc; #[cfg(not(any(target_os = "macos", windows)))] use std::sync::atomic::Ordering; use std::sync::Arc; @@ -14,11 +15,12 @@ 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 log::{error, info}; use serde_json as json; #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] use wayland_client::EventQueue; +use alacritty_config::SerdeReplace; use alacritty_terminal::event::Event as TerminalEvent; use alacritty_terminal::event_loop::{EventLoop as PtyEventLoop, Msg, Notifier}; use alacritty_terminal::grid::{Dimensions, Scroll}; @@ -28,6 +30,8 @@ use alacritty_terminal::term::test::TermSize; use alacritty_terminal::term::{Term, TermMode}; use alacritty_terminal::tty; +#[cfg(unix)] +use crate::cli::IpcConfig; use crate::cli::WindowOptions; use crate::clipboard::Clipboard; use crate::config::UiConfig; @@ -58,12 +62,14 @@ pub struct WindowContext { master_fd: RawFd, #[cfg(not(windows))] shell_pid: u32, + ipc_config: Vec<(String, serde_yaml::Value)>, + config: Rc, } impl WindowContext { /// Create a new terminal window context. pub fn new( - config: &UiConfig, + config: Rc, options: &WindowOptions, window_event_loop: &EventLoopWindowTarget, proxy: EventLoopProxy, @@ -81,7 +87,7 @@ impl WindowContext { // // The display manages a window and can draw the terminal. let display = Display::new( - config, + &config, window_event_loop, &identity, #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] @@ -109,7 +115,7 @@ impl WindowContext { // 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(&pty_config, display.size_info.into(), display.window.x11_window_id())?; + let pty = tty::new(&pty_config, display.size_info.into(), display.window.id().into())?; #[cfg(not(windows))] let master_fd = pty.file().as_raw_fd(); @@ -142,23 +148,27 @@ impl WindowContext { event_proxy.send_event(TerminalEvent::CursorBlinkingChange.into()); } + let font_size = config.font.size(); + // Create context for the Alacritty window. Ok(WindowContext { - font_size: config.font.size(), - notifier: Notifier(loop_tx), + preserve_title, + font_size, terminal, display, - preserve_title, #[cfg(not(windows))] master_fd, #[cfg(not(windows))] shell_pid, + config, + notifier: Notifier(loop_tx), cursor_blink_timed_out: Default::default(), suppress_chars: Default::default(), message_buffer: Default::default(), received_count: Default::default(), search_state: Default::default(), event_queue: Default::default(), + ipc_config: Default::default(), modifiers: Default::default(), mouse: Default::default(), dirty: Default::default(), @@ -167,33 +177,49 @@ impl WindowContext { } /// Update the terminal window to the latest config. - pub fn update_config(&mut self, old_config: &UiConfig, config: &UiConfig) { - self.display.update_config(config); - self.terminal.lock().update_config(&config.terminal_config); + pub fn update_config(&mut self, new_config: Rc) { + let old_config = mem::replace(&mut self.config, new_config); + + // Apply ipc config if there are overrides. + if !self.ipc_config.is_empty() { + let mut config = (*self.config).clone(); + + // Apply each option. + for (key, value) in &self.ipc_config { + if let Err(err) = config.replace(key, value.clone()) { + error!("Unable to override option '{}': {}", key, err); + } + } + + self.config = Rc::new(config); + } + + self.display.update_config(&self.config); + self.terminal.lock().update_config(&self.config.terminal_config); // Reload cursor if its thickness has changed. if (old_config.terminal_config.cursor.thickness() - - config.terminal_config.cursor.thickness()) + - self.config.terminal_config.cursor.thickness()) .abs() > f32::EPSILON { self.display.pending_update.set_cursor_dirty(); } - if old_config.font != config.font { + if old_config.font != self.config.font { // Do not update font size if it has been changed at runtime. if self.font_size == old_config.font.size() { - self.font_size = config.font.size(); + self.font_size = self.config.font.size(); } - let font = config.font.clone().with_size(self.font_size); + let font = self.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.window; - if window_config.padding(1.) != config.window.padding(1.) - || window_config.dynamic_padding != config.window.dynamic_padding + if window_config.padding(1.) != self.config.window.padding(1.) + || window_config.dynamic_padding != self.config.window.dynamic_padding { self.display.pending_update.dirty = true; } @@ -206,18 +232,18 @@ impl WindowContext { // │ N │ Y │ N ││ N │ // │ N │ N │ _ ││ Y │ if !self.preserve_title - && (!config.window.dynamic_title + && (!self.config.window.dynamic_title || self.display.window.title() == old_config.window.identity.title) { - self.display.window.set_title(config.window.identity.title.clone()); + self.display.window.set_title(self.config.window.identity.title.clone()); } // Disable shadows for transparent windows on macOS. #[cfg(target_os = "macos")] - self.display.window.set_has_shadow(config.window_opacity() >= 1.0); + self.display.window.set_has_shadow(self.config.window_opacity() >= 1.0); // Update hint keys. - self.display.hint_state.update_alphabet(config.hints.alphabet()); + self.display.hint_state.update_alphabet(self.config.hints.alphabet()); // Update cursor blinking. let event = Event::new(TerminalEvent::CursorBlinkingChange.into(), None); @@ -226,12 +252,39 @@ impl WindowContext { self.dirty = true; } + /// Update the IPC config overrides. + #[cfg(unix)] + pub fn update_ipc_config(&mut self, config: Rc, ipc_config: IpcConfig) { + self.ipc_config.clear(); + + if !ipc_config.reset { + for option in &ipc_config.options { + // Separate config key/value. + let (key, value) = match option.split_once('=') { + Some(split) => split, + None => { + error!("'{}': IPC config option missing value", option); + continue; + }, + }; + + // Try and parse value as yaml. + match serde_yaml::from_str(value) { + Ok(value) => self.ipc_config.push((key.to_owned(), value)), + Err(err) => error!("'{}': Invalid IPC config value: {:?}", option, err), + } + } + } + + // Reload current config to pull new IPC config. + self.update_config(config); + } + /// Process events for this terminal window. pub fn handle_event( &mut self, event_loop: &EventLoopWindowTarget, event_proxy: &EventLoopProxy, - config: &mut UiConfig, clipboard: &mut Clipboard, scheduler: &mut Scheduler, event: GlutinEvent<'_, Event>, @@ -285,11 +338,11 @@ impl WindowContext { #[cfg(not(windows))] shell_pid: self.shell_pid, preserve_title: self.preserve_title, + config: &self.config, event_proxy, event_loop, clipboard, scheduler, - config, }; let mut processor = input::Processor::new(context); @@ -306,7 +359,7 @@ impl WindowContext { &self.message_buffer, &self.search_state, old_is_searching, - config, + &self.config, ); self.dirty = true; } @@ -314,7 +367,7 @@ impl WindowContext { if self.dirty || self.mouse.hint_highlight_dirty { self.dirty |= self.display.update_highlighted_hints( &terminal, - config, + &self.config, &self.mouse, self.modifiers, ); @@ -339,7 +392,7 @@ impl WindowContext { } // Redraw screen. - self.display.draw(terminal, &self.message_buffer, config, &self.search_state); + self.display.draw(terminal, &self.message_buffer, &self.config, &self.search_state); } } diff --git a/alacritty_config/Cargo.toml b/alacritty_config/Cargo.toml new file mode 100644 index 00000000..3b287f43 --- /dev/null +++ b/alacritty_config/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "alacritty_config" +version = "0.1.0" +authors = ["Christian Duerr "] +license = "MIT/Apache-2.0" +description = "Alacritty configuration abstractions" +homepage = "https://github.com/alacritty/alacritty" +edition = "2021" +rust-version = "1.57.0" + +[dependencies] +log = { version = "0.4.17", features = ["serde"] } +serde_yaml = "0.8.24" +serde = "1.0.137" diff --git a/alacritty_config/src/lib.rs b/alacritty_config/src/lib.rs new file mode 100644 index 00000000..7a467650 --- /dev/null +++ b/alacritty_config/src/lib.rs @@ -0,0 +1,64 @@ +use std::collections::HashMap; +use std::error::Error; + +use log::LevelFilter; +use serde::Deserialize; +use serde_yaml::Value; + +pub trait SerdeReplace { + fn replace(&mut self, key: &str, value: Value) -> Result<(), Box>; +} + +macro_rules! impl_replace { + ($($ty:ty),*$(,)*) => { + $( + impl SerdeReplace for $ty { + fn replace(&mut self, key: &str, value: Value) -> Result<(), Box> { + replace_simple(self, key, value) + } + } + )* + }; +} + +#[rustfmt::skip] +impl_replace!( + usize, u8, u16, u32, u64, u128, + isize, i8, i16, i32, i64, i128, + f32, f64, + bool, + char, + String, + LevelFilter, +); + +fn replace_simple<'de, D>(data: &mut D, key: &str, value: Value) -> Result<(), Box> +where + D: Deserialize<'de>, +{ + if !key.is_empty() { + let error = format!("Fields \"{}\" do not exist", key); + return Err(error.into()); + } + *data = D::deserialize(value)?; + + Ok(()) +} + +impl<'de, T: Deserialize<'de>> SerdeReplace for Vec { + fn replace(&mut self, key: &str, value: Value) -> Result<(), Box> { + replace_simple(self, key, value) + } +} + +impl<'de, T: Deserialize<'de>> SerdeReplace for Option { + fn replace(&mut self, key: &str, value: Value) -> Result<(), Box> { + replace_simple(self, key, value) + } +} + +impl<'de, T: Deserialize<'de>> SerdeReplace for HashMap { + fn replace(&mut self, key: &str, value: Value) -> Result<(), Box> { + replace_simple(self, key, value) + } +} diff --git a/alacritty_config_derive/Cargo.toml b/alacritty_config_derive/Cargo.toml index 8584d0d2..c901815e 100644 --- a/alacritty_config_derive/Cargo.toml +++ b/alacritty_config_derive/Cargo.toml @@ -16,7 +16,11 @@ syn = { version = "1.0.53", features = ["derive", "parsing", "proc-macro", "prin proc-macro2 = "1.0.24" quote = "1.0.7" +[dev-dependencies.alacritty_config] +path = "../alacritty_config" +version = "0.1.0" + [dev-dependencies] +serde = { version = "1.0.117", features = ["derive"] } serde_yaml = "0.8.14" -serde = "1.0.117" log = "0.4.11" diff --git a/alacritty_config_derive/src/de_enum.rs b/alacritty_config_derive/src/config_deserialize/de_enum.rs similarity index 86% rename from alacritty_config_derive/src/de_enum.rs rename to alacritty_config_derive/src/config_deserialize/de_enum.rs index 98247c0c..73634e73 100644 --- a/alacritty_config_derive/src/de_enum.rs +++ b/alacritty_config_derive/src/config_deserialize/de_enum.rs @@ -1,9 +1,11 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; -use syn::{DataEnum, Ident}; +use syn::{DataEnum, Generics, Ident}; -pub fn derive_deserialize(ident: Ident, data_enum: DataEnum) -> TokenStream { +use crate::serde_replace; + +pub fn derive_deserialize(ident: Ident, generics: Generics, data_enum: DataEnum) -> TokenStream { let visitor = format_ident!("{}Visitor", ident); // Create match arm streams and get a list with all available values. @@ -30,7 +32,7 @@ pub fn derive_deserialize(ident: Ident, data_enum: DataEnum) -> TokenStream { available_values.truncate(available_values.len().saturating_sub(2)); // Generate deserialization impl. - let tokens = quote! { + let mut tokens = quote! { struct #visitor; impl<'de> serde::de::Visitor<'de> for #visitor { type Value = #ident; @@ -62,5 +64,8 @@ pub fn derive_deserialize(ident: Ident, data_enum: DataEnum) -> TokenStream { } }; + // Automatically implement [`alacritty_config::SerdeReplace`]. + tokens.extend(serde_replace::derive_direct(ident, generics)); + tokens.into() } diff --git a/alacritty_config_derive/src/de_struct.rs b/alacritty_config_derive/src/config_deserialize/de_struct.rs similarity index 74% rename from alacritty_config_derive/src/de_struct.rs rename to alacritty_config_derive/src/config_deserialize/de_struct.rs index cf7ea141..4245764f 100644 --- a/alacritty_config_derive/src/de_struct.rs +++ b/alacritty_config_derive/src/config_deserialize/de_struct.rs @@ -1,13 +1,12 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; -use syn::parse::{self, Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::spanned::Spanned; -use syn::{Error, Field, GenericParam, Generics, Ident, LitStr, Token, Type, TypeParam}; +use syn::{Error, Field, Generics, Ident, Type}; + +use crate::{serde_replace, Attr, GenericsStreams, MULTIPLE_FLATTEN_ERROR}; -/// Error message when attempting to flatten multiple fields. -const MULTIPLE_FLATTEN_ERROR: &str = "At most one instance of #[config(flatten)] is supported"; /// Use this crate's name as log target. const LOG_TARGET: &str = env!("CARGO_PKG_NAME"); @@ -18,20 +17,20 @@ pub fn derive_deserialize( ) -> TokenStream { // Create all necessary tokens for the implementation. let GenericsStreams { unconstrained, constrained, phantoms } = - generics_streams(generics.params); + crate::generics_streams(&generics.params); let FieldStreams { flatten, match_assignments } = fields_deserializer(&fields); let visitor = format_ident!("{}Visitor", ident); // Generate deserialization impl. - let tokens = quote! { + let mut tokens = quote! { #[derive(Default)] #[allow(non_snake_case)] - struct #visitor < #unconstrained > { + struct #visitor <#unconstrained> { #phantoms } - impl<'de, #constrained> serde::de::Visitor<'de> for #visitor < #unconstrained > { - type Value = #ident < #unconstrained >; + impl <'de, #constrained> serde::de::Visitor<'de> for #visitor <#unconstrained> { + type Value = #ident <#unconstrained>; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a mapping") @@ -61,7 +60,7 @@ pub fn derive_deserialize( } } - impl<'de, #constrained> serde::Deserialize<'de> for #ident < #unconstrained > { + impl <'de, #constrained> serde::Deserialize<'de> for #ident <#unconstrained> { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -71,6 +70,9 @@ pub fn derive_deserialize( } }; + // Automatically implement [`alacritty_config::SerdeReplace`]. + tokens.extend(serde_replace::derive_recursive(ident, generics, fields)); + tokens.into() } @@ -177,50 +179,3 @@ fn field_deserializer(field_streams: &mut FieldStreams, field: &Field) -> Result Ok(()) } - -/// Field attribute. -struct Attr { - ident: String, - param: Option, -} - -impl Parse for Attr { - fn parse(input: ParseStream<'_>) -> parse::Result { - let ident = input.parse::()?.to_string(); - let param = input.parse::().and_then(|_| input.parse()).ok(); - Ok(Self { ident, param }) - } -} - -/// Storage for all necessary generics information. -#[derive(Default)] -struct GenericsStreams { - unconstrained: TokenStream2, - constrained: TokenStream2, - phantoms: TokenStream2, -} - -/// Create the necessary generics annotations. -/// -/// This will create three different token streams, which might look like this: -/// - unconstrained: `T` -/// - constrained: `T: Default + Deserialize<'de>` -/// - phantoms: `T: PhantomData,` -fn generics_streams(params: Punctuated) -> GenericsStreams { - let mut generics = GenericsStreams::default(); - - for generic in params { - // NOTE: Lifetimes and const params are not supported. - if let GenericParam::Type(TypeParam { ident, .. }) = generic { - generics.unconstrained.extend(quote!( #ident , )); - generics.constrained.extend(quote! { - #ident : Default + serde::Deserialize<'de> , - }); - generics.phantoms.extend(quote! { - #ident : std::marker::PhantomData < #ident >, - }); - } - } - - generics -} diff --git a/alacritty_config_derive/src/config_deserialize/mod.rs b/alacritty_config_derive/src/config_deserialize/mod.rs new file mode 100644 index 00000000..b1923377 --- /dev/null +++ b/alacritty_config_derive/src/config_deserialize/mod.rs @@ -0,0 +1,22 @@ +use proc_macro::TokenStream; +use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Error, Fields}; + +/// Error if the derive was used on an unsupported type. +const UNSUPPORTED_ERROR: &str = "ConfigDeserialize must be used on an enum or struct with fields"; + +mod de_enum; +mod de_struct; + +pub fn derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + match input.data { + Data::Struct(DataStruct { fields: Fields::Named(fields), .. }) => { + de_struct::derive_deserialize(input.ident, input.generics, fields.named) + }, + Data::Enum(data_enum) => { + de_enum::derive_deserialize(input.ident, input.generics, data_enum) + }, + _ => Error::new(input.ident.span(), UNSUPPORTED_ERROR).to_compile_error().into(), + } +} diff --git a/alacritty_config_derive/src/lib.rs b/alacritty_config_derive/src/lib.rs index af8f2e7f..116d4828 100644 --- a/alacritty_config_derive/src/lib.rs +++ b/alacritty_config_derive/src/lib.rs @@ -2,25 +2,27 @@ #![cfg_attr(feature = "cargo-clippy", deny(warnings))] use proc_macro::TokenStream; -use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Error, Fields, Path}; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::parse::{self, Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::{GenericParam, Ident, LitStr, Path, Token, TypeParam}; -mod de_enum; -mod de_struct; +mod config_deserialize; +mod serde_replace; -/// Error if the derive was used on an unsupported type. -const UNSUPPORTED_ERROR: &str = "ConfigDeserialize must be used on a struct with fields"; +/// Error message when attempting to flatten multiple fields. +pub(crate) const MULTIPLE_FLATTEN_ERROR: &str = + "At most one instance of #[config(flatten)] is supported"; #[proc_macro_derive(ConfigDeserialize, attributes(config))] pub fn derive_config_deserialize(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); + config_deserialize::derive(input) +} - match input.data { - Data::Struct(DataStruct { fields: Fields::Named(fields), .. }) => { - de_struct::derive_deserialize(input.ident, input.generics, fields.named) - }, - Data::Enum(data_enum) => de_enum::derive_deserialize(input.ident, data_enum), - _ => Error::new(input.ident.span(), UNSUPPORTED_ERROR).to_compile_error().into(), - } +#[proc_macro_derive(SerdeReplace)] +pub fn derive_serde_replace(input: TokenStream) -> TokenStream { + serde_replace::derive(input) } /// Verify that a token path ends with a specific segment. @@ -28,3 +30,50 @@ pub(crate) fn path_ends_with(path: &Path, segment: &str) -> bool { let segments = path.segments.iter(); segments.last().map_or(false, |s| s.ident == segment) } + +/// Storage for all necessary generics information. +#[derive(Default)] +struct GenericsStreams { + unconstrained: TokenStream2, + constrained: TokenStream2, + phantoms: TokenStream2, +} + +/// Create the necessary generics annotations. +/// +/// This will create three different token streams, which might look like this: +/// - unconstrained: `T` +/// - constrained: `T: Default + Deserialize<'de>` +/// - phantoms: `T: PhantomData,` +pub(crate) fn generics_streams(params: &Punctuated) -> GenericsStreams { + let mut generics = GenericsStreams::default(); + + for generic in params { + // NOTE: Lifetimes and const params are not supported. + if let GenericParam::Type(TypeParam { ident, .. }) = generic { + generics.unconstrained.extend(quote!( #ident , )); + generics.constrained.extend(quote! { + #ident : Default + serde::Deserialize<'de> + alacritty_config::SerdeReplace, + }); + generics.phantoms.extend(quote! { + #ident : std::marker::PhantomData < #ident >, + }); + } + } + + generics +} + +/// Field attribute. +pub(crate) struct Attr { + ident: String, + param: Option, +} + +impl Parse for Attr { + fn parse(input: ParseStream<'_>) -> parse::Result { + let ident = input.parse::()?.to_string(); + let param = input.parse::().and_then(|_| input.parse()).ok(); + Ok(Self { ident, param }) + } +} diff --git a/alacritty_config_derive/src/serde_replace.rs b/alacritty_config_derive/src/serde_replace.rs new file mode 100644 index 00000000..4a0a6a99 --- /dev/null +++ b/alacritty_config_derive/src/serde_replace.rs @@ -0,0 +1,113 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::punctuated::Punctuated; +use syn::{ + parse_macro_input, Data, DataStruct, DeriveInput, Error, Field, Fields, Generics, Ident, +}; + +use crate::{Attr, GenericsStreams, MULTIPLE_FLATTEN_ERROR}; + +/// Error if the derive was used on an unsupported type. +const UNSUPPORTED_ERROR: &str = "SerdeReplace must be used on a tuple struct"; + +pub fn derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + match input.data { + Data::Struct(DataStruct { fields: Fields::Unnamed(_), .. }) | Data::Enum(_) => { + derive_direct(input.ident, input.generics).into() + }, + Data::Struct(DataStruct { fields: Fields::Named(fields), .. }) => { + derive_recursive(input.ident, input.generics, fields.named).into() + }, + _ => Error::new(input.ident.span(), UNSUPPORTED_ERROR).to_compile_error().into(), + } +} + +pub fn derive_direct(ident: Ident, generics: Generics) -> TokenStream2 { + quote! { + impl <#generics> alacritty_config::SerdeReplace for #ident <#generics> { + fn replace(&mut self, key: &str, value: serde_yaml::Value) -> Result<(), Box> { + if !key.is_empty() { + let error = format!("Fields \"{}\" do not exist", key); + return Err(error.into()); + } + *self = serde::Deserialize::deserialize(value)?; + + Ok(()) + } + } + } +} + +pub fn derive_recursive( + ident: Ident, + generics: Generics, + fields: Punctuated, +) -> TokenStream2 { + let GenericsStreams { unconstrained, constrained, .. } = + crate::generics_streams(&generics.params); + let replace_arms = match_arms(&fields); + + quote! { + #[allow(clippy::extra_unused_lifetimes)] + impl <'de, #constrained> alacritty_config::SerdeReplace for #ident <#unconstrained> { + fn replace(&mut self, key: &str, value: serde_yaml::Value) -> Result<(), Box> { + if key.is_empty() { + *self = serde::Deserialize::deserialize(value)?; + return Ok(()); + } + + let (field, next_key) = key.split_once('.').unwrap_or((key, "")); + match field { + #replace_arms + _ => { + let error = format!("Field \"{}\" does not exist", field); + return Err(error.into()); + }, + } + + Ok(()) + } + } + } +} + +/// Create SerdeReplace recursive match arms. +fn match_arms(fields: &Punctuated) -> TokenStream2 { + let mut stream = TokenStream2::default(); + let mut flattened_arm = None; + + // Create arm for each field. + for field in fields { + let ident = field.ident.as_ref().expect("unreachable tuple struct"); + let literal = ident.to_string(); + + // Check if #[config(flattened)] attribute is present. + let flatten = field + .attrs + .iter() + .filter_map(|attr| attr.parse_args::().ok()) + .any(|parsed| parsed.ident.as_str() == "flatten"); + + if flatten && flattened_arm.is_some() { + return Error::new(ident.span(), MULTIPLE_FLATTEN_ERROR).to_compile_error(); + } else if flatten { + flattened_arm = Some(quote! { + _ => alacritty_config::SerdeReplace::replace(&mut self.#ident, key, value)?, + }); + } else { + stream.extend(quote! { + #literal => alacritty_config::SerdeReplace::replace(&mut self.#ident, next_key, value)?, + }); + } + } + + // Add the flattened catch-all as last match arm. + if let Some(flattened_arm) = flattened_arm.take() { + stream.extend(flattened_arm); + } + + stream +} diff --git a/alacritty_config_derive/tests/config.rs b/alacritty_config_derive/tests/config.rs index 4828b822..bd449ff8 100644 --- a/alacritty_config_derive/tests/config.rs +++ b/alacritty_config_derive/tests/config.rs @@ -1,8 +1,10 @@ use std::sync::{Arc, Mutex}; use log::{Level, Log, Metadata, Record}; +use serde::Deserialize; -use alacritty_config_derive::ConfigDeserialize; +use alacritty_config::SerdeReplace as _; +use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; #[derive(ConfigDeserialize, Debug, PartialEq, Eq)] enum TestEnum { @@ -63,6 +65,7 @@ struct Test2 { field3: usize, #[config(alias = "aliased")] field4: u8, + newtype: NewType, } #[derive(ConfigDeserialize, Default)] @@ -70,6 +73,9 @@ struct Test3 { flatty: usize, } +#[derive(SerdeReplace, Deserialize, Default, PartialEq, Eq, Debug)] +struct NewType(usize); + #[test] fn config_deserialize() { let logger = unsafe { @@ -159,3 +165,33 @@ impl Log for Logger { fn flush(&self) {} } + +#[test] +fn field_replacement() { + let mut test = Test::default(); + + let value = serde_yaml::to_value(13).unwrap(); + test.replace("nesting.field2", value).unwrap(); + + assert_eq!(test.nesting.field2, Some(13)); +} + +#[test] +fn replace_derive() { + let mut test = Test::default(); + + let value = serde_yaml::to_value(9).unwrap(); + test.replace("nesting.newtype", value).unwrap(); + + assert_eq!(test.nesting.newtype, NewType(9)); +} + +#[test] +fn replace_flatten() { + let mut test = Test::default(); + + let value = serde_yaml::to_value(7).unwrap(); + test.replace("flatty", value).unwrap(); + + assert_eq!(test.flatten.flatty, 7); +} diff --git a/alacritty_terminal/Cargo.toml b/alacritty_terminal/Cargo.toml index fb5d2d22..06d3961a 100644 --- a/alacritty_terminal/Cargo.toml +++ b/alacritty_terminal/Cargo.toml @@ -13,6 +13,10 @@ rust-version = "1.57.0" path = "../alacritty_config_derive" version = "0.1.0" +[dependencies.alacritty_config] +path = "../alacritty_config" +version = "0.1.0" + [dependencies] libc = "0.2" bitflags = "1" diff --git a/alacritty_terminal/src/config/mod.rs b/alacritty_terminal/src/config/mod.rs index 5822d591..53a0eb77 100644 --- a/alacritty_terminal/src/config/mod.rs +++ b/alacritty_terminal/src/config/mod.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use serde::Deserialize; -use alacritty_config_derive::ConfigDeserialize; +use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; mod scrolling; @@ -17,7 +17,7 @@ pub const LOG_TARGET_CONFIG: &str = "alacritty_config_derive"; const MIN_BLINK_INTERVAL: u64 = 10; /// Top-level config type. -#[derive(ConfigDeserialize, Debug, PartialEq, Default)] +#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Default)] pub struct Config { /// TERM env variable. pub env: HashMap, @@ -125,7 +125,7 @@ impl Cursor { } } -#[derive(Deserialize, Debug, Copy, Clone, PartialEq, Eq)] +#[derive(SerdeReplace, Deserialize, Debug, Copy, Clone, PartialEq, Eq)] #[serde(untagged)] pub enum ConfigCursorStyle { Shape(CursorShape), @@ -222,7 +222,7 @@ impl Program { } /// Wrapper around f32 that represents a percentage value between 0.0 and 1.0. -#[derive(Deserialize, Clone, Copy, Debug, PartialEq)] +#[derive(SerdeReplace, Deserialize, Clone, Copy, Debug, PartialEq)] pub struct Percentage(f32); impl Default for Percentage { diff --git a/alacritty_terminal/src/config/scrolling.rs b/alacritty_terminal/src/config/scrolling.rs index 9a5a718c..f4e55787 100644 --- a/alacritty_terminal/src/config/scrolling.rs +++ b/alacritty_terminal/src/config/scrolling.rs @@ -1,7 +1,7 @@ use serde::de::Error as SerdeError; use serde::{Deserialize, Deserializer}; -use alacritty_config_derive::ConfigDeserialize; +use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; /// Maximum scrollback amount configurable. pub const MAX_SCROLLBACK_LINES: u32 = 100_000; @@ -31,7 +31,7 @@ impl Scrolling { } } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(SerdeReplace, Copy, Clone, Debug, PartialEq, Eq)] struct ScrollingHistory(u32); impl Default for ScrollingHistory { diff --git a/alacritty_terminal/src/index.rs b/alacritty_terminal/src/index.rs index e672c752..9464b8d8 100644 --- a/alacritty_terminal/src/index.rs +++ b/alacritty_terminal/src/index.rs @@ -7,6 +7,8 @@ use std::ops::{Add, AddAssign, Deref, Sub, SubAssign}; use serde::{Deserialize, Serialize}; +use alacritty_config_derive::SerdeReplace; + use crate::grid::Dimensions; /// The side of a cell. @@ -222,7 +224,19 @@ impl PartialEq for Line { /// A column. /// /// Newtype to avoid passing values incorrectly. -#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq, Default, Ord, PartialOrd)] +#[derive( + SerdeReplace, + Serialize, + Deserialize, + Debug, + Copy, + Clone, + Eq, + PartialEq, + Default, + Ord, + PartialOrd, +)] pub struct Column(pub usize); impl fmt::Display for Column { diff --git a/alacritty_terminal/src/term/color.rs b/alacritty_terminal/src/term/color.rs index 1cfdec6b..3c545b28 100644 --- a/alacritty_terminal/src/term/color.rs +++ b/alacritty_terminal/src/term/color.rs @@ -7,12 +7,14 @@ use serde::de::{Error as _, Visitor}; use serde::{Deserialize, Deserializer, Serialize}; use serde_yaml::Value; +use alacritty_config_derive::SerdeReplace; + use crate::ansi::NamedColor; /// Number of terminal colors. pub const COUNT: usize = 269; -#[derive(Debug, Eq, PartialEq, Copy, Clone, Default, Serialize)] +#[derive(SerdeReplace, Debug, Eq, PartialEq, Copy, Clone, Default, Serialize)] pub struct Rgb { pub r: u8, pub g: u8, @@ -170,7 +172,7 @@ impl FromStr for Rgb { } /// RGB color optionally referencing the cell's foreground or background. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(SerdeReplace, Copy, Clone, Debug, PartialEq, Eq)] pub enum CellRgb { CellForeground, CellBackground, diff --git a/alacritty_terminal/src/tty/unix.rs b/alacritty_terminal/src/tty/unix.rs index f52f0920..63da4f9c 100644 --- a/alacritty_terminal/src/tty/unix.rs +++ b/alacritty_terminal/src/tty/unix.rs @@ -148,7 +148,7 @@ fn default_shell_command(pw: &Passwd<'_>) -> Command { } /// Create a new TTY and return a handle to interact with it. -pub fn new(config: &PtyConfig, window_size: WindowSize, window_id: Option) -> Result { +pub fn new(config: &PtyConfig, window_size: WindowSize, window_id: u64) -> Result { let (master, slave) = make_pty(window_size.to_winsize())?; #[cfg(any(target_os = "linux", target_os = "macos"))] @@ -178,13 +178,14 @@ pub fn new(config: &PtyConfig, window_size: WindowSize, window_id: Option builder.stdout(unsafe { Stdio::from_raw_fd(slave) }); // Setup shell environment. + let window_id = window_id.to_string(); + builder.env("ALACRITTY_WINDOW_ID", &window_id); builder.env("LOGNAME", pw.name); builder.env("USER", pw.name); builder.env("HOME", pw.dir); - if let Some(window_id) = window_id { - builder.env("WINDOWID", format!("{}", window_id)); - } + // Set Window ID for clients relying on X11 hacks. + builder.env("WINDOWID", window_id); unsafe { builder.pre_exec(move || { diff --git a/alacritty_terminal/src/tty/windows/mod.rs b/alacritty_terminal/src/tty/windows/mod.rs index aa21ce14..57925f4c 100644 --- a/alacritty_terminal/src/tty/windows/mod.rs +++ b/alacritty_terminal/src/tty/windows/mod.rs @@ -27,7 +27,7 @@ pub struct Pty { child_watcher: ChildExitWatcher, } -pub fn new(config: &PtyConfig, window_size: WindowSize, _window_id: Option) -> Result { +pub fn new(config: &PtyConfig, window_size: WindowSize, _window_id: u64) -> Result { conpty::new(config, window_size) .ok_or_else(|| Error::new(ErrorKind::Other, "failed to spawn conpty")) } diff --git a/extra/alacritty-msg.man b/extra/alacritty-msg.man index ed600d40..f4a34f46 100644 --- a/extra/alacritty-msg.man +++ b/extra/alacritty-msg.man @@ -10,20 +10,43 @@ making it possible to control Alacritty without directly accessing it. \fB\-s\fR, \fB\-\-socket\fR Path for IPC socket creation .SH "MESSAGES" +.TP \fBcreate-window\fR Create a new window in the same Alacritty process .TP .SH "\tOPTIONS" .RS 12 +.TP \fB\-\-hold\fR Remain open after child process exits - +.TP \fB\-\-working\-directory\fR Start the shell in the specified working directory - +.TP \fB\-e\fR, \fB\-\-command\fR ... Command and args to execute (must be last argument) .RE +.TP +\fBconfig\fR +Update the Alacritty configuration +.TP +.SH "\tARGS" +.RS 12 +.TP +\fB...\fR +Configuration file options [example: cursor.style=Beam] +.RE +.TP +.SH "\tOPTIONS" +.RS 12 +.TP +\fB\-w\fR, \fB\-\-window\-id\fR +Window ID for the new config. + +Use `-1` to apply this change to all windows. + +[default: \fB$ALACRITTY_WINDOW_ID\fR] +.RE .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 50a5c00d..6b80a797 100644 --- a/extra/completions/_alacritty +++ b/extra/completions/_alacritty @@ -73,6 +73,17 @@ _arguments "${_arguments_options[@]}" \ '--help[Print help information]' \ && ret=0 ;; +(config) +_arguments "${_arguments_options[@]}" \ +'-w+[Window ID for the new config]:WINDOW_ID: ' \ +'--window-id=[Window ID for the new config]:WINDOW_ID: ' \ +'()-r[Clear all runtime configuration changes]' \ +'()--reset[Clear all runtime configuration changes]' \ +'-h[Print help information]' \ +'--help[Print help information]' \ +'*::options -- Configuration file options \[example\: cursor.style=Beam\]:' \ +&& ret=0 +;; (help) _arguments "${_arguments_options[@]}" \ '*::subcommand -- The subcommand whose help message to display:' \ @@ -100,6 +111,11 @@ _alacritty_commands() { ) _describe -t commands 'alacritty commands' commands "$@" } +(( $+functions[_alacritty__msg__config_commands] )) || +_alacritty__msg__config_commands() { + local commands; commands=() + _describe -t commands 'alacritty msg config commands' commands "$@" +} (( $+functions[_alacritty__msg__create-window_commands] )) || _alacritty__msg__create-window_commands() { local commands; commands=() @@ -119,6 +135,7 @@ _alacritty__msg__help_commands() { _alacritty__msg_commands() { local commands; commands=( 'create-window:Create a new window in the same Alacritty process' \ +'config:Update the Alacritty configuration' \ 'help:Print this message or the help of the given subcommand(s)' \ ) _describe -t commands 'alacritty msg commands' commands "$@" diff --git a/extra/completions/alacritty.bash b/extra/completions/alacritty.bash index 428e9583..5cca6466 100644 --- a/extra/completions/alacritty.bash +++ b/extra/completions/alacritty.bash @@ -12,6 +12,9 @@ _alacritty() { "$1") cmd="alacritty" ;; + config) + cmd+="__config" + ;; create-window) cmd+="__create__window" ;; @@ -100,7 +103,7 @@ _alacritty() { return 0 ;; alacritty__msg) - opts="-s -h --socket --help create-window help" + opts="-s -h --socket --help create-window config help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -121,6 +124,28 @@ _alacritty() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + alacritty__msg__config) + opts="-w -r -h --window-id --reset --help ..." + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --window-id) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -w) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; alacritty__msg__create__window) opts="-e -t -h --working-directory --hold --command --title --class --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then diff --git a/extra/completions/alacritty.fish b/extra/completions/alacritty.fish index 69b3d777..fdc24ab2 100644 --- a/extra/completions/alacritty.fish +++ b/extra/completions/alacritty.fish @@ -15,13 +15,17 @@ complete -c alacritty -n "__fish_use_subcommand" -s v -d 'Increases the level of complete -c alacritty -n "__fish_use_subcommand" -l hold -d 'Remain open after child process exit' complete -c alacritty -n "__fish_use_subcommand" -f -a "msg" -d 'Send a message to the Alacritty socket' complete -c alacritty -n "__fish_use_subcommand" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c alacritty -n "__fish_seen_subcommand_from msg; and not __fish_seen_subcommand_from create-window; and not __fish_seen_subcommand_from help" -s s -l socket -d 'IPC socket connection path override' -r -F -complete -c alacritty -n "__fish_seen_subcommand_from msg; and not __fish_seen_subcommand_from create-window; and not __fish_seen_subcommand_from help" -s h -l help -d 'Print help information' -complete -c alacritty -n "__fish_seen_subcommand_from msg; and not __fish_seen_subcommand_from create-window; and not __fish_seen_subcommand_from help" -f -a "create-window" -d 'Create a new window in the same Alacritty process' -complete -c alacritty -n "__fish_seen_subcommand_from msg; and not __fish_seen_subcommand_from create-window; and not __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c alacritty -n "__fish_seen_subcommand_from msg; and not __fish_seen_subcommand_from create-window; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from help" -s s -l socket -d 'IPC socket connection path override' -r -F +complete -c alacritty -n "__fish_seen_subcommand_from msg; and not __fish_seen_subcommand_from create-window; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from help" -s h -l help -d 'Print help information' +complete -c alacritty -n "__fish_seen_subcommand_from msg; and not __fish_seen_subcommand_from create-window; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from help" -f -a "create-window" -d 'Create a new window in the same Alacritty process' +complete -c alacritty -n "__fish_seen_subcommand_from msg; and not __fish_seen_subcommand_from create-window; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from help" -f -a "config" -d 'Update the Alacritty configuration' +complete -c alacritty -n "__fish_seen_subcommand_from msg; and not __fish_seen_subcommand_from create-window; and not __fish_seen_subcommand_from config; and not __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c alacritty -n "__fish_seen_subcommand_from msg; and __fish_seen_subcommand_from create-window" -l working-directory -d 'Start the shell in the specified working directory' -r -F complete -c alacritty -n "__fish_seen_subcommand_from msg; and __fish_seen_subcommand_from create-window" -s e -l command -d 'Command and args to execute (must be last argument)' -r complete -c alacritty -n "__fish_seen_subcommand_from msg; and __fish_seen_subcommand_from create-window" -s t -l title -d 'Defines the window title [default: Alacritty]' -r complete -c alacritty -n "__fish_seen_subcommand_from msg; and __fish_seen_subcommand_from create-window" -l class -d 'Defines window class/app_id on X11/Wayland [default: Alacritty]' -r complete -c alacritty -n "__fish_seen_subcommand_from msg; and __fish_seen_subcommand_from create-window" -l hold -d 'Remain open after child process exit' complete -c alacritty -n "__fish_seen_subcommand_from msg; and __fish_seen_subcommand_from create-window" -s h -l help -d 'Print help information' +complete -c alacritty -n "__fish_seen_subcommand_from msg; and __fish_seen_subcommand_from config" -s w -l window-id -d 'Window ID for the new config' -r +complete -c alacritty -n "__fish_seen_subcommand_from msg; and __fish_seen_subcommand_from config" -s r -l reset -d 'Clear all runtime configuration changes' +complete -c alacritty -n "__fish_seen_subcommand_from msg; and __fish_seen_subcommand_from config" -s h -l help -d 'Print help information'