1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-26 06:42:12 +03:00
wezterm/src/config.rs
Wez Furlong 4ad7ff3083 use openssl directly for the client when that feature is enabled
This removes some redundancy and overhead around setting up the
connection (the native_tls crate doesn't provide PEM functions,
despite every deployment I've ever seen only ever using PEM certs),
but more importantly, gives the control needed to make hostname
verification work in a PKI setup with unusual CN values.
2019-06-20 17:27:40 -07:00

794 lines
24 KiB
Rust

//! Configuration for the gui portion of the terminal
use crate::font::FontSystemSelection;
use crate::frontend::guicommon::host::KeyAssignment;
use crate::frontend::FrontEndSelection;
use crate::get_shell;
use failure::{bail, err_msg, format_err, Error, Fallible};
use lazy_static::lazy_static;
use portable_pty::{CommandBuilder, PtySystemSelection};
use serde::{Deserialize, Deserializer};
use serde_derive::*;
use std;
use std::collections::HashMap;
use std::convert::TryInto;
use std::ffi::OsStr;
use std::fs;
use std::io::prelude::*;
use std::path::PathBuf;
use term;
use term::color::RgbColor;
use termwiz::hyperlink;
use termwiz::input::{KeyCode, Modifiers};
use toml;
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
/// The font size, measured in points
#[serde(default = "default_font_size")]
pub font_size: f64,
/// The DPI to assume
#[serde(default = "default_dpi")]
pub dpi: f64,
/// The baseline font to use
#[serde(default)]
pub font: TextStyle,
/// An optional set of style rules to select the font based
/// on the cell attributes
#[serde(default)]
pub font_rules: Vec<StyleRule>,
/// The color palette
pub colors: Option<Palette>,
/// How many lines of scrollback you want to retain
pub scrollback_lines: Option<usize>,
/// If no `prog` is specified on the command line, use this
/// instead of running the user's shell.
/// For example, to have `wezterm` always run `top` by default,
/// you'd use this:
///
/// ```
/// default_prog = ["top"]
/// ```
///
/// `default_prog` is implemented as an array where the 0th element
/// is the command to run and the rest of the elements are passed
/// as the positional arguments to that command.
pub default_prog: Option<Vec<String>>,
#[serde(default = "default_hyperlink_rules")]
pub hyperlink_rules: Vec<hyperlink::Rule>,
/// What to set the TERM variable to
#[serde(default = "default_term")]
pub term: String,
#[serde(default)]
pub font_system: FontSystemSelection,
#[serde(default)]
pub front_end: FrontEndSelection,
#[serde(default)]
pub pty: PtySystemSelection,
/// When using the MuxServer, this specifies the path to the unix
/// domain socket to use to communicate with the mux server.
pub mux_server_unix_domain_socket_path: Option<String>,
/// When using the MuxServer with the NetListener, specifies
/// the address and port combination on which it should listen
pub mux_server_bind_address: Option<String>,
/// When using the MuxServer with the NetListener, specifies
/// the path to an x509 PEM encoded private key file
pub mux_server_pem_private_key: Option<PathBuf>,
/// When using the MuxServer with the NetListener, specifies
/// the path to an x509 PEM encoded certificate file
pub mux_server_pem_cert: Option<PathBuf>,
/// When using the MuxServer with the NetListener, specifies
/// the path to an x509 PEM encoded CA chain file
pub mux_server_pem_ca: Option<PathBuf>,
/// When using the mux client domain, identifies the host:port
/// pair of the remote server.
pub mux_server_remote_address: Option<String>,
/// When using the mux client domain:
/// the path to an x509 PEM encoded private key file
pub mux_client_pem_private_key: Option<PathBuf>,
/// When using the mux client domain:
/// the path to an x509 PEM encoded certificate file
pub mux_client_pem_cert: Option<PathBuf>,
/// When using the mux client domain:
/// the path to an x509 PEM encoded CA chain file
pub mux_client_pem_ca: Option<PathBuf>,
pub mux_pem_root_certs: Option<Vec<PathBuf>>,
/// When using the mux client domain, explicitly control whether
/// the client checks that the certificate presented by the
/// server matches the hostname portion of mux_server_remote_address.
/// The default is true.
/// This option is made available for troubleshooting purposes and
/// should not be used outside of a controlled environment as it
/// weakens the security of the TLS channel.
pub mux_client_accept_invalid_hostnames: Option<bool>,
/// When connecting to a mux server, the hostname string that we
/// expect to match against the common name field in the certificate
/// presented by the server. This defaults to the hostname portion
/// of the `mux_server_bind_address` configuration and you should
/// not normally need to override this value.
pub mux_client_expected_cn: Option<String>,
#[serde(default)]
pub keys: Vec<Key>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Key {
#[serde(deserialize_with = "de_keycode")]
pub key: KeyCode,
#[serde(deserialize_with = "de_modifiers")]
pub mods: Modifiers,
pub action: KeyAction,
pub arg: Option<String>,
}
impl std::convert::TryInto<KeyAssignment> for &Key {
type Error = Error;
fn try_into(self) -> Result<KeyAssignment, Error> {
Ok(match self.action {
KeyAction::SpawnTab => KeyAssignment::SpawnTab,
KeyAction::SpawnTabInCurrentTabDomain => KeyAssignment::SpawnTabInCurrentTabDomain,
KeyAction::SpawnWindow => KeyAssignment::SpawnWindow,
KeyAction::ToggleFullScreen => KeyAssignment::ToggleFullScreen,
KeyAction::Copy => KeyAssignment::Copy,
KeyAction::Paste => KeyAssignment::Paste,
KeyAction::Hide => KeyAssignment::Hide,
KeyAction::Show => KeyAssignment::Show,
KeyAction::IncreaseFontSize => KeyAssignment::IncreaseFontSize,
KeyAction::DecreaseFontSize => KeyAssignment::DecreaseFontSize,
KeyAction::ResetFontSize => KeyAssignment::ResetFontSize,
KeyAction::Nop => KeyAssignment::Nop,
KeyAction::CloseCurrentTab => KeyAssignment::CloseCurrentTab,
KeyAction::ActivateTab => KeyAssignment::ActivateTab(
self.arg
.as_ref()
.ok_or_else(|| format_err!("missing arg for {:?}", self))?
.parse()?,
),
KeyAction::ActivateTabRelative => KeyAssignment::ActivateTabRelative(
self.arg
.as_ref()
.ok_or_else(|| format_err!("missing arg for {:?}", self))?
.parse()?,
),
KeyAction::SendString => KeyAssignment::SendString(
self.arg
.as_ref()
.ok_or_else(|| format_err!("missing arg for {:?}", self))?
.to_owned(),
),
})
}
}
#[derive(Debug, Deserialize, Clone)]
pub enum KeyAction {
SpawnTab,
SpawnTabInCurrentTabDomain,
SpawnWindow,
ToggleFullScreen,
Copy,
Paste,
ActivateTabRelative,
IncreaseFontSize,
DecreaseFontSize,
ResetFontSize,
ActivateTab,
SendString,
Nop,
Hide,
Show,
CloseCurrentTab,
}
fn de_keycode<'de, D>(deserializer: D) -> Result<KeyCode, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
macro_rules! m {
($($val:ident),* $(,)?) => {
$(
if s == stringify!($val) {
return Ok(KeyCode::$val);
}
)*
}
}
m!(
Hyper,
Super,
Meta,
Cancel,
Backspace,
Tab,
Clear,
Enter,
Shift,
Escape,
LeftShift,
RightShift,
Control,
LeftControl,
RightControl,
Alt,
LeftAlt,
RightAlt,
Menu,
LeftMenu,
RightMenu,
Pause,
CapsLock,
PageUp,
PageDown,
End,
Home,
LeftArrow,
RightArrow,
UpArrow,
DownArrow,
Select,
Print,
Execute,
PrintScreen,
Insert,
Delete,
Help,
LeftWindows,
RightWindows,
Applications,
Sleep,
Numpad0,
Numpad1,
Numpad2,
Numpad3,
Numpad4,
Numpad5,
Numpad6,
Numpad7,
Numpad8,
Numpad9,
Multiply,
Add,
Separator,
Subtract,
Decimal,
Divide,
NumLock,
ScrollLock,
BrowserBack,
BrowserForward,
BrowserRefresh,
BrowserStop,
BrowserSearch,
BrowserFavorites,
BrowserHome,
VolumeMute,
VolumeDown,
VolumeUp,
MediaNextTrack,
MediaPrevTrack,
MediaStop,
MediaPlayPause,
ApplicationLeftArrow,
ApplicationRightArrow,
ApplicationUpArrow,
ApplicationDownArrow,
);
if s.len() > 1 && s.starts_with('F') {
let num: u8 = s[1..].parse().map_err(|_| {
serde::de::Error::custom(format!(
"expected F<NUMBER> function key string, got: {}",
s
))
})?;
return Ok(KeyCode::Function(num));
}
let chars: Vec<char> = s.chars().collect();
if chars.len() == 1 {
Ok(KeyCode::Char(chars[0]))
} else {
Err(serde::de::Error::custom(format!(
"invalid KeyCode string {}",
s
)))
}
}
fn de_modifiers<'de, D>(deserializer: D) -> Result<Modifiers, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let mut mods = Modifiers::NONE;
for ele in s.split('|') {
if ele == "SHIFT" {
mods |= Modifiers::SHIFT;
} else if ele == "ALT" || ele == "OPT" || ele == "META" {
mods |= Modifiers::ALT;
} else if ele == "CTRL" {
mods |= Modifiers::CTRL;
} else if ele == "SUPER" || ele == "CMD" || ele == "WIN" {
mods |= Modifiers::SUPER;
} else {
return Err(serde::de::Error::custom(format!(
"invalid modifier name {} in {}",
ele, s
)));
}
}
Ok(mods)
}
fn default_hyperlink_rules() -> Vec<hyperlink::Rule> {
vec![
// URL with a protocol
hyperlink::Rule::new(r"\b\w+://(?:[\w.-]+)\.[a-z]{2,15}\S*\b", "$0").unwrap(),
// implicit mailto link
hyperlink::Rule::new(r"\b\w+@[\w-]+(\.[\w-]+)+\b", "mailto:$0").unwrap(),
]
}
fn default_term() -> String {
"xterm-256color".into()
}
fn default_font_size() -> f64 {
11.0
}
fn default_dpi() -> f64 {
96.0
}
impl Default for Config {
fn default() -> Self {
Self {
font_size: default_font_size(),
dpi: default_dpi(),
font: TextStyle::default(),
font_rules: Vec::new(),
font_system: FontSystemSelection::default(),
front_end: FrontEndSelection::default(),
pty: PtySystemSelection::default(),
colors: None,
scrollback_lines: None,
hyperlink_rules: default_hyperlink_rules(),
term: default_term(),
default_prog: None,
mux_server_unix_domain_socket_path: None,
mux_server_bind_address: None,
mux_server_pem_private_key: None,
mux_server_pem_cert: None,
mux_server_pem_ca: None,
mux_server_remote_address: None,
mux_client_pem_private_key: None,
mux_client_pem_cert: None,
mux_client_pem_ca: None,
mux_client_accept_invalid_hostnames: None,
mux_client_expected_cn: None,
mux_pem_root_certs: None,
keys: vec![],
}
}
}
#[cfg(target_os = "macos")]
const FONT_FAMILY: &str = "Menlo";
#[cfg(windows)]
const FONT_FAMILY: &str = "Consolas";
#[cfg(all(not(target_os = "macos"), not(windows)))]
const FONT_FAMILY: &str = "monospace";
#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Hash)]
pub struct FontAttributes {
/// The font family name
pub family: String,
/// Whether the font should be a bold variant
pub bold: Option<bool>,
/// Whether the font should be an italic variant
pub italic: Option<bool>,
}
impl Default for FontAttributes {
fn default() -> Self {
Self {
family: FONT_FAMILY.into(),
bold: None,
italic: None,
}
}
}
fn empty_font_attributes() -> Vec<FontAttributes> {
Vec::new()
}
/// Represents textual styling.
#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Hash)]
pub struct TextStyle {
#[serde(default = "empty_font_attributes")]
pub font: Vec<FontAttributes>,
/// If set, when rendering text that is set to the default
/// foreground color, use this color instead. This is most
/// useful in a `[[font_rules]]` section to implement changing
/// the text color for eg: bold text.
pub foreground: Option<RgbColor>,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
foreground: None,
font: vec![FontAttributes::default()],
}
}
}
impl TextStyle {
/// Make a version of this style with bold enabled.
fn make_bold(&self) -> Self {
Self {
foreground: self.foreground,
font: self
.font
.iter()
.map(|attr| {
let mut attr = attr.clone();
attr.bold = Some(true);
attr
})
.collect(),
}
}
/// Make a version of this style with italic enabled.
fn make_italic(&self) -> Self {
Self {
foreground: self.foreground,
font: self
.font
.iter()
.map(|attr| {
let mut attr = attr.clone();
attr.italic = Some(true);
attr
})
.collect(),
}
}
#[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
pub fn font_with_fallback(&self) -> Vec<FontAttributes> {
#[allow(unused_mut)]
let mut font = self.font.clone();
if font.is_empty() {
// This can happen when migratin from the old fontconfig_pattern
// configuration syntax; ensure that we have something likely
// sane in the font configuration
font.push(FontAttributes::default());
}
#[cfg(target_os = "macos")]
font.push(FontAttributes {
family: "Apple Color Emoji".into(),
bold: None,
italic: None,
});
#[cfg(target_os = "macos")]
font.push(FontAttributes {
family: "Apple Symbols".into(),
bold: None,
italic: None,
});
#[cfg(target_os = "macos")]
font.push(FontAttributes {
family: "Zapf Dingbats".into(),
bold: None,
italic: None,
});
#[cfg(windows)]
font.push(FontAttributes {
family: "Segoe UI Emoji".into(),
bold: None,
italic: None,
});
#[cfg(windows)]
font.push(FontAttributes {
family: "Segoe UI Symbol".into(),
bold: None,
italic: None,
});
font
}
}
/// Defines a rule that can be used to select a `TextStyle` given
/// an input `CellAttributes` value. The logic that applies the
/// matching can be found in src/font/mod.rs. The concept is that
/// the user can specify something like this:
///
/// ```
/// [[font_rules]]
/// italic = true
/// font = { font = [{family = "Operator Mono SSm Lig", italic=true}]}
/// ```
///
/// The above is translated as: "if the `CellAttributes` have the italic bit
/// set, then use the italic style of font rather than the default", and
/// stop processing further font rules.
#[derive(Debug, Default, Deserialize, Clone)]
pub struct StyleRule {
/// If present, this rule matches when CellAttributes::intensity holds
/// a value that matches this rule. Valid values are "Bold", "Normal",
/// "Half".
pub intensity: Option<term::Intensity>,
/// If present, this rule matches when CellAttributes::underline holds
/// a value that matches this rule. Valid values are "None", "Single",
/// "Double".
pub underline: Option<term::Underline>,
/// If present, this rule matches when CellAttributes::italic holds
/// a value that matches this rule.
pub italic: Option<bool>,
/// If present, this rule matches when CellAttributes::blink holds
/// a value that matches this rule.
pub blink: Option<term::Blink>,
/// If present, this rule matches when CellAttributes::reverse holds
/// a value that matches this rule.
pub reverse: Option<bool>,
/// If present, this rule matches when CellAttributes::strikethrough holds
/// a value that matches this rule.
pub strikethrough: Option<bool>,
/// If present, this rule matches when CellAttributes::invisible holds
/// a value that matches this rule.
pub invisible: Option<bool>,
/// When this rule matches, `font` specifies the styling to be used.
pub font: TextStyle,
}
fn compute_runtime_dir() -> Result<PathBuf, Error> {
if let Some(runtime) = dirs::runtime_dir() {
return Ok(runtime.join("wezterm"));
}
let home = dirs::home_dir().ok_or_else(|| err_msg("can't find home dir"))?;
Ok(home.join(".local/share/wezterm"))
}
lazy_static! {
static ref HOME_DIR: PathBuf = dirs::home_dir().expect("can't find HOME dir");
static ref RUNTIME_DIR: PathBuf = compute_runtime_dir().unwrap();
}
impl Config {
pub fn load() -> Result<Self, Error> {
// Note that the directories crate has methods for locating project
// specific config directories, but only returns one of them, not
// multiple. In addition, it spawns a lot of subprocesses,
// so we do this bit "by-hand"
let paths = [
HOME_DIR
.join(".config")
.join("wezterm")
.join("wezterm.toml"),
HOME_DIR.join(".wezterm.toml"),
];
for p in &paths {
let mut file = match fs::File::open(p) {
Ok(file) => file,
Err(err) => match err.kind() {
std::io::ErrorKind::NotFound => continue,
_ => bail!("Error opening {}: {:?}", p.display(), err),
},
};
let mut s = String::new();
file.read_to_string(&mut s)?;
let cfg: Self = toml::from_str(&s)
.map_err(|e| format_err!("Error parsing TOML from {}: {:?}", p.display(), e))?;
// Compute but discard the key bindings here so that we raise any
// problems earlier than we use them.
let _ = cfg.key_bindings()?;
return Ok(cfg.compute_extra_defaults());
}
Ok(Self::default().compute_extra_defaults())
}
pub fn default_config() -> Self {
Self::default().compute_extra_defaults()
}
pub fn key_bindings(&self) -> Fallible<HashMap<(KeyCode, Modifiers), KeyAssignment>> {
let mut map = HashMap::new();
for k in &self.keys {
let value = k.try_into()?;
map.insert((k.key, k.mods), value);
}
Ok(map)
}
/// In some cases we need to compute expanded values based
/// on those provided by the user. This is where we do that.
fn compute_extra_defaults(&self) -> Self {
let mut cfg = self.clone();
if cfg.mux_server_unix_domain_socket_path.is_none() {
cfg.mux_server_unix_domain_socket_path =
RUNTIME_DIR.join("sock").to_str().map(str::to_owned);
}
if cfg.font_rules.is_empty() {
// Expand out some reasonable default font rules
let bold = self.font.make_bold();
let italic = self.font.make_italic();
let bold_italic = bold.make_italic();
cfg.font_rules.push(StyleRule {
italic: Some(true),
font: italic,
..Default::default()
});
cfg.font_rules.push(StyleRule {
intensity: Some(term::Intensity::Bold),
font: bold,
..Default::default()
});
cfg.font_rules.push(StyleRule {
italic: Some(true),
intensity: Some(term::Intensity::Bold),
font: bold_italic,
..Default::default()
});
}
cfg
}
/// On macOS, we get launched from: eg: spotlight or alfred
/// or the finder with whatever SHELL was set to at login time
/// (which have been subsequently changed via `chsh`) and with
/// a cwd=/.
/// That feels a bit broken, so we follow the lead of
/// Terminal.app and use `login -pf $USER` as the default
/// program to run.
/// This function computes and returns that command.
/// We don't do this on Linux because the linux `login`
/// program refuses to run except when started by root.
#[cfg(target_os = "macos")]
fn macos_login() -> Result<Vec<String>, Error> {
let ent = unsafe { libc::getpwuid(libc::getuid()) };
if ent.is_null() {
bail!("unable to resolve my own uid");
} else {
let name = unsafe { std::ffi::CStr::from_ptr((*ent).pw_name) };
let name = name.to_str().map(str::to_owned)?;
Ok(vec!["login".to_owned(), "-pf".to_owned(), name])
}
}
pub fn default_prog(&self) -> Result<Vec<String>, Error> {
if let Some(prog) = self.default_prog.as_ref() {
Ok(prog.clone())
} else {
#[cfg(target_os = "macos")]
{
if let Ok(login) = Self::macos_login() {
return Ok(login);
}
}
Ok(vec![get_shell()?])
}
}
pub fn build_prog(&self, prog: Option<Vec<&OsStr>>) -> Result<CommandBuilder, Error> {
let mut cmd = match prog {
Some(args) => {
let mut args = args.iter();
let mut cmd = CommandBuilder::new(args.next().expect("executable name"));
cmd.args(args);
cmd
}
None => {
let prog = self.default_prog()?;
let mut args = prog.iter();
let mut cmd = CommandBuilder::new(args.next().expect("executable name"));
cmd.args(args);
cmd
}
};
cmd.env("TERM", &self.term);
Ok(cmd)
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct Palette {
/// The text color to use when the attributes are reset to default
pub foreground: Option<RgbColor>,
/// The background color to use when the attributes are reset to default
pub background: Option<RgbColor>,
/// The color of the cursor
pub cursor_fg: Option<RgbColor>,
pub cursor_bg: Option<RgbColor>,
/// The color of selected text
pub selection_fg: Option<RgbColor>,
pub selection_bg: Option<RgbColor>,
/// A list of 8 colors corresponding to the basic ANSI palette
pub ansi: Option<[RgbColor; 8]>,
/// A list of 8 colors corresponding to bright versions of the
/// ANSI palette
pub brights: Option<[RgbColor; 8]>,
}
impl From<Palette> for term::color::ColorPalette {
fn from(cfg: Palette) -> term::color::ColorPalette {
let mut p = term::color::ColorPalette::default();
macro_rules! apply_color {
($name:ident) => {
if let Some($name) = cfg.$name {
p.$name = $name;
}
};
}
apply_color!(foreground);
apply_color!(background);
apply_color!(cursor_fg);
apply_color!(cursor_bg);
apply_color!(selection_fg);
apply_color!(selection_bg);
if let Some(ansi) = cfg.ansi {
for (idx, col) in ansi.iter().enumerate() {
p.colors.0[idx] = *col;
}
}
if let Some(brights) = cfg.brights {
for (idx, col) in brights.iter().enumerate() {
p.colors.0[idx + 8] = *col;
}
}
p
}
}