Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+466
View File
@@ -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;