Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,466 @@
|
||||
use crate::settings::import::config::ThemeError;
|
||||
use async_recursion::async_recursion;
|
||||
use async_trait::async_trait;
|
||||
use serde::Deserialize;
|
||||
use std::{env, io::ErrorKind, path::PathBuf};
|
||||
use warp_core::ui::{
|
||||
color::hex_color::coloru_from_hex_string,
|
||||
theme::{AnsiColor, AnsiColors, TerminalColors, WarpTheme},
|
||||
};
|
||||
use warpui::fonts::FontInfo;
|
||||
|
||||
use super::config::{
|
||||
calculate_accent_color, Config, ConfigError, ImportableSetting, ParseableConfig, SettingType,
|
||||
ThemeType,
|
||||
};
|
||||
use pathfinder_color::ColorU;
|
||||
|
||||
type AlacrittyColor = String;
|
||||
const CONFIG_DEPTH_LIMIT: u8 = 5;
|
||||
|
||||
// Constants for Alacritty's defaults: see the "Colors" section
|
||||
// of https://alacritty.org/config-alacritty.html
|
||||
pub const DEFAULT_ALACRITTY_FOREGROUND: ColorU = ColorU {
|
||||
r: 0xd8,
|
||||
g: 0xd8,
|
||||
b: 0xd8,
|
||||
a: 255,
|
||||
};
|
||||
|
||||
pub const DEFAULT_ALACRITTY_BACKGROUND: ColorU = ColorU {
|
||||
r: 0x18,
|
||||
g: 0x18,
|
||||
b: 0x18,
|
||||
a: 255,
|
||||
};
|
||||
|
||||
pub const DEFAULT_ALACRITTY_BRIGHT_COLORS: AnsiColors = AnsiColors {
|
||||
black: AnsiColor {
|
||||
r: 0x6b,
|
||||
g: 0x6b,
|
||||
b: 0x6b,
|
||||
},
|
||||
red: AnsiColor {
|
||||
r: 0xc5,
|
||||
g: 0x55,
|
||||
b: 0x55,
|
||||
},
|
||||
green: AnsiColor {
|
||||
r: 0xaa,
|
||||
g: 0xc4,
|
||||
b: 0x74,
|
||||
},
|
||||
yellow: AnsiColor {
|
||||
r: 0xfe,
|
||||
g: 0xca,
|
||||
b: 0x88,
|
||||
},
|
||||
blue: AnsiColor {
|
||||
r: 0x82,
|
||||
g: 0xb8,
|
||||
b: 0xc8,
|
||||
},
|
||||
magenta: AnsiColor {
|
||||
r: 0xc2,
|
||||
g: 0x8c,
|
||||
b: 0xb8,
|
||||
},
|
||||
cyan: AnsiColor {
|
||||
r: 0x93,
|
||||
g: 0xd3,
|
||||
b: 0xc3,
|
||||
},
|
||||
white: AnsiColor {
|
||||
r: 0xf8,
|
||||
g: 0xf8,
|
||||
b: 0xf8,
|
||||
},
|
||||
};
|
||||
|
||||
pub const DEFAULT_ALACRITTY_NORMAL_COLORS: AnsiColors = AnsiColors {
|
||||
black: AnsiColor {
|
||||
r: 0x18,
|
||||
g: 0x18,
|
||||
b: 0x18,
|
||||
},
|
||||
red: AnsiColor {
|
||||
r: 0xac,
|
||||
g: 0x42,
|
||||
b: 0x42,
|
||||
},
|
||||
green: AnsiColor {
|
||||
r: 0x90,
|
||||
g: 0xa9,
|
||||
b: 0x59,
|
||||
},
|
||||
yellow: AnsiColor {
|
||||
r: 0xf4,
|
||||
g: 0xbf,
|
||||
b: 0x75,
|
||||
},
|
||||
blue: AnsiColor {
|
||||
r: 0x6a,
|
||||
g: 0x9f,
|
||||
b: 0xb5,
|
||||
},
|
||||
magenta: AnsiColor {
|
||||
r: 0xaa,
|
||||
g: 0x75,
|
||||
b: 0x9f,
|
||||
},
|
||||
cyan: AnsiColor {
|
||||
r: 0x75,
|
||||
g: 0xb5,
|
||||
b: 0xaa,
|
||||
},
|
||||
white: AnsiColor {
|
||||
r: 0xd8,
|
||||
g: 0xd8,
|
||||
b: 0xd8,
|
||||
},
|
||||
};
|
||||
|
||||
/// `RecursivelyParseable` is a trait that bundles recursive functions over the different
|
||||
/// data structures in our representation of Alacritty's config.
|
||||
trait RecursivelyParseable {
|
||||
/// Fills in any None fields from `self` with values from `other`.
|
||||
fn merge_left(self, other: Self) -> Self;
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct AlacrittyTheme {
|
||||
primary: Option<PrimaryAlacrittyColors>,
|
||||
normal: Option<AlacrittyColors>,
|
||||
bright: Option<AlacrittyColors>,
|
||||
cursor: Option<AlacrittyCursorColor>,
|
||||
}
|
||||
|
||||
/// Since Alacritty's config stores the cursor color in colors.cursor.cursor, we use this struct
|
||||
/// to match Alacritty's keys exactly.
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct AlacrittyCursorColor {
|
||||
cursor: Option<AlacrittyColor>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct PrimaryAlacrittyColors {
|
||||
foreground: Option<AlacrittyColor>,
|
||||
background: Option<AlacrittyColor>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct AlacrittyColors {
|
||||
black: Option<AlacrittyColor>,
|
||||
red: Option<AlacrittyColor>,
|
||||
green: Option<AlacrittyColor>,
|
||||
yellow: Option<AlacrittyColor>,
|
||||
blue: Option<AlacrittyColor>,
|
||||
magenta: Option<AlacrittyColor>,
|
||||
cyan: Option<AlacrittyColor>,
|
||||
white: Option<AlacrittyColor>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct AlacrittyConfig {
|
||||
colors: Option<AlacrittyTheme>,
|
||||
import: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl RecursivelyParseable for AlacrittyConfig {
|
||||
fn merge_left(mut self, other: Self) -> Self {
|
||||
self.colors = self
|
||||
.colors
|
||||
.map(|inner| inner.merge_left(other.colors.clone().unwrap_or_default()))
|
||||
.or(other.colors);
|
||||
|
||||
// Concatenate import lists if we can. Otherwise, take the other import list.
|
||||
if let Some(import) = self.import {
|
||||
self.import = Some(
|
||||
import
|
||||
.into_iter()
|
||||
.chain(other.import.unwrap_or_default())
|
||||
.collect(),
|
||||
);
|
||||
} else {
|
||||
self.import = other.import;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RecursivelyParseable for AlacrittyColors {
|
||||
fn merge_left(mut self, other: Self) -> Self {
|
||||
self.black = self.black.or(other.black);
|
||||
self.red = self.red.or(other.red);
|
||||
self.green = self.green.or(other.green);
|
||||
self.yellow = self.yellow.or(other.yellow);
|
||||
self.blue = self.blue.or(other.blue);
|
||||
self.magenta = self.magenta.or(other.magenta);
|
||||
self.cyan = self.cyan.or(other.cyan);
|
||||
self.white = self.white.or(other.white);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RecursivelyParseable for PrimaryAlacrittyColors {
|
||||
fn merge_left(mut self, other: Self) -> Self {
|
||||
self.foreground = self.foreground.or(other.foreground);
|
||||
self.background = self.background.or(other.background);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RecursivelyParseable for AlacrittyTheme {
|
||||
fn merge_left(mut self, other: Self) -> Self {
|
||||
self.primary = self
|
||||
.primary
|
||||
.map(|inner| inner.merge_left(other.primary.clone().unwrap_or_default()))
|
||||
.or(other.primary);
|
||||
|
||||
self.normal = self
|
||||
.normal
|
||||
.map(|inner| inner.merge_left(other.normal.clone().unwrap_or_default()))
|
||||
.or(other.normal);
|
||||
|
||||
self.bright = self
|
||||
.bright
|
||||
.map(|inner| inner.merge_left(other.bright.clone().unwrap_or_default()))
|
||||
.or(other.bright);
|
||||
|
||||
self.cursor = self
|
||||
.cursor
|
||||
.map(|inner| inner.merge_left(other.cursor.clone().unwrap_or_default()))
|
||||
.or(other.cursor);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RecursivelyParseable for AlacrittyCursorColor {
|
||||
fn merge_left(mut self, other: Self) -> Self {
|
||||
self.cursor = self.cursor.or(other.cursor);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a hex string into an AnsiColor, returning an error only if
|
||||
/// the hex string is present but malformatted.
|
||||
fn parse_alacritty_color(
|
||||
color_string: Option<AlacrittyColor>,
|
||||
) -> Result<Option<AnsiColor>, ThemeError> {
|
||||
let Some(color) = color_string else {
|
||||
return Ok(None);
|
||||
};
|
||||
if color.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
match coloru_from_hex_string(color.as_str()) {
|
||||
Ok(color) => Ok(Some(color.into())),
|
||||
Err(e) => Err(ThemeError::HexColorError(e)),
|
||||
}
|
||||
}
|
||||
|
||||
impl AlacrittyTheme {
|
||||
fn parse(self) -> Result<ThemeType, ThemeError> {
|
||||
let primary = self.primary.unwrap_or_default();
|
||||
let foreground = parse_alacritty_color(primary.foreground)?
|
||||
.unwrap_or(DEFAULT_ALACRITTY_FOREGROUND.into());
|
||||
let background = parse_alacritty_color(primary.background)?
|
||||
.unwrap_or(DEFAULT_ALACRITTY_BACKGROUND.into());
|
||||
let cursor_color =
|
||||
parse_alacritty_color(self.cursor.unwrap_or_default().cursor)?.unwrap_or(foreground);
|
||||
|
||||
// Create TerminalColors from the config, filling in any missing values from Alacritty's default
|
||||
// normal and bright colors.
|
||||
let terminal_colors = TerminalColors {
|
||||
normal: self
|
||||
.normal
|
||||
.unwrap_or_default()
|
||||
.into_ansi_with_default(DEFAULT_ALACRITTY_NORMAL_COLORS)?,
|
||||
bright: self
|
||||
.bright
|
||||
.unwrap_or_default()
|
||||
.into_ansi_with_default(DEFAULT_ALACRITTY_BRIGHT_COLORS)?,
|
||||
};
|
||||
|
||||
if foreground == DEFAULT_ALACRITTY_FOREGROUND.into()
|
||||
|| background == DEFAULT_ALACRITTY_BACKGROUND.into()
|
||||
{
|
||||
Err(ThemeError::MissingValueError)
|
||||
} else {
|
||||
let bright = terminal_colors.bright;
|
||||
let accent = calculate_accent_color(background, foreground, cursor_color, bright);
|
||||
Ok(ThemeType::Single(WarpTheme::new(
|
||||
background.into(),
|
||||
foreground.into(),
|
||||
accent.into(),
|
||||
Some(cursor_color.into()),
|
||||
None,
|
||||
terminal_colors,
|
||||
None,
|
||||
Some(String::from("Imported Alacritty Theme")),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AlacrittyColors {
|
||||
/// Returns terminal colors with Warp's default colors substituted in for any
|
||||
/// missing terminal colors.
|
||||
fn into_ansi_with_default(self, default: AnsiColors) -> Result<AnsiColors, ThemeError> {
|
||||
Ok(AnsiColors {
|
||||
black: parse_alacritty_color(self.black)?.unwrap_or(default.black),
|
||||
red: parse_alacritty_color(self.red)?.unwrap_or(default.red),
|
||||
green: parse_alacritty_color(self.green)?.unwrap_or(default.green),
|
||||
yellow: parse_alacritty_color(self.yellow)?.unwrap_or(default.yellow),
|
||||
blue: parse_alacritty_color(self.blue)?.unwrap_or(default.blue),
|
||||
magenta: parse_alacritty_color(self.magenta)?.unwrap_or(default.magenta),
|
||||
cyan: parse_alacritty_color(self.cyan)?.unwrap_or(default.cyan),
|
||||
white: parse_alacritty_color(self.white)?.unwrap_or(default.white),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ParseableConfig for AlacrittyConfig {
|
||||
fn parse(self, _font_info: &[FontInfo]) -> Config {
|
||||
Config {
|
||||
theme: ImportableSetting::new(self.parse_theme(), SettingType::Theme),
|
||||
terminal_name: "Alacritty".to_string(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
async fn from_file(path: PathBuf) -> Result<Vec<Self>, ConfigError> {
|
||||
Self::from_file_bounded_depth(path, 0).await
|
||||
}
|
||||
|
||||
fn default_paths() -> Vec<std::path::PathBuf> {
|
||||
// We follow Alacritty's strategy described here: https://github.com/alacritty/alacritty?tab=readme-ov-file#configuration.
|
||||
// Since alacritty uses the `xdg` crate to read config files, we include paths in XDG_CONFIG_DIRS.
|
||||
|
||||
// If we are on Windows, search the only path: %APPDATA%\alacritty\alacritty.toml.
|
||||
if cfg!(windows) {
|
||||
return dirs::config_dir()
|
||||
.map(|path| vec![path.join("alacritty").join("alacritty.toml")])
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
let mut file_paths = vec![];
|
||||
let mut second_file_paths = vec![];
|
||||
|
||||
let xdg_config_dirs = env::var("XDG_CONFIG_DIRS")
|
||||
.ok()
|
||||
.filter(|val| !val.is_empty())
|
||||
.or_else(|| Some("/usr/local/share/:/usr/share/".to_string()));
|
||||
|
||||
// Add to file_paths:
|
||||
// - $XDG_CONFIG_HOME/alacritty/alacritty.toml
|
||||
// Add to second_file_paths:
|
||||
// - $XDG_CONFIG_HOME/alacritty.toml
|
||||
if let Some(xdg_config_home) = dirs::config_dir() {
|
||||
file_paths.push(xdg_config_home.join("alacritty").join("alacritty.toml"));
|
||||
second_file_paths.push(xdg_config_home.join("alacritty.toml"));
|
||||
}
|
||||
|
||||
// Add to file_paths:
|
||||
// - $XDG_CONFIG_DIRS/alacritty/alacritty.toml
|
||||
// Add to second_file_paths:
|
||||
// - $XDG_CONFIG_DIRS/alacritty.toml
|
||||
if let Some(xdg_config_dirs) = xdg_config_dirs.clone() {
|
||||
for dir in xdg_config_dirs.split(':') {
|
||||
if !dir.is_empty() {
|
||||
file_paths.push(PathBuf::from(dir).join("alacritty").join("alacritty.toml"));
|
||||
second_file_paths.push(PathBuf::from(dir).join("alacritty.toml"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add second_file_paths to the end of file_paths to maintain the correct order.
|
||||
file_paths.extend(second_file_paths);
|
||||
|
||||
// As a backup, check
|
||||
// - $HOME/.config/alacritty/alacritty.toml
|
||||
// - $HOME/.alacritty.toml
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
file_paths.push(
|
||||
home.join(".config")
|
||||
.join("alacritty")
|
||||
.join("alacritty.toml"),
|
||||
);
|
||||
file_paths.push(home.join(".alacritty.toml"));
|
||||
}
|
||||
|
||||
file_paths
|
||||
}
|
||||
|
||||
fn remove_default_values(self) -> Self {
|
||||
// The default Alacritty config is an empty file,
|
||||
// so we don't need to remove anything.
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AlacrittyConfig {
|
||||
/// Reads Alacritty configs asynchronously and folds in any imported configs up to a given depth.
|
||||
#[async_recursion]
|
||||
async fn from_file_bounded_depth(path: PathBuf, depth: u8) -> Result<Vec<Self>, ConfigError> {
|
||||
// Since Alacritty only reads configs up to depth 5,
|
||||
// return an empty config if we are at depth 5.
|
||||
if depth >= CONFIG_DEPTH_LIMIT {
|
||||
log::warn!(
|
||||
"Maximum configuration depth reached while parsing Alacritty configuration at {path:?}"
|
||||
);
|
||||
return Ok(vec![AlacrittyConfig::default()]);
|
||||
}
|
||||
let contents = match async_fs::read_to_string(path.clone()).await {
|
||||
Ok(string) => string,
|
||||
Err(e) => {
|
||||
if e.kind() == ErrorKind::NotFound {
|
||||
return Err(ConfigError::FileNotFoundError);
|
||||
} else {
|
||||
return Err(ConfigError::FileIOError(e));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let Ok(mut out) = toml::from_str::<AlacrittyConfig>(contents.as_str()) else {
|
||||
return Err(ConfigError::MalformattedFileError(path));
|
||||
};
|
||||
// Alacritty prioritizes settings set in the current config.
|
||||
// If a setting is not set in the highest-level config, it then looks to the imported configs.
|
||||
// It reads each imported config in order, effectively prioritizing the last config listed.
|
||||
// It does support tilde as the home directory, but not environment variables or relative paths.
|
||||
|
||||
// Start with a config with None in all fields.
|
||||
let mut imported_config: AlacrittyConfig = Default::default();
|
||||
if let Some(ref imports) = out.import {
|
||||
// Reverse the iterator, so that the last configs listed get priority.
|
||||
for file_path in imports.iter().rev() {
|
||||
// Merge left, which replaces None values in the first argument
|
||||
// with values from the second argument.
|
||||
imported_config = imported_config.merge_left(
|
||||
Self::from_file_bounded_depth(
|
||||
PathBuf::from(shellexpand::tilde(file_path).into_owned().to_string()),
|
||||
depth + 1,
|
||||
)
|
||||
.await?
|
||||
.pop()
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
}
|
||||
out = out.merge_left(imported_config);
|
||||
Ok(vec![out])
|
||||
}
|
||||
|
||||
fn parse_theme(self) -> Result<ThemeType, ThemeError> {
|
||||
match self.colors {
|
||||
Some(colors) => Ok(colors.parse()?),
|
||||
None => Err(ThemeError::MissingValueError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "alacritty_parser_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,475 @@
|
||||
use async_io::block_on;
|
||||
use virtual_fs::{Stub, VirtualFS};
|
||||
use warp_core::ui::{color::hex_color::coloru_from_hex_string, theme::AnsiColor};
|
||||
|
||||
use crate::settings::import::config::{ParseableConfig, ThemeType};
|
||||
|
||||
use super::{
|
||||
AlacrittyColors, AlacrittyConfig, AlacrittyTheme, PrimaryAlacrittyColors, RecursivelyParseable,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_parse_cobalt2() {
|
||||
let cobalt2_config = "# From the famous Cobalt2 sublime theme
|
||||
# Source https//github.com/wesbos/cobalt2/tree/master/Cobalt2
|
||||
|
||||
# Default colors
|
||||
[colors.primary]
|
||||
background = '#122637'
|
||||
foreground = '#ffffff'
|
||||
|
||||
[colors.cursor]
|
||||
text = '#122637'
|
||||
cursor = '#f0cb09'
|
||||
|
||||
# Normal colors
|
||||
[colors.normal]
|
||||
black = '#000000'
|
||||
red = '#ff0000'
|
||||
green = '#37dd21'
|
||||
yellow = '#fee409'
|
||||
blue = '#1460d2'
|
||||
magenta = '#ff005d'
|
||||
cyan = '#00bbbb'
|
||||
white = '#bbbbbb'
|
||||
|
||||
# Bright colors
|
||||
[colors.bright]
|
||||
black = '#545454'
|
||||
red = '#f40d17'
|
||||
green = '#3bcf1d'
|
||||
yellow = '#ecc809'
|
||||
blue = '#5555ff'
|
||||
magenta = '#ff55ff'
|
||||
cyan = '#6ae3f9'
|
||||
white = '#ffffff'";
|
||||
let config: AlacrittyConfig =
|
||||
toml::from_str(cobalt2_config).expect("Should be able to parse toml!");
|
||||
let ThemeType::Single(theme) = config
|
||||
.colors
|
||||
.expect("Should have read colors!")
|
||||
.parse()
|
||||
.expect("Theme should have read!")
|
||||
else {
|
||||
panic!("Should not have a dark and light theme for Alacritty!")
|
||||
};
|
||||
|
||||
// Check that the three primary colors are the same.
|
||||
assert_eq!(
|
||||
theme.accent().into_solid(),
|
||||
coloru_from_hex_string("#f0cb09").expect("Should be able to parse a color!")
|
||||
);
|
||||
assert_eq!(
|
||||
theme.background().into_solid(),
|
||||
coloru_from_hex_string("#122637").expect("Should be able to parse a color!")
|
||||
);
|
||||
assert_eq!(
|
||||
theme.foreground().into_solid(),
|
||||
coloru_from_hex_string("#ffffff").expect("Should be able to parse a color!")
|
||||
);
|
||||
|
||||
// Check some terminal colors.
|
||||
assert_eq!(
|
||||
theme.terminal_colors().bright.yellow,
|
||||
AnsiColor {
|
||||
r: 0xec,
|
||||
g: 0xc8,
|
||||
b: 0x09
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
theme.terminal_colors().bright.cyan,
|
||||
AnsiColor {
|
||||
r: 0x6a,
|
||||
g: 0xe3,
|
||||
b: 0xf9
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
theme.terminal_colors().normal.yellow,
|
||||
AnsiColor {
|
||||
r: 0xfe,
|
||||
g: 0xe4,
|
||||
b: 0x09
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
theme.terminal_colors().normal.cyan,
|
||||
AnsiColor {
|
||||
r: 0x00,
|
||||
g: 0xbb,
|
||||
b: 0xbb
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_cobalt2_missing_color() {
|
||||
let cobalt2_config = "# From the famous Cobalt2 sublime theme
|
||||
# Source https//github.com/wesbos/cobalt2/tree/master/Cobalt2
|
||||
|
||||
# Default colors
|
||||
[colors.primary]
|
||||
background = '#122637'
|
||||
foreground = '#ffffff'
|
||||
|
||||
[colors.cursor]
|
||||
text = '#122637'
|
||||
cursor = '#f0cb09'
|
||||
|
||||
# Normal colors
|
||||
[colors.normal]
|
||||
red = '#ff0000'
|
||||
green = '#37dd21'
|
||||
yellow = '#fee409'
|
||||
blue = '#1460d2'
|
||||
magenta = '#ff005d'
|
||||
cyan = '#00bbbb'
|
||||
white = '#bbbbbb'
|
||||
|
||||
# Bright colors
|
||||
[colors.bright]
|
||||
black = '#545454'
|
||||
red = '#f40d17'
|
||||
green = '#3bcf1d'
|
||||
yellow = '#ecc809'
|
||||
blue = '#5555ff'
|
||||
magenta = '#ff55ff'
|
||||
cyan = '#6ae3f9'
|
||||
white = '#ffffff'";
|
||||
let config: AlacrittyConfig =
|
||||
toml::from_str(cobalt2_config).expect("Should be able to parse toml!");
|
||||
let ThemeType::Single(theme) = config
|
||||
.colors
|
||||
.expect("Should have read colors!")
|
||||
.parse()
|
||||
.expect("Theme should have read!")
|
||||
else {
|
||||
panic!("Should not have a dark and light theme for Alacritty!")
|
||||
};
|
||||
|
||||
// Check that the three primary colors are the same.
|
||||
assert_eq!(
|
||||
theme.accent().into_solid(),
|
||||
coloru_from_hex_string("#f0cb09").expect("Should be able to parse a color!")
|
||||
);
|
||||
assert_eq!(
|
||||
theme.background().into_solid(),
|
||||
coloru_from_hex_string("#122637").expect("Should be able to parse a color!")
|
||||
);
|
||||
assert_eq!(
|
||||
theme.foreground().into_solid(),
|
||||
coloru_from_hex_string("#ffffff").expect("Should be able to parse a color!")
|
||||
);
|
||||
|
||||
// Check some terminal colors.
|
||||
assert_eq!(
|
||||
theme.terminal_colors().bright.yellow,
|
||||
AnsiColor {
|
||||
r: 0xec,
|
||||
g: 0xc8,
|
||||
b: 0x09
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
theme.terminal_colors().bright.cyan,
|
||||
AnsiColor {
|
||||
r: 0x6a,
|
||||
g: 0xe3,
|
||||
b: 0xf9
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
theme.terminal_colors().normal.yellow,
|
||||
AnsiColor {
|
||||
r: 0xfe,
|
||||
g: 0xe4,
|
||||
b: 0x09
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
theme.terminal_colors().normal.cyan,
|
||||
AnsiColor {
|
||||
r: 0x00,
|
||||
g: 0xbb,
|
||||
b: 0xbb
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
theme.terminal_colors().normal.black,
|
||||
AnsiColor {
|
||||
r: 0x18,
|
||||
g: 0x18,
|
||||
b: 0x18,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_cobalt2_missing_section() {
|
||||
let cobalt2_config = "# From the famous Cobalt2 sublime theme
|
||||
# Source https//github.com/wesbos/cobalt2/tree/master/Cobalt2
|
||||
|
||||
# Default colors
|
||||
[colors.primary]
|
||||
background = '#122637'
|
||||
foreground = '#ffffff'
|
||||
|
||||
[colors.cursor]
|
||||
text = '#122637'
|
||||
cursor = '#f0cb09'";
|
||||
let config: AlacrittyConfig =
|
||||
toml::from_str(cobalt2_config).expect("Should be able to parse toml!");
|
||||
let ThemeType::Single(theme) = config
|
||||
.colors
|
||||
.expect("Should have read colors!")
|
||||
.parse()
|
||||
.expect("Theme should have read!")
|
||||
else {
|
||||
panic!("Should not have a dark and light theme for Alacritty!")
|
||||
};
|
||||
|
||||
// Check that the three primary colors are the same.
|
||||
assert_eq!(
|
||||
theme.accent().into_solid(),
|
||||
coloru_from_hex_string("#f0cb09").expect("Should be able to parse a color!")
|
||||
);
|
||||
assert_eq!(
|
||||
theme.background().into_solid(),
|
||||
coloru_from_hex_string("#122637").expect("Should be able to parse a color!")
|
||||
);
|
||||
assert_eq!(
|
||||
theme.foreground().into_solid(),
|
||||
coloru_from_hex_string("#ffffff").expect("Should be able to parse a color!")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_cobalt2_bad_terminal_color() {
|
||||
let cobalt2_config = "# From the famous Cobalt2 sublime theme
|
||||
# Source https//github.com/wesbos/cobalt2/tree/master/Cobalt2
|
||||
|
||||
# Default colors
|
||||
[colors.primary]
|
||||
background = '#122637'
|
||||
foreground = '#ffffff'
|
||||
|
||||
[colors.cursor]
|
||||
text = '#122637'
|
||||
cursor = '#f0cb09'
|
||||
|
||||
# Normal colors
|
||||
[colors.normal]
|
||||
red = '#ff000'
|
||||
green = '#37dd21'
|
||||
yellow = '#fee409'
|
||||
blue = '#1460d2'
|
||||
magenta = '#ff005d'
|
||||
cyan = '#00bbbb'
|
||||
white = '#bbbbbb'
|
||||
|
||||
# Bright colors
|
||||
[colors.bright]
|
||||
black = '#545454'
|
||||
red = '#f40d17'
|
||||
green = '#3bcf1d'
|
||||
yellow = '#ecc809'
|
||||
blue = '#5555ff'
|
||||
magenta = '#ff55ff'
|
||||
cyan = '#6ae3f9'
|
||||
white = '#ffffff'";
|
||||
let config: AlacrittyConfig =
|
||||
toml::from_str(cobalt2_config).expect("Should be able to parse toml!");
|
||||
let _ = config
|
||||
.colors
|
||||
.expect("Should have read colors!")
|
||||
.parse()
|
||||
.expect_err("Theme should not have read!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_cobalt2_no_colors() {
|
||||
let cobalt2_config = "# From the famous Cobalt2 sublime theme
|
||||
# Source https//github.com/wesbos/cobalt2/tree/master/Cobalt2
|
||||
|
||||
# Default colors
|
||||
[colors.primary]
|
||||
background = ''
|
||||
foreground = ''
|
||||
|
||||
[colors.cursor]
|
||||
text = ''
|
||||
cursor = ''";
|
||||
let config: AlacrittyConfig =
|
||||
toml::from_str(cobalt2_config).expect("Should be able to parse toml!");
|
||||
let _ = config
|
||||
.colors
|
||||
.expect("Should have read colors!")
|
||||
.parse()
|
||||
.expect_err("Theme should not have read!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_left() {
|
||||
let mut colors = AlacrittyTheme {
|
||||
primary: Some(PrimaryAlacrittyColors {
|
||||
foreground: Some("#ffffff".to_string()),
|
||||
background: Some("#000000".to_string()),
|
||||
}),
|
||||
normal: Some(AlacrittyColors {
|
||||
black: Some("#000000".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let second_colors = AlacrittyTheme {
|
||||
primary: Some(PrimaryAlacrittyColors {
|
||||
foreground: None,
|
||||
background: Some("#ff0000".to_string()),
|
||||
}),
|
||||
normal: Some(AlacrittyColors {
|
||||
red: Some("#ff1111".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
colors = colors.merge_left(second_colors);
|
||||
let primary = colors
|
||||
.primary
|
||||
.clone()
|
||||
.expect("Primary colors should be present");
|
||||
let normal = colors
|
||||
.normal
|
||||
.clone()
|
||||
.expect("Normal colors should be present");
|
||||
|
||||
// Check all four combinations of present and absent in the inner struct.
|
||||
assert_eq!(primary.foreground, Some("#ffffff".to_string()));
|
||||
assert_eq!(primary.background, Some("#000000".to_string()));
|
||||
assert_eq!(normal.black, Some("#000000".to_string()));
|
||||
assert_eq!(normal.red, Some("#ff1111".to_string()));
|
||||
assert_eq!(normal.white, None);
|
||||
|
||||
// Check that None values are preserved.
|
||||
assert!(colors.bright.is_none());
|
||||
}
|
||||
|
||||
/// This is a unit test that tests reading from the default config location with one import.
|
||||
#[test]
|
||||
fn test_parse_cobalt2_from_import() {
|
||||
VirtualFS::test("test_parse_cobalt2_from_import", |dirs, mut sandbox| {
|
||||
sandbox.mkdir("config");
|
||||
sandbox.with_files(vec![
|
||||
Stub::FileWithContent(
|
||||
"config/config.toml",
|
||||
format!(
|
||||
"
|
||||
import = [{:?}]
|
||||
",
|
||||
dirs.tests().join("config").join("Cobalt2.toml")
|
||||
)
|
||||
.as_str(),
|
||||
),
|
||||
Stub::FileWithContent(
|
||||
"config/Cobalt2.toml",
|
||||
"# From the famous Cobalt2 sublime theme
|
||||
# Source https//github.com/wesbos/cobalt2/tree/master/Cobalt2
|
||||
|
||||
# Default colors
|
||||
[colors.primary]
|
||||
background = '#122637'
|
||||
foreground = '#ffffff'
|
||||
|
||||
[colors.cursor]
|
||||
text = '#122637'
|
||||
cursor = '#f0cb09'
|
||||
|
||||
# Normal colors
|
||||
[colors.normal]
|
||||
black = '#000000'
|
||||
red = '#ff0000'
|
||||
green = '#37dd21'
|
||||
yellow = '#fee409'
|
||||
blue = '#1460d2'
|
||||
magenta = '#ff005d'
|
||||
cyan = '#00bbbb'
|
||||
white = '#bbbbbb'
|
||||
|
||||
# Bright colors
|
||||
[colors.bright]
|
||||
black = '#545454'
|
||||
red = '#f40d17'
|
||||
green = '#3bcf1d'
|
||||
yellow = '#ecc809'
|
||||
blue = '#5555ff'
|
||||
magenta = '#ff55ff'
|
||||
cyan = '#6ae3f9'
|
||||
white = '#ffffff'",
|
||||
),
|
||||
]);
|
||||
let config: AlacrittyConfig = block_on(AlacrittyConfig::from_file(
|
||||
dirs.tests().join("config").join("config.toml"),
|
||||
))
|
||||
.expect("Should be able to read file!")
|
||||
.pop()
|
||||
.expect("Should have returned at least one config!");
|
||||
let ThemeType::Single(theme) = config
|
||||
.colors
|
||||
.expect("Should have read colors!")
|
||||
.parse()
|
||||
.expect("Theme should have read!")
|
||||
else {
|
||||
panic!("Should not have a dark and light theme for Alacritty!")
|
||||
};
|
||||
|
||||
// Check that the three primary colors are the same.
|
||||
assert_eq!(
|
||||
theme.accent().into_solid(),
|
||||
coloru_from_hex_string("#f0cb09").expect("Should be able to parse a color!")
|
||||
);
|
||||
assert_eq!(
|
||||
theme.background().into_solid(),
|
||||
coloru_from_hex_string("#122637").expect("Should be able to parse a color!")
|
||||
);
|
||||
assert_eq!(
|
||||
theme.foreground().into_solid(),
|
||||
coloru_from_hex_string("#ffffff").expect("Should be able to parse a color!")
|
||||
);
|
||||
|
||||
// Check some terminal colors.
|
||||
assert_eq!(
|
||||
theme.terminal_colors().bright.yellow,
|
||||
AnsiColor {
|
||||
r: 0xec,
|
||||
g: 0xc8,
|
||||
b: 0x09
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
theme.terminal_colors().bright.cyan,
|
||||
AnsiColor {
|
||||
r: 0x6a,
|
||||
g: 0xe3,
|
||||
b: 0xf9
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
theme.terminal_colors().normal.yellow,
|
||||
AnsiColor {
|
||||
r: 0xfe,
|
||||
g: 0xe4,
|
||||
b: 0x09
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
theme.terminal_colors().normal.cyan,
|
||||
AnsiColor {
|
||||
r: 0x00,
|
||||
g: 0xbb,
|
||||
b: 0xbb
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use pathfinder_color::ColorU;
|
||||
use serde::Serialize;
|
||||
use strum_macros::EnumIter;
|
||||
use warp_core::ui::{
|
||||
color::hex_color::HexColorError as UiHexColorError,
|
||||
theme::{AnsiColors, WarpTheme},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use thiserror::Error;
|
||||
use warpui::{fonts::FontInfo, keymap::Keystroke, DisplayIdx};
|
||||
|
||||
use crate::{
|
||||
interval_timer::IntervalTimer,
|
||||
root_view::QuakeModePinPosition,
|
||||
settings::ExtraMetaKeys,
|
||||
terminal::session_settings::{StartupShell, WorkingDirectoryConfig},
|
||||
themes::theme_creator::pick_accent_color_from_options,
|
||||
};
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::{themes::theme_creator_body::ThemeCreatorBody, user_config};
|
||||
|
||||
use super::{alacritty_parser::AlacrittyConfig, model::TerminalType};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use super::iterm_parser::ITermProfile;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ThemeType {
|
||||
LightAndDark { light: WarpTheme, dark: WarpTheme },
|
||||
Single(WarpTheme),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ThemeError {
|
||||
/// A hex color is malformatted (not missing).
|
||||
HexColorError(UiHexColorError),
|
||||
/// A value in the theme is missing.
|
||||
MissingValueError,
|
||||
}
|
||||
|
||||
#[derive(Clone, Error, Debug)]
|
||||
pub enum HotkeyError {
|
||||
#[error("A hotkey window opens in a way Warp does not support")]
|
||||
UnsupportedWindowType,
|
||||
#[error("There are multiple hotkeys configured")]
|
||||
MultipleHotkeys,
|
||||
#[error("No hotkey is set")]
|
||||
MissingHotkey,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ConfigError {
|
||||
/// A general IO error when reading the file, excluding
|
||||
/// a NotFound error.
|
||||
FileIOError(std::io::Error),
|
||||
/// A file is missing.
|
||||
FileNotFoundError,
|
||||
/// A file is readable but is formatted incorrectly.
|
||||
MalformattedFileError(PathBuf),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, EnumIter, Serialize)]
|
||||
pub enum SettingType {
|
||||
Theme,
|
||||
OptionAsMeta,
|
||||
MouseAndScrollReporting,
|
||||
Font,
|
||||
DefaultShell,
|
||||
WorkingDirectory,
|
||||
HotkeyMode,
|
||||
WindowSize,
|
||||
CopyOnSelect,
|
||||
Opacity,
|
||||
CursorBlinking,
|
||||
}
|
||||
|
||||
impl SettingType {
|
||||
pub fn get_name(&self) -> &'static str {
|
||||
match self {
|
||||
SettingType::Theme => "Theme",
|
||||
SettingType::OptionAsMeta => "Option as Meta",
|
||||
SettingType::MouseAndScrollReporting => "Mouse/Scroll Reporting",
|
||||
SettingType::Font => "Font",
|
||||
SettingType::DefaultShell => "Default Shell",
|
||||
SettingType::WorkingDirectory => "Working Directory",
|
||||
SettingType::HotkeyMode => "Global hotkey",
|
||||
SettingType::WindowSize => "Window Dimensions",
|
||||
SettingType::CopyOnSelect => "Copy On Select",
|
||||
SettingType::Opacity => "Window Opacity",
|
||||
SettingType::CursorBlinking => "Cursor Blinking",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct MouseAndScrollReporting {
|
||||
pub mouse_reporting: bool,
|
||||
pub scroll_reporting: bool,
|
||||
}
|
||||
|
||||
impl Default for MouseAndScrollReporting {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mouse_reporting: true,
|
||||
scroll_reporting: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ImportedFont {
|
||||
pub family: Option<String>,
|
||||
pub size: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct QuakeModeWindow {
|
||||
pub keystroke: Keystroke,
|
||||
pub autohide: bool,
|
||||
pub screen: Option<DisplayIdx>,
|
||||
pub pin_position: QuakeModePinPosition,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum GlobalHotkey {
|
||||
Activation(Keystroke),
|
||||
QuakeMode(QuakeModeWindow),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct OpacitySettings {
|
||||
pub opacity: Option<u8>,
|
||||
pub blur_radius: Option<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub theme: ImportableSetting<Result<ThemeType, ThemeError>>,
|
||||
pub option_as_meta: ImportableSetting<ExtraMetaKeys>,
|
||||
pub mouse_and_scroll_reporting: ImportableSetting<Option<MouseAndScrollReporting>>,
|
||||
pub font: ImportableSetting<ImportedFont>,
|
||||
pub default_shell: ImportableSetting<Option<StartupShell>>,
|
||||
pub working_directory: ImportableSetting<Option<WorkingDirectoryConfig>>,
|
||||
pub terminal_name: String,
|
||||
pub description: Option<String>,
|
||||
pub hotkey_mode: ImportableSetting<Result<GlobalHotkey, HotkeyError>>,
|
||||
pub opacity: ImportableSetting<OpacitySettings>,
|
||||
pub window_size: ImportableSetting<(Option<u16>, Option<u16>)>,
|
||||
pub copy_on_select: ImportableSetting<Option<bool>>,
|
||||
pub cursor_blinking: ImportableSetting<Option<bool>>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Creates all valid Configs for a given ParseableConfig type.
|
||||
/// Each profile (in terminals that support profiles) is mapped to a separate Config.
|
||||
pub async fn create_from_external_configs<Input: ParseableConfig>(
|
||||
fonts: Arc<Vec<FontInfo>>,
|
||||
) -> (Result<Vec<Self>, ConfigError>, IntervalTimer) {
|
||||
let mut timer = IntervalTimer::new();
|
||||
let config = match Input::from_config_paths(&mut timer).await {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
return (Err(err), timer);
|
||||
}
|
||||
};
|
||||
let configs = config
|
||||
.into_iter()
|
||||
.map(|config| config.remove_default_values())
|
||||
.map(|config| config.parse(&fonts))
|
||||
.filter(|config| config.is_valid())
|
||||
.collect();
|
||||
timer.mark_interval_end("TERMINAL_SETTINGS_PARSED");
|
||||
(Ok(configs), timer)
|
||||
}
|
||||
|
||||
pub(super) fn write_theme(&self) -> Option<ThemeType> {
|
||||
#[cfg(feature = "local_fs")]
|
||||
{
|
||||
if !self.theme.should_import {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Ok(theme) = self.theme.value() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let dir = user_config::themes_dir();
|
||||
|
||||
match theme {
|
||||
ThemeType::LightAndDark { light, dark } => {
|
||||
let light_theme_yaml_file_name =
|
||||
format!("{}_light_theme.yaml", self.terminal_name);
|
||||
let light_written = ThemeCreatorBody::write_theme(
|
||||
light,
|
||||
dir.clone(),
|
||||
light_theme_yaml_file_name,
|
||||
None,
|
||||
|_| light.clone(),
|
||||
);
|
||||
|
||||
let dark_theme_yaml_file_name =
|
||||
format!("{}_dark_theme.yaml", self.terminal_name);
|
||||
let dark_written = ThemeCreatorBody::write_theme(
|
||||
dark,
|
||||
dir,
|
||||
dark_theme_yaml_file_name,
|
||||
None,
|
||||
|_| dark.clone(),
|
||||
);
|
||||
|
||||
if let (Some(light), Some(dark)) = (light_written, dark_written) {
|
||||
Some(ThemeType::LightAndDark { light, dark })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ThemeType::Single(normal) => {
|
||||
let theme_yaml_file_name = format!("{}_theme.yaml", self.terminal_name);
|
||||
ThemeCreatorBody::write_theme(normal, dir, theme_yaml_file_name, None, |_| {
|
||||
ThemeType::Single(normal.clone())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "local_fs"))]
|
||||
{
|
||||
log::warn!("Tried to save theme without a local filesystem.");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a Config from the given TerminalType.
|
||||
pub async fn create_from_terminal_type(
|
||||
terminal: TerminalType,
|
||||
fonts: Arc<Vec<FontInfo>>,
|
||||
) -> (Result<Vec<Self>, ConfigError>, IntervalTimer) {
|
||||
match terminal {
|
||||
TerminalType::Alacritty => {
|
||||
Config::create_from_external_configs::<AlacrittyConfig>(fonts).await
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
TerminalType::ITerm => {
|
||||
Config::create_from_external_configs::<ITermProfile>(fonts).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the list of [`SettingType`]s that have an importable setting associated with it.
|
||||
pub(super) fn valid_setting_types(&self) -> Vec<SettingType> {
|
||||
let mut out = vec![];
|
||||
let default_config = Config::default();
|
||||
if self.theme.value().is_ok() {
|
||||
out.push(self.theme.setting_type().clone());
|
||||
}
|
||||
|
||||
if *self.option_as_meta.value() != *default_config.option_as_meta.value() {
|
||||
out.push(self.option_as_meta.setting_type().clone());
|
||||
}
|
||||
|
||||
if *self.mouse_and_scroll_reporting.value()
|
||||
!= *default_config.mouse_and_scroll_reporting.value()
|
||||
{
|
||||
out.push(self.mouse_and_scroll_reporting.setting_type().clone());
|
||||
}
|
||||
|
||||
if *self.font.value() != *default_config.font.value() {
|
||||
out.push(self.font.setting_type().clone());
|
||||
}
|
||||
|
||||
if *self.default_shell.value() != *default_config.default_shell.value() {
|
||||
out.push(self.default_shell.setting_type().clone());
|
||||
}
|
||||
|
||||
if *self.working_directory.value() != *default_config.working_directory.value() {
|
||||
out.push(self.working_directory.setting_type().clone());
|
||||
}
|
||||
if *self.copy_on_select.value() != *default_config.copy_on_select.value() {
|
||||
out.push(self.copy_on_select.setting_type().clone());
|
||||
}
|
||||
if *self.cursor_blinking.value() != *default_config.cursor_blinking.value() {
|
||||
out.push(self.cursor_blinking.setting_type().clone());
|
||||
}
|
||||
if *self.window_size.value() != *default_config.window_size.value() {
|
||||
out.push(self.window_size.setting_type().clone());
|
||||
}
|
||||
if *self.opacity.value() != *default_config.opacity.value() {
|
||||
out.push(self.opacity.setting_type().clone());
|
||||
}
|
||||
|
||||
if self.hotkey_mode.value().is_ok() {
|
||||
out.push(self.hotkey_mode.setting_type().clone());
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Returns whether or not this Config contains valid importable settings.
|
||||
pub fn is_valid(&self) -> bool {
|
||||
!self.valid_setting_types().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Used for telemetry.
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct ParsedTerminalSetting {
|
||||
pub setting_type: SettingType,
|
||||
pub was_imported_by_user: bool,
|
||||
}
|
||||
|
||||
/// A wrapper for a setting along with its display name
|
||||
/// and whether or not the user has selected to import it.
|
||||
#[derive(Debug)]
|
||||
pub struct ImportableSetting<T> {
|
||||
pub(super) setting: T,
|
||||
setting_type: SettingType,
|
||||
pub should_import: bool,
|
||||
}
|
||||
|
||||
impl<T> ImportableSetting<T> {
|
||||
pub fn new(setting: T, setting_type: SettingType) -> Self {
|
||||
Self {
|
||||
setting,
|
||||
setting_type,
|
||||
should_import: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the a reference to the inner setting.
|
||||
pub fn value(&self) -> &T {
|
||||
&self.setting
|
||||
}
|
||||
|
||||
/// Returns the setting type.
|
||||
pub fn setting_type(&self) -> &SettingType {
|
||||
&self.setting_type
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> ImportableSetting<T> {
|
||||
/// Returns Some(value) if we should import this setting and None otherwise.
|
||||
pub fn importable_value(&self) -> Option<T> {
|
||||
if self.should_import {
|
||||
Some(self.setting.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: ImportableSetting::new(Err(ThemeError::MissingValueError), SettingType::Theme),
|
||||
option_as_meta: ImportableSetting::new(Default::default(), SettingType::OptionAsMeta),
|
||||
mouse_and_scroll_reporting: ImportableSetting::new(
|
||||
None,
|
||||
SettingType::MouseAndScrollReporting,
|
||||
),
|
||||
terminal_name: "".to_string(),
|
||||
font: ImportableSetting::new(
|
||||
ImportedFont {
|
||||
family: None,
|
||||
size: None,
|
||||
},
|
||||
SettingType::Font,
|
||||
),
|
||||
description: None,
|
||||
default_shell: ImportableSetting::new(None, SettingType::DefaultShell),
|
||||
working_directory: ImportableSetting::new(None, SettingType::WorkingDirectory),
|
||||
hotkey_mode: ImportableSetting::new(
|
||||
Err(HotkeyError::MissingHotkey),
|
||||
SettingType::HotkeyMode,
|
||||
),
|
||||
window_size: ImportableSetting::new((None, None), SettingType::WindowSize),
|
||||
opacity: ImportableSetting::new(
|
||||
OpacitySettings {
|
||||
opacity: None,
|
||||
blur_radius: None,
|
||||
},
|
||||
SettingType::Opacity,
|
||||
),
|
||||
copy_on_select: ImportableSetting::new(None, SettingType::CopyOnSelect),
|
||||
cursor_blinking: ImportableSetting::new(None, SettingType::CursorBlinking),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ParseableConfig: PartialEq + Sized + Send {
|
||||
/// Reads the file at the given path into the struct implementing ParseableConfig.
|
||||
async fn from_file(path: PathBuf) -> Result<Vec<Self>, ConfigError>;
|
||||
|
||||
/// Creates a Warp-readable `Config`. Sets corresponding errors if values have
|
||||
/// not been configured from the default.
|
||||
fn parse(self, fonts: &[FontInfo]) -> Config;
|
||||
|
||||
/// Tries to read configuration from the list of default paths.
|
||||
///
|
||||
/// NOTE: Returns an error if
|
||||
/// none of the files are found or any of the files throw some other error.
|
||||
async fn from_config_paths(timer: &mut IntervalTimer) -> Result<Vec<Self>, ConfigError> {
|
||||
for path in Self::default_paths() {
|
||||
match Self::from_file(path).await {
|
||||
Err(ConfigError::FileNotFoundError) => continue,
|
||||
result => {
|
||||
timer.mark_interval_end("TERMINAL_SETTINGS_READ_FROM_FILE");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
timer.mark_interval_end("TERMINAL_SETTINGS_READ_FROM_FILE");
|
||||
Err(ConfigError::FileNotFoundError)
|
||||
}
|
||||
|
||||
/// Returns the list of paths in which to search for configuration files.
|
||||
fn default_paths() -> Vec<PathBuf>;
|
||||
|
||||
/// Strips all fields that have not been configured from the default.
|
||||
fn remove_default_values(self) -> Self;
|
||||
}
|
||||
|
||||
pub fn calculate_accent_color(
|
||||
background: impl Into<ColorU>,
|
||||
foreground: impl Into<ColorU>,
|
||||
cursor_color: impl Into<ColorU>,
|
||||
bright: AnsiColors,
|
||||
) -> ColorU {
|
||||
let cursor_color = cursor_color.into();
|
||||
let foreground = foreground.into();
|
||||
if cursor_color == foreground {
|
||||
pick_accent_color_from_options(
|
||||
&[background.into(), foreground],
|
||||
// Exclude white and black so that we don't choose either.
|
||||
&[
|
||||
bright.red.into(),
|
||||
bright.green.into(),
|
||||
bright.yellow.into(),
|
||||
bright.magenta.into(),
|
||||
bright.cyan.into(),
|
||||
bright.blue.into(),
|
||||
],
|
||||
)
|
||||
} else {
|
||||
cursor_color
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
pub mod alacritty_parser;
|
||||
pub mod config;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod iterm_parser;
|
||||
pub mod model;
|
||||
pub mod view;
|
||||
@@ -0,0 +1,275 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::interval_timer::IntervalTimer;
|
||||
use crate::settings::import::config::{Config, ConfigError};
|
||||
use crate::{send_telemetry_from_ctx, TelemetryEvent};
|
||||
|
||||
use serde::Serialize;
|
||||
use strum::IntoEnumIterator;
|
||||
use strum_macros::{EnumDiscriminants, EnumIter};
|
||||
use warp_core::features::FeatureFlag;
|
||||
use warpui::Entity;
|
||||
use warpui::ModelContext;
|
||||
use warpui::SingletonEntity;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use super::config::HotkeyError;
|
||||
use super::config::SettingType;
|
||||
use super::config::ThemeType;
|
||||
|
||||
#[derive(Clone, Copy, Debug, EnumDiscriminants, Eq, Hash, PartialEq)]
|
||||
#[strum_discriminants(derive(EnumIter, Hash, Serialize))]
|
||||
#[strum_discriminants(name(TerminalType))]
|
||||
pub enum TerminalTypeAndProfile {
|
||||
Alacritty,
|
||||
#[cfg(target_os = "macos")]
|
||||
ITerm(usize),
|
||||
}
|
||||
|
||||
pub struct CompletedParseEvent {
|
||||
pub terminal: TerminalType,
|
||||
}
|
||||
|
||||
pub struct ImportedConfigModel {
|
||||
started: bool,
|
||||
parsed_terminals: HashMap<TerminalType, Result<Vec<Config>, ConfigError>>,
|
||||
}
|
||||
|
||||
impl ImportedConfigModel {
|
||||
pub fn new(_ctx: &mut ModelContext<Self>) -> Self {
|
||||
ImportedConfigModel {
|
||||
parsed_terminals: Default::default(),
|
||||
started: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub fn search_for_settings_to_import(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
use itertools::Itertools;
|
||||
use std::sync::Arc;
|
||||
use strum::IntoEnumIterator;
|
||||
self.started = true;
|
||||
|
||||
let loaded_system_fonts = warpui::fonts::Cache::handle(ctx)
|
||||
.update(ctx, |font_cache, ctx| font_cache.all_system_fonts(ctx));
|
||||
ctx.spawn(loaded_system_fonts, |_, fonts, ctx| {
|
||||
let fonts = fonts
|
||||
.into_iter()
|
||||
.map(|(_family_id, font_info)| font_info)
|
||||
.collect_vec();
|
||||
let fonts_ref = Arc::new(fonts);
|
||||
for terminal_type in TerminalType::iter() {
|
||||
if terminal_type == TerminalType::Alacritty
|
||||
&& !FeatureFlag::AlacrittySettingsImport.is_enabled()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let fonts_ref_clone = Arc::clone(&fonts_ref);
|
||||
ctx.spawn(
|
||||
async move {
|
||||
Config::create_from_terminal_type(terminal_type, fonts_ref_clone).await
|
||||
},
|
||||
move |model, output, ctx| {
|
||||
model.write_parse_results(terminal_type, output, ctx);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn is_started(&self) -> bool {
|
||||
self.started
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn maybe_send_multiple_hotkeys_telemetry_event(
|
||||
&self,
|
||||
terminal_type: &TerminalType,
|
||||
configs: &Result<Vec<Config>, ConfigError>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let TerminalType::ITerm = terminal_type {
|
||||
if let Ok(configs) = configs {
|
||||
if configs.iter().any(|config| {
|
||||
matches!(
|
||||
config.hotkey_mode.setting,
|
||||
Err(HotkeyError::MultipleHotkeys)
|
||||
)
|
||||
}) {
|
||||
send_telemetry_from_ctx!(TelemetryEvent::ITermMultipleHotkeys, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_parse_results(
|
||||
&mut self,
|
||||
terminal_type: TerminalType,
|
||||
(configs, timer): (Result<Vec<Config>, ConfigError>, IntervalTimer),
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
send_telemetry_from_ctx!(
|
||||
TelemetryEvent::SettingsImportConfigParsed {
|
||||
timing_data: timer.compute_stats(),
|
||||
terminal_type,
|
||||
settings_shown_to_user: configs
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|configs| configs.first())
|
||||
.map(|config| config.valid_setting_types())
|
||||
},
|
||||
ctx
|
||||
);
|
||||
#[cfg(target_os = "macos")]
|
||||
self.maybe_send_multiple_hotkeys_telemetry_event(&terminal_type, &configs, ctx);
|
||||
self.parsed_terminals.insert(terminal_type, configs);
|
||||
ctx.emit(CompletedParseEvent {
|
||||
terminal: terminal_type,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn configs(&self) -> impl Iterator<Item = (TerminalTypeAndProfile, &Config)> {
|
||||
self.parsed_terminals
|
||||
.iter()
|
||||
.filter_map(|(terminal, parse_result)| {
|
||||
parse_result.as_ref().ok().map(|value| (terminal, value))
|
||||
})
|
||||
.flat_map(|(discriminant, vec)| match discriminant {
|
||||
TerminalType::Alacritty => vec
|
||||
.iter()
|
||||
.take(1)
|
||||
.map(|item| (TerminalTypeAndProfile::Alacritty, item))
|
||||
.collect::<Vec<(TerminalTypeAndProfile, &Config)>>()
|
||||
.into_iter(),
|
||||
#[cfg(target_os = "macos")]
|
||||
TerminalType::ITerm => vec
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, item)| (TerminalTypeAndProfile::ITerm(idx), item))
|
||||
.collect::<Vec<(TerminalTypeAndProfile, &Config)>>()
|
||||
.into_iter(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn config(&self, profile: &TerminalTypeAndProfile) -> Option<&Config> {
|
||||
self.parsed_terminals
|
||||
.get(&TerminalType::from(profile))
|
||||
.map(|vec| match profile {
|
||||
TerminalTypeAndProfile::Alacritty => vec.as_ref().ok().and_then(|vec| vec.first()),
|
||||
#[cfg(target_os = "macos")]
|
||||
TerminalTypeAndProfile::ITerm(idx) => {
|
||||
vec.as_ref().ok().and_then(|vec| vec.get(*idx))
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("Attempted to access an invalid profile.");
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
fn config_mut(&mut self, profile: &TerminalTypeAndProfile) -> Option<&mut Config> {
|
||||
self.parsed_terminals
|
||||
.get_mut(&TerminalType::from(profile))
|
||||
.map(|vec| match profile {
|
||||
TerminalTypeAndProfile::Alacritty => {
|
||||
vec.as_mut().ok().and_then(|vec| vec.first_mut())
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
TerminalTypeAndProfile::ITerm(idx) => {
|
||||
vec.as_mut().ok().and_then(|vec| vec.get_mut(*idx))
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("Attempted to access an invalid profile.");
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
pub fn toggle_should_import(
|
||||
&mut self,
|
||||
profile: &TerminalTypeAndProfile,
|
||||
setting: &SettingType,
|
||||
) {
|
||||
let Some(config) = self.config_mut(profile) else {
|
||||
log::warn!("Attempted to toggle import on an invalid profile!");
|
||||
return;
|
||||
};
|
||||
match setting {
|
||||
SettingType::Theme => config.theme.should_import = !config.theme.should_import,
|
||||
SettingType::OptionAsMeta => {
|
||||
config.option_as_meta.should_import = !config.option_as_meta.should_import
|
||||
}
|
||||
SettingType::MouseAndScrollReporting => {
|
||||
config.mouse_and_scroll_reporting.should_import =
|
||||
!config.mouse_and_scroll_reporting.should_import
|
||||
}
|
||||
SettingType::Font => config.font.should_import = !config.font.should_import,
|
||||
SettingType::DefaultShell => {
|
||||
config.default_shell.should_import = !config.default_shell.should_import
|
||||
}
|
||||
SettingType::WorkingDirectory => {
|
||||
config.working_directory.should_import = !config.working_directory.should_import
|
||||
}
|
||||
SettingType::HotkeyMode => {
|
||||
config.hotkey_mode.should_import = !config.hotkey_mode.should_import
|
||||
}
|
||||
SettingType::Opacity => config.opacity.should_import = !config.opacity.should_import,
|
||||
SettingType::WindowSize => {
|
||||
config.window_size.should_import = !config.window_size.should_import
|
||||
}
|
||||
SettingType::CopyOnSelect => {
|
||||
config.copy_on_select.should_import = !config.copy_on_select.should_import
|
||||
}
|
||||
SettingType::CursorBlinking => {
|
||||
config.cursor_blinking.should_import = !config.cursor_blinking.should_import
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_import(&self, profile: &TerminalTypeAndProfile, setting: &SettingType) -> bool {
|
||||
let Some(config) = self.config(profile) else {
|
||||
log::warn!("Attempted to read should_import on an invalid profile!");
|
||||
return false;
|
||||
};
|
||||
match setting {
|
||||
SettingType::Theme => config.theme.should_import,
|
||||
SettingType::OptionAsMeta => config.option_as_meta.should_import,
|
||||
SettingType::MouseAndScrollReporting => config.mouse_and_scroll_reporting.should_import,
|
||||
SettingType::Font => config.font.should_import,
|
||||
SettingType::DefaultShell => config.default_shell.should_import,
|
||||
SettingType::WorkingDirectory => config.working_directory.should_import,
|
||||
SettingType::HotkeyMode => config.hotkey_mode.should_import,
|
||||
SettingType::Opacity => config.opacity.should_import,
|
||||
SettingType::WindowSize => config.window_size.should_import,
|
||||
SettingType::CopyOnSelect => config.copy_on_select.should_import,
|
||||
SettingType::CursorBlinking => config.cursor_blinking.should_import,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_theme(&self, profile: &TerminalTypeAndProfile) -> Option<ThemeType> {
|
||||
self.config(profile)
|
||||
.map(|config| config.write_theme())
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("Attempted to write the theme from an invalid profile.");
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
pub fn finished_searching_for_settings(&self) -> bool {
|
||||
TerminalType::iter()
|
||||
.filter(|terminal_type| {
|
||||
if !FeatureFlag::AlacrittySettingsImport.is_enabled() {
|
||||
*terminal_type != TerminalType::Alacritty
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.all(|terminal| self.parsed_terminals.contains_key(&terminal))
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ImportedConfigModel {
|
||||
type Event = CompletedParseEvent;
|
||||
}
|
||||
|
||||
impl SingletonEntity for ImportedConfigModel {}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user