1
0
Fork 0
mirror of https://github.com/alacritty/alacritty.git synced 2024-11-25 14:05:41 -05:00

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.
This commit is contained in:
Christian Duerr 2022-08-31 22:48:38 +00:00 committed by GitHub
parent 18f9c27939
commit 4ddb608563
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 629 additions and 164 deletions

12
Cargo.lock generated
View file

@ -18,6 +18,7 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
name = "alacritty" name = "alacritty"
version = "0.11.0-dev" version = "0.11.0-dev"
dependencies = [ dependencies = [
"alacritty_config",
"alacritty_config_derive", "alacritty_config_derive",
"alacritty_terminal", "alacritty_terminal",
"bitflags", "bitflags",
@ -49,10 +50,20 @@ dependencies = [
"xdg", "xdg",
] ]
[[package]]
name = "alacritty_config"
version = "0.1.0"
dependencies = [
"log",
"serde",
"serde_yaml",
]
[[package]] [[package]]
name = "alacritty_config_derive" name = "alacritty_config_derive"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"alacritty_config",
"log", "log",
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -65,6 +76,7 @@ dependencies = [
name = "alacritty_terminal" name = "alacritty_terminal"
version = "0.17.0-dev" version = "0.17.0-dev"
dependencies = [ dependencies = [
"alacritty_config",
"alacritty_config_derive", "alacritty_config_derive",
"base64", "base64",
"bitflags", "bitflags",

View file

@ -2,6 +2,7 @@
members = [ members = [
"alacritty", "alacritty",
"alacritty_terminal", "alacritty_terminal",
"alacritty_config",
"alacritty_config_derive", "alacritty_config_derive",
] ]

View file

@ -18,8 +18,12 @@ default-features = false
path = "../alacritty_config_derive" path = "../alacritty_config_derive"
version = "0.1.0" version = "0.1.0"
[dependencies.alacritty_config]
path = "../alacritty_config"
version = "0.1.0"
[dependencies] [dependencies]
clap = { version = "3.0.0", features = ["derive"] } clap = { version = "3.0.0", features = ["derive", "env"] }
log = { version = "0.4", features = ["std", "serde"] } log = { version = "0.4", features = ["std", "serde"] }
fnv = "1" fnv = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }

View file

@ -81,14 +81,7 @@ impl Options {
let mut options = Self::parse(); let mut options = Self::parse();
// Convert `--option` flags into serde `Value`. // Convert `--option` flags into serde `Value`.
for option in &options.option { options.config_options = options_as_value(&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 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> { fn option_as_value(option: &str) -> Result<Value, serde_yaml::Error> {
let mut yaml_text = String::with_capacity(option.len()); let mut yaml_text = String::with_capacity(option.len());
let mut closing_brackets = String::new(); let mut closing_brackets = String::new();
@ -266,7 +270,7 @@ pub enum Subcommands {
#[derive(Args, Debug)] #[derive(Args, Debug)]
pub struct MessageOptions { pub struct MessageOptions {
/// IPC socket connection path override. /// IPC socket connection path override.
#[clap(long, short, value_hint = ValueHint::FilePath)] #[clap(short, long, value_hint = ValueHint::FilePath)]
pub socket: Option<PathBuf>, pub socket: Option<PathBuf>,
/// Message which should be sent. /// Message which should be sent.
@ -280,9 +284,12 @@ pub struct MessageOptions {
pub enum SocketMessage { pub enum SocketMessage {
/// Create a new window in the same Alacritty process. /// Create a new window in the same Alacritty process.
CreateWindow(WindowOptions), 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)] #[derive(Serialize, Deserialize, Args, Default, Clone, Debug, PartialEq, Eq)]
pub struct WindowOptions { pub struct WindowOptions {
/// Terminal options which can be passed via IPC. /// Terminal options which can be passed via IPC.
@ -294,6 +301,25 @@ pub struct WindowOptions {
pub window_identity: WindowIdentity, 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -9,7 +9,7 @@ use serde::de::{self, Error as SerdeError, MapAccess, Unexpected, Visitor};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use serde_yaml::Value as SerdeValue; 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::config::Program;
use alacritty_terminal::term::TermMode; 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 /// Our deserialize impl wouldn't be covered by a derive(Deserialize); see the
/// impl below. /// impl below.
#[derive(Debug, Copy, Clone, Hash, Default, Eq, PartialEq)] #[derive(SerdeReplace, Debug, Copy, Clone, Hash, Default, Eq, PartialEq)]
pub struct ModsWrapper(pub ModifiersState); pub struct ModsWrapper(pub ModifiersState);
impl ModsWrapper { impl ModsWrapper {

View file

@ -4,7 +4,7 @@ use crossfont::Size as FontSize;
use serde::de::{self, Visitor}; use serde::de::{self, Visitor};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use alacritty_config_derive::ConfigDeserialize; use alacritty_config_derive::{ConfigDeserialize, SerdeReplace};
use crate::config::ui_config::Delta; 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); struct Size(FontSize);
impl Default for Size { impl Default for Size {

View file

@ -9,7 +9,7 @@ use serde::de::{Error as SerdeError, MapAccess, Visitor};
use serde::{self, Deserialize, Deserializer}; use serde::{self, Deserialize, Deserializer};
use unicode_width::UnicodeWidthChar; use unicode_width::UnicodeWidthChar;
use alacritty_config_derive::ConfigDeserialize; use alacritty_config_derive::{ConfigDeserialize, SerdeReplace};
use alacritty_terminal::config::{ use alacritty_terminal::config::{
Config as TerminalConfig, Percentage, Program, LOG_TARGET_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:)\ 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{-}\\^⟨⟩`]+"; [^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+";
#[derive(ConfigDeserialize, Debug, PartialEq)] #[derive(ConfigDeserialize, Clone, Debug, PartialEq)]
pub struct UiConfig { pub struct UiConfig {
/// Font configuration. /// Font configuration.
pub font: Font, pub font: Font,
@ -145,7 +145,7 @@ impl UiConfig {
} }
} }
#[derive(Debug, PartialEq, Eq)] #[derive(SerdeReplace, Clone, Debug, PartialEq, Eq)]
struct KeyBindings(Vec<KeyBinding>); struct KeyBindings(Vec<KeyBinding>);
impl Default for KeyBindings { 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>); struct MouseBindings(Vec<MouseBinding>);
impl Default for MouseBindings { impl Default for MouseBindings {
@ -223,7 +223,7 @@ pub struct Delta<T: Default> {
} }
/// Regex terminal hints. /// Regex terminal hints.
#[derive(ConfigDeserialize, Debug, PartialEq, Eq)] #[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)]
pub struct Hints { pub struct Hints {
/// Characters for the hint labels. /// Characters for the hint labels.
alphabet: HintsAlphabet, alphabet: HintsAlphabet,
@ -273,7 +273,7 @@ impl Hints {
} }
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(SerdeReplace, Clone, Debug, PartialEq, Eq)]
struct HintsAlphabet(String); struct HintsAlphabet(String);
impl Default for HintsAlphabet { impl Default for HintsAlphabet {

View file

@ -6,7 +6,7 @@ use log::{error, warn};
use serde::de::{self, MapAccess, Visitor}; use serde::de::{self, MapAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize}; 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::config::{Percentage, LOG_TARGET_CONFIG};
use alacritty_terminal::index::Column; use alacritty_terminal::index::Column;
@ -201,7 +201,7 @@ pub struct Dimensions {
} }
/// Window class hint. /// Window class hint.
#[derive(Serialize, Debug, Clone, PartialEq, Eq)] #[derive(SerdeReplace, Serialize, Debug, Clone, PartialEq, Eq)]
pub struct Class { pub struct Class {
pub general: String, pub general: String,
pub instance: String, pub instance: String,

View file

@ -405,16 +405,6 @@ impl Window {
self.window().request_user_attention(attention); 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 { pub fn id(&self) -> WindowId {
self.window().id() self.window().id()
} }

View file

@ -9,6 +9,7 @@ use std::fmt::Debug;
#[cfg(not(windows))] #[cfg(not(windows))]
use std::os::unix::io::RawFd; use std::os::unix::io::RawFd;
use std::path::PathBuf; use std::path::PathBuf;
use std::rc::Rc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::{env, f32, mem}; 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::search::{Match, RegexSearch};
use alacritty_terminal::term::{self, ClipboardType, Term, TermMode}; use alacritty_terminal::term::{self, ClipboardType, Term, TermMode};
#[cfg(unix)]
use crate::cli::IpcConfig;
use crate::cli::{Options as CliOptions, WindowOptions}; use crate::cli::{Options as CliOptions, WindowOptions};
use crate::clipboard::Clipboard; use crate::clipboard::Clipboard;
use crate::config::ui_config::{HintAction, HintInternalAction}; use crate::config::ui_config::{HintAction, HintInternalAction};
@ -93,6 +96,8 @@ pub enum EventType {
Message(Message), Message(Message),
Scroll(Scroll), Scroll(Scroll),
CreateWindow(WindowOptions), CreateWindow(WindowOptions),
#[cfg(unix)]
IpcConfig(IpcConfig),
BlinkCursor, BlinkCursor,
BlinkCursorTimeout, BlinkCursorTimeout,
SearchNext, SearchNext,
@ -184,7 +189,7 @@ pub struct ActionContext<'a, N, T> {
pub modifiers: &'a mut ModifiersState, pub modifiers: &'a mut ModifiersState,
pub display: &'a mut Display, pub display: &'a mut Display,
pub message_buffer: &'a mut MessageBuffer, pub message_buffer: &'a mut MessageBuffer,
pub config: &'a mut UiConfig, pub config: &'a UiConfig,
pub cursor_blink_timed_out: &'a mut bool, pub cursor_blink_timed_out: &'a mut bool,
pub event_loop: &'a EventLoopWindowTarget<Event>, pub event_loop: &'a EventLoopWindowTarget<Event>,
pub event_proxy: &'a EventLoopProxy<Event>, pub event_proxy: &'a EventLoopProxy<Event>,
@ -1162,6 +1167,8 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> {
TerminalEvent::Exit => (), TerminalEvent::Exit => (),
TerminalEvent::CursorBlinkingChange => self.ctx.update_cursor_blinking(), TerminalEvent::CursorBlinkingChange => self.ctx.update_cursor_blinking(),
}, },
#[cfg(unix)]
EventType::IpcConfig(_) => (),
EventType::ConfigReload(_) | EventType::CreateWindow(_) => (), EventType::ConfigReload(_) | EventType::CreateWindow(_) => (),
}, },
GlutinEvent::RedrawRequested(_) => *self.ctx.dirty = true, GlutinEvent::RedrawRequested(_) => *self.ctx.dirty = true,
@ -1292,7 +1299,7 @@ pub struct Processor {
wayland_event_queue: Option<EventQueue>, wayland_event_queue: Option<EventQueue>,
windows: HashMap<WindowId, WindowContext>, windows: HashMap<WindowId, WindowContext>,
cli_options: CliOptions, cli_options: CliOptions,
config: UiConfig, config: Rc<UiConfig>,
} }
impl Processor { impl Processor {
@ -1313,8 +1320,8 @@ impl Processor {
Processor { Processor {
windows: HashMap::new(), windows: HashMap::new(),
config: Rc::new(config),
cli_options, cli_options,
config,
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
wayland_event_queue, wayland_event_queue,
} }
@ -1328,7 +1335,7 @@ impl Processor {
options: WindowOptions, options: WindowOptions,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
let window_context = WindowContext::new( let window_context = WindowContext::new(
&self.config, self.config.clone(),
&options, &options,
event_loop, event_loop,
proxy, proxy,
@ -1436,7 +1443,6 @@ impl Processor {
window_context.handle_event( window_context.handle_event(
event_loop, event_loop,
&proxy, &proxy,
&mut self.config,
&mut clipboard, &mut clipboard,
&mut scheduler, &mut scheduler,
GlutinEvent::RedrawEventsCleared, GlutinEvent::RedrawEventsCleared,
@ -1457,13 +1463,27 @@ impl Processor {
// Load config and update each terminal. // Load config and update each terminal.
if let Ok(config) = config::reload(&path, &self.cli_options) { 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() { 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. // Create a new terminal window.
GlutinEvent::UserEvent(Event { GlutinEvent::UserEvent(Event {
payload: EventType::CreateWindow(options), .. payload: EventType::CreateWindow(options), ..
@ -1485,7 +1505,6 @@ impl Processor {
window_context.handle_event( window_context.handle_event(
event_loop, event_loop,
&proxy, &proxy,
&mut self.config,
&mut clipboard, &mut clipboard,
&mut scheduler, &mut scheduler,
event.clone().into(), event.clone().into(),
@ -1500,7 +1519,6 @@ impl Processor {
window_context.handle_event( window_context.handle_event(
event_loop, event_loop,
&proxy, &proxy,
&mut self.config,
&mut clipboard, &mut clipboard,
&mut scheduler, &mut scheduler,
event, event,

View file

@ -7,6 +7,7 @@ use std::path::PathBuf;
use std::{env, fs, process}; use std::{env, fs, process};
use glutin::event_loop::EventLoopProxy; use glutin::event_loop::EventLoopProxy;
use glutin::window::WindowId;
use log::warn; use log::warn;
use alacritty_terminal::thread; 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 = Event::new(EventType::CreateWindow(options), None);
let _ = event_proxy.send_event(event); 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);
},
} }
} }
}); });

View file

@ -6,6 +6,7 @@ use std::io::Write;
use std::mem; use std::mem;
#[cfg(not(windows))] #[cfg(not(windows))]
use std::os::unix::io::{AsRawFd, RawFd}; use std::os::unix::io::{AsRawFd, RawFd};
use std::rc::Rc;
#[cfg(not(any(target_os = "macos", windows)))] #[cfg(not(any(target_os = "macos", windows)))]
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::Arc; use std::sync::Arc;
@ -14,11 +15,12 @@ use crossfont::Size;
use glutin::event::{Event as GlutinEvent, ModifiersState, WindowEvent}; use glutin::event::{Event as GlutinEvent, ModifiersState, WindowEvent};
use glutin::event_loop::{EventLoopProxy, EventLoopWindowTarget}; use glutin::event_loop::{EventLoopProxy, EventLoopWindowTarget};
use glutin::window::WindowId; use glutin::window::WindowId;
use log::info; use log::{error, info};
use serde_json as json; use serde_json as json;
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
use wayland_client::EventQueue; use wayland_client::EventQueue;
use alacritty_config::SerdeReplace;
use alacritty_terminal::event::Event as TerminalEvent; use alacritty_terminal::event::Event as TerminalEvent;
use alacritty_terminal::event_loop::{EventLoop as PtyEventLoop, Msg, Notifier}; use alacritty_terminal::event_loop::{EventLoop as PtyEventLoop, Msg, Notifier};
use alacritty_terminal::grid::{Dimensions, Scroll}; 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::term::{Term, TermMode};
use alacritty_terminal::tty; use alacritty_terminal::tty;
#[cfg(unix)]
use crate::cli::IpcConfig;
use crate::cli::WindowOptions; use crate::cli::WindowOptions;
use crate::clipboard::Clipboard; use crate::clipboard::Clipboard;
use crate::config::UiConfig; use crate::config::UiConfig;
@ -58,12 +62,14 @@ pub struct WindowContext {
master_fd: RawFd, master_fd: RawFd,
#[cfg(not(windows))] #[cfg(not(windows))]
shell_pid: u32, shell_pid: u32,
ipc_config: Vec<(String, serde_yaml::Value)>,
config: Rc<UiConfig>,
} }
impl WindowContext { impl WindowContext {
/// Create a new terminal window context. /// Create a new terminal window context.
pub fn new( pub fn new(
config: &UiConfig, config: Rc<UiConfig>,
options: &WindowOptions, options: &WindowOptions,
window_event_loop: &EventLoopWindowTarget<Event>, window_event_loop: &EventLoopWindowTarget<Event>,
proxy: EventLoopProxy<Event>, proxy: EventLoopProxy<Event>,
@ -81,7 +87,7 @@ impl WindowContext {
// //
// The display manages a window and can draw the terminal. // The display manages a window and can draw the terminal.
let display = Display::new( let display = Display::new(
config, &config,
window_event_loop, window_event_loop,
&identity, &identity,
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] #[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 // 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 // pseudoterminal. A file descriptor for the master side is retained for
// reading/writing to the shell. // 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))] #[cfg(not(windows))]
let master_fd = pty.file().as_raw_fd(); let master_fd = pty.file().as_raw_fd();
@ -142,23 +148,27 @@ impl WindowContext {
event_proxy.send_event(TerminalEvent::CursorBlinkingChange.into()); event_proxy.send_event(TerminalEvent::CursorBlinkingChange.into());
} }
let font_size = config.font.size();
// Create context for the Alacritty window. // Create context for the Alacritty window.
Ok(WindowContext { Ok(WindowContext {
font_size: config.font.size(), preserve_title,
notifier: Notifier(loop_tx), font_size,
terminal, terminal,
display, display,
preserve_title,
#[cfg(not(windows))] #[cfg(not(windows))]
master_fd, master_fd,
#[cfg(not(windows))] #[cfg(not(windows))]
shell_pid, shell_pid,
config,
notifier: Notifier(loop_tx),
cursor_blink_timed_out: Default::default(), cursor_blink_timed_out: Default::default(),
suppress_chars: Default::default(), suppress_chars: Default::default(),
message_buffer: Default::default(), message_buffer: Default::default(),
received_count: Default::default(), received_count: Default::default(),
search_state: Default::default(), search_state: Default::default(),
event_queue: Default::default(), event_queue: Default::default(),
ipc_config: Default::default(),
modifiers: Default::default(), modifiers: Default::default(),
mouse: Default::default(), mouse: Default::default(),
dirty: Default::default(), dirty: Default::default(),
@ -167,33 +177,49 @@ impl WindowContext {
} }
/// Update the terminal window to the latest config. /// Update the terminal window to the latest config.
pub fn update_config(&mut self, old_config: &UiConfig, config: &UiConfig) { pub fn update_config(&mut self, new_config: Rc<UiConfig>) {
self.display.update_config(config); let old_config = mem::replace(&mut self.config, new_config);
self.terminal.lock().update_config(&config.terminal_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. // Reload cursor if its thickness has changed.
if (old_config.terminal_config.cursor.thickness() if (old_config.terminal_config.cursor.thickness()
- config.terminal_config.cursor.thickness()) - self.config.terminal_config.cursor.thickness())
.abs() .abs()
> f32::EPSILON > f32::EPSILON
{ {
self.display.pending_update.set_cursor_dirty(); 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. // Do not update font size if it has been changed at runtime.
if self.font_size == old_config.font.size() { 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); self.display.pending_update.set_font(font);
} }
// Update display if padding options were changed. // Update display if padding options were changed.
let window_config = &old_config.window; let window_config = &old_config.window;
if window_config.padding(1.) != config.window.padding(1.) if window_config.padding(1.) != self.config.window.padding(1.)
|| window_config.dynamic_padding != config.window.dynamic_padding || window_config.dynamic_padding != self.config.window.dynamic_padding
{ {
self.display.pending_update.dirty = true; self.display.pending_update.dirty = true;
} }
@ -206,18 +232,18 @@ impl WindowContext {
// │ N │ Y │ N ││ N │ // │ N │ Y │ N ││ N │
// │ N │ N │ _ ││ Y │ // │ N │ N │ _ ││ Y │
if !self.preserve_title 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.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. // Disable shadows for transparent windows on macOS.
#[cfg(target_os = "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. // 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. // Update cursor blinking.
let event = Event::new(TerminalEvent::CursorBlinkingChange.into(), None); let event = Event::new(TerminalEvent::CursorBlinkingChange.into(), None);
@ -226,12 +252,39 @@ impl WindowContext {
self.dirty = true; 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) => 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. /// Process events for this terminal window.
pub fn handle_event( pub fn handle_event(
&mut self, &mut self,
event_loop: &EventLoopWindowTarget<Event>, event_loop: &EventLoopWindowTarget<Event>,
event_proxy: &EventLoopProxy<Event>, event_proxy: &EventLoopProxy<Event>,
config: &mut UiConfig,
clipboard: &mut Clipboard, clipboard: &mut Clipboard,
scheduler: &mut Scheduler, scheduler: &mut Scheduler,
event: GlutinEvent<'_, Event>, event: GlutinEvent<'_, Event>,
@ -285,11 +338,11 @@ impl WindowContext {
#[cfg(not(windows))] #[cfg(not(windows))]
shell_pid: self.shell_pid, shell_pid: self.shell_pid,
preserve_title: self.preserve_title, preserve_title: self.preserve_title,
config: &self.config,
event_proxy, event_proxy,
event_loop, event_loop,
clipboard, clipboard,
scheduler, scheduler,
config,
}; };
let mut processor = input::Processor::new(context); let mut processor = input::Processor::new(context);
@ -306,7 +359,7 @@ impl WindowContext {
&self.message_buffer, &self.message_buffer,
&self.search_state, &self.search_state,
old_is_searching, old_is_searching,
config, &self.config,
); );
self.dirty = true; self.dirty = true;
} }
@ -314,7 +367,7 @@ impl WindowContext {
if self.dirty || self.mouse.hint_highlight_dirty { if self.dirty || self.mouse.hint_highlight_dirty {
self.dirty |= self.display.update_highlighted_hints( self.dirty |= self.display.update_highlighted_hints(
&terminal, &terminal,
config, &self.config,
&self.mouse, &self.mouse,
self.modifiers, self.modifiers,
); );
@ -339,7 +392,7 @@ impl WindowContext {
} }
// Redraw screen. // 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);
} }
} }

View file

@ -0,0 +1,14 @@
[package]
name = "alacritty_config"
version = "0.1.0"
authors = ["Christian Duerr <contact@christianduerr.com>"]
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"

View file

@ -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<dyn Error>>;
}
macro_rules! impl_replace {
($($ty:ty),*$(,)*) => {
$(
impl SerdeReplace for $ty {
fn replace(&mut self, key: &str, value: Value) -> Result<(), Box<dyn Error>> {
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<dyn Error>>
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<T> {
fn replace(&mut self, key: &str, value: Value) -> Result<(), Box<dyn Error>> {
replace_simple(self, key, value)
}
}
impl<'de, T: Deserialize<'de>> SerdeReplace for Option<T> {
fn replace(&mut self, key: &str, value: Value) -> Result<(), Box<dyn Error>> {
replace_simple(self, key, value)
}
}
impl<'de, T: Deserialize<'de>> SerdeReplace for HashMap<String, T> {
fn replace(&mut self, key: &str, value: Value) -> Result<(), Box<dyn Error>> {
replace_simple(self, key, value)
}
}

View file

@ -16,7 +16,11 @@ syn = { version = "1.0.53", features = ["derive", "parsing", "proc-macro", "prin
proc-macro2 = "1.0.24" proc-macro2 = "1.0.24"
quote = "1.0.7" quote = "1.0.7"
[dev-dependencies.alacritty_config]
path = "../alacritty_config"
version = "0.1.0"
[dev-dependencies] [dev-dependencies]
serde = { version = "1.0.117", features = ["derive"] }
serde_yaml = "0.8.14" serde_yaml = "0.8.14"
serde = "1.0.117"
log = "0.4.11" log = "0.4.11"

View file

@ -1,9 +1,11 @@
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote}; 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); let visitor = format_ident!("{}Visitor", ident);
// Create match arm streams and get a list with all available values. // 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)); available_values.truncate(available_values.len().saturating_sub(2));
// Generate deserialization impl. // Generate deserialization impl.
let tokens = quote! { let mut tokens = quote! {
struct #visitor; struct #visitor;
impl<'de> serde::de::Visitor<'de> for #visitor { impl<'de> serde::de::Visitor<'de> for #visitor {
type Value = #ident; 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() tokens.into()
} }

View file

@ -1,13 +1,12 @@
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote}; use quote::{format_ident, quote};
use syn::parse::{self, Parse, ParseStream};
use syn::punctuated::Punctuated; use syn::punctuated::Punctuated;
use syn::spanned::Spanned; 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. /// Use this crate's name as log target.
const LOG_TARGET: &str = env!("CARGO_PKG_NAME"); const LOG_TARGET: &str = env!("CARGO_PKG_NAME");
@ -18,20 +17,20 @@ pub fn derive_deserialize<T>(
) -> TokenStream { ) -> TokenStream {
// Create all necessary tokens for the implementation. // Create all necessary tokens for the implementation.
let GenericsStreams { unconstrained, constrained, phantoms } = let GenericsStreams { unconstrained, constrained, phantoms } =
generics_streams(generics.params); crate::generics_streams(&generics.params);
let FieldStreams { flatten, match_assignments } = fields_deserializer(&fields); let FieldStreams { flatten, match_assignments } = fields_deserializer(&fields);
let visitor = format_ident!("{}Visitor", ident); let visitor = format_ident!("{}Visitor", ident);
// Generate deserialization impl. // Generate deserialization impl.
let tokens = quote! { let mut tokens = quote! {
#[derive(Default)] #[derive(Default)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
struct #visitor < #unconstrained > { struct #visitor <#unconstrained> {
#phantoms #phantoms
} }
impl<'de, #constrained> serde::de::Visitor<'de> for #visitor < #unconstrained > { impl <'de, #constrained> serde::de::Visitor<'de> for #visitor <#unconstrained> {
type Value = #ident < #unconstrained >; type Value = #ident <#unconstrained>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a mapping") formatter.write_str("a mapping")
@ -61,7 +60,7 @@ pub fn derive_deserialize<T>(
} }
} }
impl<'de, #constrained> serde::Deserialize<'de> for #ident < #unconstrained > { impl <'de, #constrained> serde::Deserialize<'de> for #ident <#unconstrained> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
@ -71,6 +70,9 @@ pub fn derive_deserialize<T>(
} }
}; };
// Automatically implement [`alacritty_config::SerdeReplace`].
tokens.extend(serde_replace::derive_recursive(ident, generics, fields));
tokens.into() tokens.into()
} }
@ -177,50 +179,3 @@ fn field_deserializer(field_streams: &mut FieldStreams, field: &Field) -> Result
Ok(()) Ok(())
} }
/// Field attribute.
struct Attr {
ident: String,
param: Option<LitStr>,
}
impl Parse for Attr {
fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
let ident = input.parse::<Ident>()?.to_string();
let param = input.parse::<Token![=]>().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<T>,`
fn generics_streams<T>(params: Punctuated<GenericParam, T>) -> 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
}

View file

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

View file

@ -2,25 +2,27 @@
#![cfg_attr(feature = "cargo-clippy", deny(warnings))] #![cfg_attr(feature = "cargo-clippy", deny(warnings))]
use proc_macro::TokenStream; 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 config_deserialize;
mod de_struct; mod serde_replace;
/// Error if the derive was used on an unsupported type. /// Error message when attempting to flatten multiple fields.
const UNSUPPORTED_ERROR: &str = "ConfigDeserialize must be used on a struct with fields"; pub(crate) const MULTIPLE_FLATTEN_ERROR: &str =
"At most one instance of #[config(flatten)] is supported";
#[proc_macro_derive(ConfigDeserialize, attributes(config))] #[proc_macro_derive(ConfigDeserialize, attributes(config))]
pub fn derive_config_deserialize(input: TokenStream) -> TokenStream { pub fn derive_config_deserialize(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput); config_deserialize::derive(input)
}
match input.data { #[proc_macro_derive(SerdeReplace)]
Data::Struct(DataStruct { fields: Fields::Named(fields), .. }) => { pub fn derive_serde_replace(input: TokenStream) -> TokenStream {
de_struct::derive_deserialize(input.ident, input.generics, fields.named) serde_replace::derive(input)
},
Data::Enum(data_enum) => de_enum::derive_deserialize(input.ident, data_enum),
_ => Error::new(input.ident.span(), UNSUPPORTED_ERROR).to_compile_error().into(),
}
} }
/// Verify that a token path ends with a specific segment. /// 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(); let segments = path.segments.iter();
segments.last().map_or(false, |s| s.ident == segment) 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<T>,`
pub(crate) fn generics_streams<T>(params: &Punctuated<GenericParam, T>) -> 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<LitStr>,
}
impl Parse for Attr {
fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
let ident = input.parse::<Ident>()?.to_string();
let param = input.parse::<Token![=]>().and_then(|_| input.parse()).ok();
Ok(Self { ident, param })
}
}

View file

@ -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<dyn std::error::Error>> {
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<T>(
ident: Ident,
generics: Generics,
fields: Punctuated<Field, T>,
) -> 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<dyn std::error::Error>> {
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<T>(fields: &Punctuated<Field, T>) -> 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::<Attr>().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
}

View file

@ -1,8 +1,10 @@
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use log::{Level, Log, Metadata, Record}; 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)] #[derive(ConfigDeserialize, Debug, PartialEq, Eq)]
enum TestEnum { enum TestEnum {
@ -63,6 +65,7 @@ struct Test2<T: Default> {
field3: usize, field3: usize,
#[config(alias = "aliased")] #[config(alias = "aliased")]
field4: u8, field4: u8,
newtype: NewType,
} }
#[derive(ConfigDeserialize, Default)] #[derive(ConfigDeserialize, Default)]
@ -70,6 +73,9 @@ struct Test3 {
flatty: usize, flatty: usize,
} }
#[derive(SerdeReplace, Deserialize, Default, PartialEq, Eq, Debug)]
struct NewType(usize);
#[test] #[test]
fn config_deserialize() { fn config_deserialize() {
let logger = unsafe { let logger = unsafe {
@ -159,3 +165,33 @@ impl Log for Logger {
fn flush(&self) {} 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);
}

View file

@ -13,6 +13,10 @@ rust-version = "1.57.0"
path = "../alacritty_config_derive" path = "../alacritty_config_derive"
version = "0.1.0" version = "0.1.0"
[dependencies.alacritty_config]
path = "../alacritty_config"
version = "0.1.0"
[dependencies] [dependencies]
libc = "0.2" libc = "0.2"
bitflags = "1" bitflags = "1"

View file

@ -4,7 +4,7 @@ use std::path::PathBuf;
use serde::Deserialize; use serde::Deserialize;
use alacritty_config_derive::ConfigDeserialize; use alacritty_config_derive::{ConfigDeserialize, SerdeReplace};
mod scrolling; mod scrolling;
@ -17,7 +17,7 @@ pub const LOG_TARGET_CONFIG: &str = "alacritty_config_derive";
const MIN_BLINK_INTERVAL: u64 = 10; const MIN_BLINK_INTERVAL: u64 = 10;
/// Top-level config type. /// Top-level config type.
#[derive(ConfigDeserialize, Debug, PartialEq, Default)] #[derive(ConfigDeserialize, Clone, Debug, PartialEq, Default)]
pub struct Config { pub struct Config {
/// TERM env variable. /// TERM env variable.
pub env: HashMap<String, String>, pub env: HashMap<String, String>,
@ -125,7 +125,7 @@ impl Cursor {
} }
} }
#[derive(Deserialize, Debug, Copy, Clone, PartialEq, Eq)] #[derive(SerdeReplace, Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
#[serde(untagged)] #[serde(untagged)]
pub enum ConfigCursorStyle { pub enum ConfigCursorStyle {
Shape(CursorShape), Shape(CursorShape),
@ -222,7 +222,7 @@ impl Program {
} }
/// Wrapper around f32 that represents a percentage value between 0.0 and 1.0. /// 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); pub struct Percentage(f32);
impl Default for Percentage { impl Default for Percentage {

View file

@ -1,7 +1,7 @@
use serde::de::Error as SerdeError; use serde::de::Error as SerdeError;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use alacritty_config_derive::ConfigDeserialize; use alacritty_config_derive::{ConfigDeserialize, SerdeReplace};
/// Maximum scrollback amount configurable. /// Maximum scrollback amount configurable.
pub const MAX_SCROLLBACK_LINES: u32 = 100_000; 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); struct ScrollingHistory(u32);
impl Default for ScrollingHistory { impl Default for ScrollingHistory {

View file

@ -7,6 +7,8 @@ use std::ops::{Add, AddAssign, Deref, Sub, SubAssign};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use alacritty_config_derive::SerdeReplace;
use crate::grid::Dimensions; use crate::grid::Dimensions;
/// The side of a cell. /// The side of a cell.
@ -222,7 +224,19 @@ impl PartialEq<usize> for Line {
/// A column. /// A column.
/// ///
/// Newtype to avoid passing values incorrectly. /// 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); pub struct Column(pub usize);
impl fmt::Display for Column { impl fmt::Display for Column {

View file

@ -7,12 +7,14 @@ use serde::de::{Error as _, Visitor};
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use serde_yaml::Value; use serde_yaml::Value;
use alacritty_config_derive::SerdeReplace;
use crate::ansi::NamedColor; use crate::ansi::NamedColor;
/// Number of terminal colors. /// Number of terminal colors.
pub const COUNT: usize = 269; 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 struct Rgb {
pub r: u8, pub r: u8,
pub g: u8, pub g: u8,
@ -170,7 +172,7 @@ impl FromStr for Rgb {
} }
/// RGB color optionally referencing the cell's foreground or background. /// 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 { pub enum CellRgb {
CellForeground, CellForeground,
CellBackground, CellBackground,

View file

@ -148,7 +148,7 @@ fn default_shell_command(pw: &Passwd<'_>) -> Command {
} }
/// Create a new TTY and return a handle to interact with it. /// Create a new TTY and return a handle to interact with it.
pub fn new(config: &PtyConfig, window_size: WindowSize, window_id: Option<usize>) -> Result<Pty> { pub fn new(config: &PtyConfig, window_size: WindowSize, window_id: u64) -> Result<Pty> {
let (master, slave) = make_pty(window_size.to_winsize())?; let (master, slave) = make_pty(window_size.to_winsize())?;
#[cfg(any(target_os = "linux", target_os = "macos"))] #[cfg(any(target_os = "linux", target_os = "macos"))]
@ -178,13 +178,14 @@ pub fn new(config: &PtyConfig, window_size: WindowSize, window_id: Option<usize>
builder.stdout(unsafe { Stdio::from_raw_fd(slave) }); builder.stdout(unsafe { Stdio::from_raw_fd(slave) });
// Setup shell environment. // Setup shell environment.
let window_id = window_id.to_string();
builder.env("ALACRITTY_WINDOW_ID", &window_id);
builder.env("LOGNAME", pw.name); builder.env("LOGNAME", pw.name);
builder.env("USER", pw.name); builder.env("USER", pw.name);
builder.env("HOME", pw.dir); builder.env("HOME", pw.dir);
if let Some(window_id) = window_id { // Set Window ID for clients relying on X11 hacks.
builder.env("WINDOWID", format!("{}", window_id)); builder.env("WINDOWID", window_id);
}
unsafe { unsafe {
builder.pre_exec(move || { builder.pre_exec(move || {

View file

@ -27,7 +27,7 @@ pub struct Pty {
child_watcher: ChildExitWatcher, child_watcher: ChildExitWatcher,
} }
pub fn new(config: &PtyConfig, window_size: WindowSize, _window_id: Option<usize>) -> Result<Pty> { pub fn new(config: &PtyConfig, window_size: WindowSize, _window_id: u64) -> Result<Pty> {
conpty::new(config, window_size) conpty::new(config, window_size)
.ok_or_else(|| Error::new(ErrorKind::Other, "failed to spawn conpty")) .ok_or_else(|| Error::new(ErrorKind::Other, "failed to spawn conpty"))
} }

View file

@ -10,20 +10,43 @@ making it possible to control Alacritty without directly accessing it.
\fB\-s\fR, \fB\-\-socket\fR <socket> \fB\-s\fR, \fB\-\-socket\fR <socket>
Path for IPC socket creation Path for IPC socket creation
.SH "MESSAGES" .SH "MESSAGES"
.TP
\fBcreate-window\fR \fBcreate-window\fR
Create a new window in the same Alacritty process Create a new window in the same Alacritty process
.TP .TP
.SH "\tOPTIONS" .SH "\tOPTIONS"
.RS 12 .RS 12
.TP
\fB\-\-hold\fR \fB\-\-hold\fR
Remain open after child process exits Remain open after child process exits
.TP
\fB\-\-working\-directory\fR <working\-directory> \fB\-\-working\-directory\fR <working\-directory>
Start the shell in the specified working directory Start the shell in the specified working directory
.TP
\fB\-e\fR, \fB\-\-command\fR <command>... \fB\-e\fR, \fB\-\-command\fR <command>...
Command and args to execute (must be last argument) Command and args to execute (must be last argument)
.RE .RE
.TP
\fBconfig\fR
Update the Alacritty configuration
.TP
.SH "\tARGS"
.RS 12
.TP
\fB<CONFIG_OPTIONS>...\fR
Configuration file options [example: cursor.style=Beam]
.RE
.TP
.SH "\tOPTIONS"
.RS 12
.TP
\fB\-w\fR, \fB\-\-window\-id\fR <WINDOW_ID>
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" .SH "SEE ALSO"
See the alacritty github repository at https://github.com/alacritty/alacritty for the full documentation. See the alacritty github repository at https://github.com/alacritty/alacritty for the full documentation.
.SH "BUGS" .SH "BUGS"

View file

@ -73,6 +73,17 @@ _arguments "${_arguments_options[@]}" \
'--help[Print help information]' \ '--help[Print help information]' \
&& ret=0 && 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) (help)
_arguments "${_arguments_options[@]}" \ _arguments "${_arguments_options[@]}" \
'*::subcommand -- The subcommand whose help message to display:' \ '*::subcommand -- The subcommand whose help message to display:' \
@ -100,6 +111,11 @@ _alacritty_commands() {
) )
_describe -t commands 'alacritty commands' 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] )) || (( $+functions[_alacritty__msg__create-window_commands] )) ||
_alacritty__msg__create-window_commands() { _alacritty__msg__create-window_commands() {
local commands; commands=() local commands; commands=()
@ -119,6 +135,7 @@ _alacritty__msg__help_commands() {
_alacritty__msg_commands() { _alacritty__msg_commands() {
local commands; commands=( local commands; commands=(
'create-window:Create a new window in the same Alacritty process' \ '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)' \ 'help:Print this message or the help of the given subcommand(s)' \
) )
_describe -t commands 'alacritty msg commands' commands "$@" _describe -t commands 'alacritty msg commands' commands "$@"

View file

@ -12,6 +12,9 @@ _alacritty() {
"$1") "$1")
cmd="alacritty" cmd="alacritty"
;; ;;
config)
cmd+="__config"
;;
create-window) create-window)
cmd+="__create__window" cmd+="__create__window"
;; ;;
@ -100,7 +103,7 @@ _alacritty() {
return 0 return 0
;; ;;
alacritty__msg) 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 if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -121,6 +124,28 @@ _alacritty() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
;; ;;
alacritty__msg__config)
opts="-w -r -h --window-id --reset --help <CONFIG_OPTIONS>..."
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) alacritty__msg__create__window)
opts="-e -t -h --working-directory --hold --command --title --class --help" opts="-e -t -h --working-directory --hold --command --title --class --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then

View file

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