568 lines
18 KiB
Rust
568 lines
18 KiB
Rust
use super::default_themes::*;
|
|
use anyhow::Result;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::iter::FromIterator;
|
|
use std::path::PathBuf;
|
|
use warp_core::ui::color::pick_foreground_color;
|
|
use warpui::assets::asset_cache::AssetSource;
|
|
use warpui::{
|
|
color::ColorU,
|
|
elements::{
|
|
Align, Border, ConstrainedBox, Container, Element, Empty, Flex, ParentElement, Rect,
|
|
Shrinkable, Stack, Text,
|
|
},
|
|
fonts::FamilyId,
|
|
};
|
|
|
|
use super::theme_creator::{pick_accent_color_from_options, top_colors_for_image};
|
|
|
|
pub use warp_core::ui::color::blend::Blend;
|
|
pub use warp_core::ui::theme::*;
|
|
|
|
const THUMBNAIL_MARGIN: f32 = 10.;
|
|
|
|
// We use the discriminant of enum variants to determine the order of theme types in the
|
|
// theme chooser view.
|
|
#[derive(
|
|
Clone,
|
|
Debug,
|
|
Default,
|
|
PartialEq,
|
|
Serialize,
|
|
Deserialize,
|
|
Hash,
|
|
Eq,
|
|
Ord,
|
|
PartialOrd,
|
|
schemars::JsonSchema,
|
|
settings_value::SettingsValue,
|
|
)]
|
|
#[schemars(description = "The color theme.", rename_all = "snake_case")]
|
|
pub enum ThemeKind {
|
|
// Need an alias for backwards-compatibility: Originally we only had a single reward theme
|
|
// so it was named `ReferralReward`.
|
|
#[serde(alias = "ReferralReward")]
|
|
#[schemars(skip)]
|
|
SentReferralReward,
|
|
#[schemars(skip)]
|
|
ReceivedReferralReward,
|
|
#[schemars(description = "Adeberry")]
|
|
Adeberry,
|
|
#[schemars(description = "Phenomenon")]
|
|
Phenomenon,
|
|
#[default]
|
|
#[schemars(description = "Dark")]
|
|
Dark,
|
|
#[schemars(description = "Dracula")]
|
|
Dracula,
|
|
#[schemars(description = "Fancy Dracula")]
|
|
FancyDracula,
|
|
#[schemars(description = "Cyber Wave")]
|
|
CyberWave,
|
|
#[schemars(description = "Solar Flare")]
|
|
SolarFlare,
|
|
#[schemars(description = "Solarized Dark")]
|
|
SolarizedDark,
|
|
#[schemars(description = "Willow Dream")]
|
|
WillowDream,
|
|
#[schemars(description = "Light")]
|
|
Light,
|
|
#[schemars(description = "Dark City")]
|
|
DarkCity,
|
|
#[schemars(description = "Gruvbox Dark")]
|
|
GruvboxDark,
|
|
#[schemars(description = "Red Rock")]
|
|
RedRock,
|
|
#[schemars(description = "Jellyfish")]
|
|
JellyFish,
|
|
#[schemars(description = "Leafy")]
|
|
Leafy,
|
|
#[schemars(description = "Koi")]
|
|
Koi,
|
|
#[schemars(description = "Solarized Light")]
|
|
SolarizedLight,
|
|
#[schemars(description = "Snowy")]
|
|
Snowy,
|
|
#[schemars(description = "Gruvbox Light")]
|
|
GruvboxLight,
|
|
#[schemars(description = "Pink City")]
|
|
PinkCity,
|
|
#[schemars(description = "Marble")]
|
|
Marble,
|
|
#[schemars(description = "A user-provided custom theme loaded from a file.")]
|
|
Custom(CustomTheme),
|
|
/// Base16 themes are a special case of custom themes with their own semantics for ANSI colors that override "bright" color variants.
|
|
#[schemars(description = "A custom theme using the Base16 color scheme format.")]
|
|
CustomBase16(CustomTheme),
|
|
#[schemars(skip)]
|
|
InMemory(InMemoryThemeOptions),
|
|
}
|
|
|
|
impl From<CustomTheme> for ThemeKind {
|
|
fn from(custom_theme: CustomTheme) -> ThemeKind {
|
|
if custom_theme.name.as_str().starts_with("Base16") {
|
|
ThemeKind::CustomBase16(custom_theme)
|
|
} else {
|
|
ThemeKind::Custom(custom_theme)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for ThemeKind {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let value = match &self {
|
|
ThemeKind::Light => "Light",
|
|
ThemeKind::Dark => "Dark",
|
|
ThemeKind::Dracula => "Dracula",
|
|
ThemeKind::SolarizedDark => "Solarized Dark",
|
|
ThemeKind::SolarizedLight => "Solarized Light",
|
|
ThemeKind::GruvboxDark => "Gruvbox Dark",
|
|
ThemeKind::GruvboxLight => "Gruvbox Light",
|
|
ThemeKind::JellyFish => "Jellyfish",
|
|
ThemeKind::Koi => "Koi",
|
|
ThemeKind::Leafy => "Leafy",
|
|
ThemeKind::Marble => "Marble",
|
|
ThemeKind::PinkCity => "Pink City",
|
|
ThemeKind::Snowy => "Snowy",
|
|
ThemeKind::DarkCity => "Dark City",
|
|
ThemeKind::RedRock => "Red Rock",
|
|
ThemeKind::CyberWave => "Cyber Wave",
|
|
ThemeKind::WillowDream => "Willow Dream",
|
|
ThemeKind::FancyDracula => "Fancy Dracula",
|
|
ThemeKind::Phenomenon => "Phenomenon",
|
|
ThemeKind::SolarFlare => "Solar Flare",
|
|
ThemeKind::Adeberry => "Adeberry",
|
|
ThemeKind::SentReferralReward => "Warp Referral",
|
|
ThemeKind::ReceivedReferralReward => "Referred to Warp",
|
|
ThemeKind::Custom(custom_theme) => custom_theme.name.as_str(),
|
|
ThemeKind::CustomBase16(custom_theme) => custom_theme.name.as_str(),
|
|
ThemeKind::InMemory(in_memory_theme) => in_memory_theme.name.as_str(),
|
|
};
|
|
write!(f, "{value}")
|
|
}
|
|
}
|
|
|
|
impl ThemeKind {
|
|
pub fn matches(&self, query: &str) -> bool {
|
|
let theme_name = format!("{self}").to_lowercase();
|
|
theme_name.contains(&query.to_lowercase())
|
|
}
|
|
}
|
|
|
|
#[derive(
|
|
Debug,
|
|
Clone,
|
|
Hash,
|
|
PartialEq,
|
|
Eq,
|
|
Serialize,
|
|
Deserialize,
|
|
PartialOrd,
|
|
Ord,
|
|
schemars::JsonSchema,
|
|
settings_value::SettingsValue,
|
|
)]
|
|
#[schemars(description = "A user-provided custom theme.")]
|
|
pub struct CustomTheme {
|
|
#[schemars(description = "The display name of the custom theme.")]
|
|
name: String,
|
|
#[schemars(description = "The file path to the custom theme definition.")]
|
|
path: PathBuf,
|
|
}
|
|
|
|
impl CustomTheme {
|
|
pub fn new(s: String, p: PathBuf) -> Self {
|
|
CustomTheme { name: s, path: p }
|
|
}
|
|
|
|
pub fn name(&self) -> String {
|
|
self.name.clone()
|
|
}
|
|
|
|
pub fn path(&self) -> PathBuf {
|
|
self.path.clone()
|
|
}
|
|
}
|
|
|
|
#[derive(
|
|
Debug,
|
|
Clone,
|
|
Hash,
|
|
PartialEq,
|
|
Eq,
|
|
Serialize,
|
|
Deserialize,
|
|
PartialOrd,
|
|
Ord,
|
|
settings_value::SettingsValue,
|
|
)]
|
|
pub struct InMemoryThemeOptions {
|
|
name: String,
|
|
path: PathBuf,
|
|
#[serde(skip)]
|
|
possible_bg_colors: Vec<ColorU>,
|
|
#[serde(skip)]
|
|
chosen_bg_color_index: usize,
|
|
}
|
|
|
|
impl InMemoryThemeOptions {
|
|
pub async fn new(name: String, path: PathBuf) -> Result<Self> {
|
|
top_colors_for_image(path.clone()).map(|top_colors| InMemoryThemeOptions {
|
|
name,
|
|
path,
|
|
possible_bg_colors: top_colors,
|
|
chosen_bg_color_index: 0,
|
|
})
|
|
}
|
|
|
|
pub fn chosen_bg_color(&self) -> ColorU {
|
|
self.possible_bg_colors[self.chosen_bg_color_index]
|
|
}
|
|
|
|
pub fn possible_bg_colors(&self) -> Vec<ColorU> {
|
|
self.possible_bg_colors.clone()
|
|
}
|
|
|
|
pub fn chosen_bg_color_index(&self) -> usize {
|
|
self.chosen_bg_color_index
|
|
}
|
|
|
|
pub fn set_chosen_bg_color_index(&mut self, index: usize) {
|
|
self.chosen_bg_color_index = index;
|
|
}
|
|
|
|
pub fn name(&self) -> String {
|
|
self.name.clone()
|
|
}
|
|
|
|
pub fn set_name(&mut self, name: String) {
|
|
self.name = name;
|
|
}
|
|
|
|
pub fn path(&self) -> PathBuf {
|
|
self.path.clone()
|
|
}
|
|
|
|
pub fn set_path(&mut self, path: PathBuf) {
|
|
self.path = path;
|
|
}
|
|
|
|
pub fn theme(&self) -> WarpTheme {
|
|
let bg_color = self.chosen_bg_color();
|
|
let fg_color = pick_foreground_color(bg_color);
|
|
let possible_accent_colors: Vec<ColorU> = self
|
|
.possible_bg_colors
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(index, _)| *index != self.chosen_bg_color_index)
|
|
.map(|(_, color)| *color)
|
|
.collect();
|
|
let accent_color =
|
|
pick_accent_color_from_options(&[bg_color, fg_color], &possible_accent_colors[..]);
|
|
|
|
let (details, terminal_colors) = if fg_color.eq(&ColorU::white()) {
|
|
(Details::Darker, dark_mode_colors())
|
|
} else {
|
|
(Details::Lighter, light_mode_colors())
|
|
};
|
|
|
|
WarpTheme::new(
|
|
bg_color.into(),
|
|
fg_color,
|
|
accent_color.into(),
|
|
None,
|
|
Some(details),
|
|
terminal_colors,
|
|
Some(Image {
|
|
// Note that, as an invariant, in-memory themes come from local files.
|
|
source: AssetSource::LocalFile {
|
|
path: self.path().to_str().unwrap_or_default().to_owned(),
|
|
},
|
|
opacity: 30,
|
|
}),
|
|
Some(self.name()),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct WarpThemeConfig {
|
|
theme_map: HashMap<ThemeKind, WarpTheme>,
|
|
}
|
|
|
|
impl WarpThemeConfig {
|
|
pub fn new() -> Self {
|
|
// preload with built-in themes
|
|
let theme_map: HashMap<ThemeKind, WarpTheme> = HashMap::from_iter([
|
|
(ThemeKind::SentReferralReward, sent_referral_reward()),
|
|
(
|
|
ThemeKind::ReceivedReferralReward,
|
|
received_referral_reward(),
|
|
),
|
|
(ThemeKind::Dark, dark_theme()),
|
|
(ThemeKind::Light, light_theme()),
|
|
(ThemeKind::SolarizedDark, solarized_dark()),
|
|
(ThemeKind::SolarizedLight, solarized_light()),
|
|
(ThemeKind::Dracula, dracula()),
|
|
(ThemeKind::GruvboxDark, gruvbox_dark()),
|
|
(ThemeKind::GruvboxLight, gruvbox_light()),
|
|
(ThemeKind::JellyFish, jellyfish()),
|
|
(ThemeKind::Koi, koi()),
|
|
(ThemeKind::Leafy, leafy()),
|
|
(ThemeKind::Marble, marble()),
|
|
(ThemeKind::PinkCity, pink_city()),
|
|
(ThemeKind::Snowy, snowy()),
|
|
(ThemeKind::DarkCity, dark_city()),
|
|
(ThemeKind::RedRock, red_rock()),
|
|
(ThemeKind::CyberWave, cyber_wave()),
|
|
(ThemeKind::WillowDream, willow_dream()),
|
|
(ThemeKind::FancyDracula, fancy_dracula()),
|
|
(ThemeKind::Phenomenon, phenomenon()),
|
|
(ThemeKind::SolarFlare, solar_flare()),
|
|
(ThemeKind::Adeberry, adeberry()),
|
|
]);
|
|
WarpThemeConfig { theme_map }
|
|
}
|
|
|
|
pub fn add_new_theme(&mut self, theme_name: ThemeKind, theme: WarpTheme) {
|
|
self.theme_map.insert(theme_name, theme);
|
|
}
|
|
|
|
pub fn file_to_theme(name: String, path: PathBuf) -> ThemeKind {
|
|
CustomTheme::new(name, path).into()
|
|
}
|
|
|
|
pub fn theme_items(&self) -> impl Iterator<Item = (&ThemeKind, &WarpTheme)> {
|
|
self.theme_map.iter()
|
|
}
|
|
|
|
pub fn theme(&self, name: &ThemeKind) -> WarpTheme {
|
|
self.theme_map.get(name).cloned().unwrap_or_else(dark_theme)
|
|
}
|
|
}
|
|
|
|
impl Default for WarpThemeConfig {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub enum RespectSystemTheme {
|
|
#[default]
|
|
Off,
|
|
On(SelectedSystemThemes),
|
|
}
|
|
|
|
#[derive(
|
|
Clone,
|
|
Debug,
|
|
PartialEq,
|
|
Eq,
|
|
Serialize,
|
|
Deserialize,
|
|
schemars::JsonSchema,
|
|
settings_value::SettingsValue,
|
|
)]
|
|
#[schemars(description = "Themes to use when following the system light/dark mode.")]
|
|
pub struct SelectedSystemThemes {
|
|
#[schemars(description = "The theme to use in light mode.")]
|
|
pub light: ThemeKind,
|
|
#[schemars(description = "The theme to use in dark mode.")]
|
|
pub dark: ThemeKind,
|
|
}
|
|
|
|
impl RespectSystemTheme {
|
|
pub fn selected_system_themes(&self) -> Option<&SelectedSystemThemes> {
|
|
match self {
|
|
RespectSystemTheme::Off => None,
|
|
RespectSystemTheme::On(selected) => Some(selected),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for SelectedSystemThemes {
|
|
fn default() -> Self {
|
|
Self {
|
|
light: ThemeKind::Light,
|
|
dark: ThemeKind::Dark,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub struct PromptColors {
|
|
pub input_prompt_conversation_management: ColorU,
|
|
pub input_prompt_pwd: ColorU,
|
|
pub input_prompt_git: ColorU,
|
|
pub input_prompt_branch: ColorU,
|
|
pub input_prompt_agent_mode_hint: ColorU,
|
|
pub input_prompt_agent_mode_tasks: ColorU,
|
|
pub input_prompt_dirty_color: ColorU,
|
|
pub input_prompt_virtual_env: ColorU,
|
|
pub input_prompt_user_and_host: ColorU,
|
|
pub input_prompt_date: ColorU,
|
|
pub input_prompt_time: ColorU,
|
|
pub input_prompt_kubernetes: ColorU,
|
|
pub input_prompt_svn: ColorU,
|
|
pub input_prompt_separator: ColorU,
|
|
pub input_prompt_subshell: ColorU,
|
|
pub input_prompt_ssh: ColorU,
|
|
}
|
|
|
|
impl From<WarpTheme> for PromptColors {
|
|
fn from(theme: WarpTheme) -> Self {
|
|
PromptColors {
|
|
input_prompt_conversation_management: theme.terminal_colors().normal.white.into(),
|
|
input_prompt_pwd: theme.terminal_colors().normal.magenta.into(),
|
|
input_prompt_git: theme.terminal_colors().normal.green.into(),
|
|
input_prompt_agent_mode_hint: theme.terminal_colors().normal.yellow.into(),
|
|
input_prompt_agent_mode_tasks: theme.terminal_colors().normal.yellow.into(),
|
|
input_prompt_branch: theme.terminal_colors().normal.yellow.into(),
|
|
input_prompt_dirty_color: theme.terminal_colors().normal.green.into(),
|
|
input_prompt_virtual_env: theme.terminal_colors().normal.yellow.into(),
|
|
input_prompt_user_and_host: theme.terminal_colors().normal.green.into(),
|
|
input_prompt_date: theme.terminal_colors().normal.cyan.into(),
|
|
input_prompt_time: theme.terminal_colors().normal.red.into(),
|
|
input_prompt_kubernetes: theme.terminal_colors().normal.cyan.into(),
|
|
input_prompt_ssh: theme.terminal_colors().normal.blue.into(),
|
|
input_prompt_subshell: theme.terminal_colors().normal.blue.into(),
|
|
input_prompt_svn: theme.terminal_colors().normal.blue.into(),
|
|
input_prompt_separator: theme.terminal_colors().normal.magenta.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn render_preview(
|
|
theme: &WarpTheme,
|
|
font_family: FamilyId,
|
|
form_factor: Option<f32>,
|
|
) -> Box<dyn Element> {
|
|
let text_size = 8. * form_factor.unwrap_or(1.);
|
|
let margin = THUMBNAIL_MARGIN * form_factor.unwrap_or(1.);
|
|
let padding = 5. * form_factor.unwrap_or(1.);
|
|
let text_line_1 = Container::new(
|
|
Text::new_inline("ls", font_family, text_size)
|
|
.with_color(theme.foreground().into_solid())
|
|
.finish(),
|
|
)
|
|
.with_margin_left(margin)
|
|
.with_margin_right(margin)
|
|
.finish();
|
|
|
|
let text_line_2 = Container::new(
|
|
Flex::row()
|
|
.with_child(
|
|
Text::new_inline("dir ", font_family, text_size)
|
|
.with_color(theme.terminal_colors().normal.blue.into())
|
|
.finish(),
|
|
)
|
|
.with_child(
|
|
Text::new_inline("executable ", font_family, text_size)
|
|
.with_color(theme.terminal_colors().normal.red.into())
|
|
.finish(),
|
|
)
|
|
.with_child(
|
|
Text::new_inline("file", font_family, text_size)
|
|
.with_color(theme.foreground().into_solid())
|
|
.finish(),
|
|
)
|
|
.finish(),
|
|
)
|
|
.with_margin_left(margin)
|
|
.with_margin_right(margin)
|
|
.finish();
|
|
|
|
let input_box = Shrinkable::new(
|
|
1.,
|
|
Align::new(
|
|
Flex::column()
|
|
// The border above the input box.
|
|
.with_child(
|
|
Container::new(Empty::new().finish())
|
|
.with_padding_bottom(padding)
|
|
.with_border(
|
|
Border::top(1.2 * form_factor.unwrap_or(1.))
|
|
.with_border_color(theme.outline().into_solid()),
|
|
)
|
|
.finish(),
|
|
)
|
|
// The fake cursor within the input box.
|
|
.with_child(
|
|
Container::new(
|
|
ConstrainedBox::new(
|
|
Rect::new()
|
|
.with_background_color(theme.accent().into_solid())
|
|
.finish(),
|
|
)
|
|
.with_height(12. * form_factor.unwrap_or(1.))
|
|
.with_width(2. * form_factor.unwrap_or(1.))
|
|
.finish(),
|
|
)
|
|
.with_margin_left(margin)
|
|
.with_margin_right(margin)
|
|
.finish(),
|
|
)
|
|
.finish(),
|
|
)
|
|
.bottom_left()
|
|
.finish(),
|
|
)
|
|
.finish();
|
|
|
|
let mut thumbnail = Stack::new();
|
|
let mut background_opacity = 100;
|
|
if let Some(background_image) = theme.background_image() {
|
|
thumbnail.add_child(
|
|
Shrinkable::new(
|
|
1.,
|
|
warpui::elements::Image::new(
|
|
background_image.source(),
|
|
warpui::elements::CacheOption::BySize,
|
|
)
|
|
.cover()
|
|
.finish(),
|
|
)
|
|
.finish(),
|
|
);
|
|
background_opacity -= background_image.opacity;
|
|
}
|
|
|
|
thumbnail.add_child(
|
|
Container::new(
|
|
Container::new(
|
|
Flex::column()
|
|
.with_child(text_line_1)
|
|
.with_child(
|
|
Container::new(text_line_2)
|
|
.with_padding_top(padding)
|
|
.finish(),
|
|
)
|
|
.with_child(input_box)
|
|
.finish(),
|
|
)
|
|
.with_margin_top(margin)
|
|
.with_margin_bottom(margin)
|
|
.finish(),
|
|
)
|
|
.with_background(theme.background().with_opacity(background_opacity))
|
|
.finish(),
|
|
);
|
|
|
|
Align::new(
|
|
Container::new(
|
|
ConstrainedBox::new(thumbnail.finish())
|
|
.with_height(100. * form_factor.unwrap_or(1.))
|
|
.with_width(190. * form_factor.unwrap_or(1.))
|
|
.finish(),
|
|
)
|
|
.finish(),
|
|
)
|
|
.finish()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "theme_test.rs"]
|
|
mod tests;
|