diff options
author | Christian Duerr <contact@christianduerr.com> | 2022-08-31 22:48:38 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-09-01 01:48:38 +0300 |
commit | 4ddb608563d985060d69594d1004550a680ae3bd (patch) | |
tree | 0b02a330b3e59300cff80a147f3c1bdab7f9ea57 | |
parent | 18f9c2793924aec91c80a69ccb45f529adaffae5 (diff) |
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.
32 files changed, 630 insertions, 165 deletions
@@ -18,6 +18,7 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" name = "alacritty" version = "0.11.0-dev" dependencies = [ + "alacritty_config", "alacritty_config_derive", "alacritty_terminal", "bitflags", @@ -50,9 +51,19 @@ dependencies = [ ] [[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", @@ -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<Value, serde_yaml::Error> { 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<PathBuf>, /// 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<String>, + + /// 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<i128>, + + /// 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<KeyBinding>); 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<MouseBinding>); impl Default for MouseBindings { @@ -223,7 +223,7 @@ pub struct Delta<T: Default> { } /// 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<usize> { - 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<usize> { - 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<Event>, pub event_proxy: &'a EventLoopProxy<Event>, @@ -1162,6 +1167,8 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> { 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<EventQueue>, windows: HashMap<WindowId, WindowContext>, cli_options: CliOptions, - config: UiConfig, + config: Rc<UiConfig>, } 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<dyn Error>> { 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<Event>) - 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<UiConfig>, } impl WindowContext { /// Create a new terminal window context. pub fn new( - config: &UiConfig, + config: Rc<UiConfig>, options: &WindowOptions, window_event_loop: &EventLoopWindowTarget<Event>, proxy: EventLoopProxy<Event>, @@ -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<UiConfig>) { + 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<UiConfig>, 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) =& |