first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+7 -7
View File
@@ -1,12 +1,12 @@
use asset_macro::bundled_or_fetched_asset;
use galaxy_core::ui::{
color::{blend::Blend, coloru_with_opacity, OPAQUE},
theme::{
color::CustomDetails, AnsiColor, AnsiColors, Details, Fill, GalaxyTheme,
HorizontalGradient, Image, TerminalColors, VerticalGradient,
},
};
use pathfinder_color::ColorU;
use galaxy_core::ui::color::blend::Blend;
use galaxy_core::ui::color::{coloru_with_opacity, OPAQUE};
use galaxy_core::ui::theme::color::CustomDetails;
use galaxy_core::ui::theme::{
AnsiColor, AnsiColors, Details, Fill, HorizontalGradient, Image, TerminalColors,
VerticalGradient, WarpTheme,
};
const DARK_MODE_NORMAL_COLORS: AnsiColors = AnsiColors::new(
AnsiColor::from_u32(0x616161FF),
+199 -28
View File
@@ -1,24 +1,22 @@
use super::default_themes::*;
use anyhow::Result;
use galaxy_core::ui::color::pick_foreground_color;
use galaxyui::assets::asset_cache::AssetSource;
use galaxyui::{
color::ColorU,
elements::{
Align, Border, ConstrainedBox, Container, Element, Empty, Flex, ParentElement, Rect,
Shrinkable, Stack, Text,
},
fonts::FamilyId,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::iter::FromIterator;
use std::path::PathBuf;
use super::theme_creator::{pick_accent_color_from_options, top_colors_for_image};
use std::path::{Component, Path, PathBuf};
use anyhow::Result;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub use galaxy_core::ui::color::blend::Blend;
use galaxy_core::ui::color::pick_foreground_color;
pub use galaxy_core::ui::theme::*;
use galaxyui::assets::asset_cache::AssetSource;
use galaxyui::color::ColorU;
use galaxyui::elements::{
Align, Border, ConstrainedBox, Container, Element, Empty, Flex, ParentElement, Rect,
Shrinkable, Stack, Text,
};
use galaxyui::fonts::FamilyId;
use super::default_themes::*;
use super::theme_creator::{pick_accent_color_from_options, top_colors_for_image};
const THUMBNAIL_MARGIN: f32 = 10.;
@@ -154,29 +152,201 @@ impl ThemeKind {
let theme_name = format!("{self}").to_lowercase();
theme_name.contains(&query.to_lowercase())
}
pub(crate) fn is_custom_theme_reference_syncable(&self) -> bool {
match self {
ThemeKind::Custom(custom_theme) | ThemeKind::CustomBase16(custom_theme) => {
custom_theme_path_is_portable(&custom_theme.path, &crate::user_config::themes_dir())
}
_ => true,
}
}
}
#[derive(
Debug,
Clone,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
PartialOrd,
Ord,
schemars::JsonSchema,
settings_value::SettingsValue,
Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, schemars::JsonSchema,
)]
#[schemars(description = "A user-provided custom theme.")]
pub struct CustomTheme {
#[schemars(description = "The display name of the custom theme.")]
name: String,
#[serde(
deserialize_with = "deserialize_custom_theme_path",
serialize_with = "serialize_custom_theme_path"
)]
#[schemars(description = "The file path to the custom theme definition.")]
path: PathBuf,
}
impl settings_value::SettingsValue for CustomTheme {
fn to_file_value(&self) -> serde_json::Value {
serde_json::json!({
"name": &self.name,
"path": custom_theme_path_storage_value(&self.path, &crate::user_config::themes_dir()),
})
}
fn from_file_value(value: &serde_json::Value) -> Option<Self> {
#[derive(Deserialize)]
struct FileValue {
name: String,
path: String,
}
let value = serde_json::from_value::<FileValue>(value.clone()).ok()?;
Some(Self {
name: value.name,
path: portable_custom_theme_path_from_stored_raw(
&value.path,
&crate::user_config::themes_dir(),
),
})
}
}
fn serialize_custom_theme_path<S>(path: &Path, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(path) =
portable_custom_theme_storage_string(path, &crate::user_config::themes_dir())
{
path.serialize(serializer)
} else {
path.serialize(serializer)
}
}
fn deserialize_custom_theme_path<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
where
D: Deserializer<'de>,
{
let path = String::deserialize(deserializer)?;
Ok(portable_custom_theme_path_from_stored_raw(
&path,
&crate::user_config::themes_dir(),
))
}
fn custom_theme_path_storage_value(path: &Path, theme_root: &Path) -> serde_json::Value {
if let Some(path) = portable_custom_theme_storage_string(path, theme_root) {
serde_json::Value::String(path)
} else {
serde_json::json!(path)
}
}
pub(crate) fn custom_theme_path_is_portable(path: &Path, theme_root: &Path) -> bool {
if path_is_absolute_or_foreign_absolute(path) {
return portable_custom_theme_storage_string(path, theme_root).is_some();
}
path.to_str()
.is_some_and(|path| portable_stored_raw_components(path).is_some())
}
pub(crate) fn portable_custom_theme_path_from_stored_raw(raw: &str, theme_root: &Path) -> PathBuf {
portable_stored_raw_components(raw)
.map(|components| {
components
.iter()
.fold(theme_root.to_path_buf(), |path, component| {
path.join(component)
})
})
.unwrap_or_else(|| PathBuf::from(raw))
}
pub(crate) fn portable_custom_theme_storage_string(
path: &Path,
theme_root: &Path,
) -> Option<String> {
if path_starts_with_windows_drive_prefix_using_forward_slash(path) {
return None;
}
let relative = path.strip_prefix(theme_root).ok()?;
let mut components = Vec::new();
for component in relative.components() {
let Component::Normal(value) = component else {
return None;
};
let value = value.to_str()?;
if value.contains('\\') {
return None;
}
components.push(value);
}
if components.is_empty() {
return None;
}
let path = components.join("/");
if portable_stored_raw_components(&path).is_some() {
Some(path)
} else {
None
}
}
fn portable_stored_raw_components(raw: &str) -> Option<Vec<&str>> {
if raw.is_empty()
|| raw.contains('\\')
|| raw.starts_with('/')
|| raw_starts_with_windows_drive_prefix(raw)
{
return None;
}
let components = raw.split('/').collect::<Vec<_>>();
if components
.iter()
.all(|component| !component.is_empty() && *component != "." && *component != "..")
{
Some(components)
} else {
None
}
}
fn raw_starts_with_windows_drive_prefix(raw: &str) -> bool {
let bytes = raw.as_bytes();
bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':'
}
fn path_starts_with_windows_drive_prefix_using_forward_slash(path: &Path) -> bool {
let Some(path) = path.as_os_str().to_str() else {
return false;
};
let bytes = path.as_bytes();
bytes.len() >= 3 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' && bytes[2] == b'/'
}
fn path_is_absolute_or_foreign_absolute(path: &Path) -> bool {
path.has_root() || path_looks_like_foreign_windows_absolute(path)
}
fn path_looks_like_foreign_windows_absolute(path: &Path) -> bool {
if path.has_root() {
return false;
}
let Some(path) = path.as_os_str().to_str() else {
return false;
};
let bytes = path.as_bytes();
let starts_with_drive_root = bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& matches!(bytes[2], b'\\' | b'/');
starts_with_drive_root || path.starts_with(r"\\")
}
impl CustomTheme {
pub fn new(s: String, p: PathBuf) -> Self {
CustomTheme { name: s, path: p }
@@ -284,6 +454,7 @@ impl InMemoryThemeOptions {
// 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(),
content_version: None,
},
opacity: 30,
}),
@@ -571,5 +742,5 @@ pub fn render_preview(
}
#[cfg(test)]
#[path = "theme_test.rs"]
#[path = "theme_tests.rs"]
mod tests;
+38 -45
View File
@@ -1,55 +1,48 @@
use pathfinder_color::ColorU;
use settings::Setting as _;
use galaxy_editor::editor::NavigationKey;
use galaxyui::accessibility::{AccessibilityContent, WarpA11yRole};
use galaxyui::elements::{
Align, ChildAnchor, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
DispatchEventResult, Element, Empty, EventHandler, Fill, Flex, Hoverable, Icon,
MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor,
ParentElement, ParentOffsetBounds, Radius, Rect, SavePosition, ScrollStateHandle, Scrollable,
ScrollableElement, ScrollbarWidth, Shrinkable, Stack, Text, UniformList, UniformListState,
};
use galaxyui::fonts::{FamilyId, Weight};
use galaxyui::geometry::vector::vec2f;
use galaxyui::keymap::FixedBinding;
use galaxyui::platform::{Cursor, SystemTheme};
use galaxyui::ui_components::components::{UiComponent, UiComponentStyles};
use galaxyui::windowing::{StateEvent, WindowManager};
use galaxyui::{
accessibility::{AccessibilityContent, GalaxyA11yRole},
elements::{
Align, ChildAnchor, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
DispatchEventResult, Element, Empty, EventHandler, Fill, Flex, Hoverable, Icon,
MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor,
ParentElement, ParentOffsetBounds, Radius, Rect, SavePosition, ScrollStateHandle,
Scrollable, ScrollableElement, ScrollbarWidth, Shrinkable, Stack, Text, UniformList,
UniformListState,
},
fonts::{FamilyId, Weight},
geometry::vector::vec2f,
keymap::FixedBinding,
platform::{Cursor, SystemTheme},
ui_components::components::{UiComponent, UiComponentStyles},
windowing::{StateEvent, WindowManager},
AppContext, Entity, FocusContext, ModelHandle, SingletonEntity, Tracked, TypedActionView,
UpdateModel, View, ViewContext, ViewHandle,
};
use pathfinder_color::ColorU;
use settings::Setting as _;
use crate::resource_center::{mark_feature_used_and_write_to_user_defaults, Tip, TipAction};
use crate::themes::theme::{GalaxyTheme, RespectSystemTheme, ThemeKind};
use crate::util::traffic_lights::traffic_light_data;
use crate::workspace::PANEL_HEADER_HEIGHT;
use crate::{
appearance::Appearance,
editor::{
Event as EditorEvent, PropagateAndNoOpNavigationKeys, SingleLineEditorOptions, TextOptions,
},
referral_theme_status::ReferralThemeStatus,
report_if_error,
settings::{respect_system_theme, ThemeSettings},
themes::theme::SelectedSystemThemes,
user_config::{load_theme_configs, themes_dir, GalaxyConfig, GalaxyConfigUpdateEvent},
util::traffic_lights::{TrafficLightData, TrafficLightSide},
window_settings::WindowSettings,
};
use crate::{appearance::AppearanceManager, send_telemetry_from_ctx};
use crate::{editor::EditorView, resource_center::TipsCompleted};
use crate::{
server::telemetry::TelemetryEvent, ui_components::window_focus_dimming::WindowFocusDimming,
};
use crate::{
themes::theme::GalaxyThemeConfig,
ui_components::buttons::{close_button, icon_button},
ui_components::icons,
};
use super::theme;
use crate::appearance::{Appearance, AppearanceManager};
use crate::editor::{
EditorView, Event as EditorEvent, PropagateAndNoOpNavigationKeys, SingleLineEditorOptions,
TextOptions,
};
use crate::referral_theme_status::ReferralThemeStatus;
use crate::resource_center::{
mark_feature_used_and_write_to_user_defaults, Tip, TipAction, TipsCompleted,
};
use crate::server::telemetry::TelemetryEvent;
use crate::settings::{respect_system_theme, ThemeSettings};
use crate::themes::theme::{
RespectSystemTheme, SelectedSystemThemes, ThemeKind, WarpTheme, WarpThemeConfig,
};
use crate::ui_components::buttons::{close_button, icon_button};
use crate::ui_components::icons;
use crate::ui_components::window_focus_dimming::WindowFocusDimming;
use crate::user_config::{load_theme_configs, themes_dir, WarpConfig, WarpConfigUpdateEvent};
use crate::util::traffic_lights::{traffic_light_data, TrafficLightData, TrafficLightSide};
use crate::window_settings::WindowSettings;
use crate::workspace::PANEL_HEADER_HEIGHT;
use crate::{report_if_error, send_telemetry_from_ctx};
// All units in px
const THEME_CHOOSER_TITLE: &str = "Themes";
+1 -1
View File
@@ -1,6 +1,6 @@
use anyhow::{anyhow, Result};
use std::path::PathBuf;
use anyhow::{anyhow, Result};
use deltae::*;
use kmeans_colors::{get_kmeans_hamerly, Calculate, CentroidData, Sort};
use palette::{FromColor, IntoColor, Lab, Pixel, Srgb, Srgba};
+24 -23
View File
@@ -1,3 +1,27 @@
use std::default::Default;
use std::fmt;
use std::path::PathBuf;
#[cfg(feature = "local_fs")]
use std::{fs::copy, io::Write};
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::vec2f;
#[cfg(feature = "local_fs")]
use galaxy_core::ui::theme::WarpTheme;
use galaxyui::elements::{
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, DispatchEventResult,
EventHandler, Fill, Flex, Icon, MainAxisAlignment, MainAxisSize, MouseStateHandle,
ParentElement, Radius, Rect, SavePosition, Shrinkable, Text,
};
use galaxyui::fonts::Weight;
use galaxyui::platform::Cursor;
use galaxyui::ui_components::button::{ButtonVariant, TextAndIcon, TextAndIconAlignment};
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use galaxyui::ui_components::text_input::TextInput;
use galaxyui::{
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
};
use crate::appearance::{Appearance, AppearanceManager};
use crate::editor::{EditorView, Event as EditorEvent};
use crate::themes::theme::{InMemoryThemeOptions, ThemeKind};
@@ -6,29 +30,6 @@ use crate::user_config;
use crate::{
send_telemetry_from_ctx, server::telemetry::TelemetryEvent, themes::theme::CustomTheme,
};
#[cfg(feature = "local_fs")]
use galaxy_core::ui::theme::GalaxyTheme;
use galaxyui::elements::{
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, DispatchEventResult,
EventHandler, Fill, Flex, Icon, MainAxisAlignment, MainAxisSize, MouseStateHandle,
ParentElement, Radius, Rect, SavePosition, Shrinkable, Text,
};
use galaxyui::fonts::Weight;
use galaxyui::ui_components::button::{ButtonVariant, TextAndIcon, TextAndIconAlignment};
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use galaxyui::ui_components::text_input::TextInput;
use galaxyui::ViewHandle;
use galaxyui::{
platform::Cursor, AppContext, Element, Entity, SingletonEntity, TypedActionView, View,
ViewContext,
};
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::vec2f;
use std::default::Default;
use std::fmt;
use std::path::PathBuf;
#[cfg(feature = "local_fs")]
use std::{fs::copy, io::Write};
const BUTTON_PADDING: f32 = 12.;
const BUTTON_FONT_SIZE: f32 = 14.;
+13 -10
View File
@@ -1,3 +1,16 @@
use std::default::Default;
use std::path::PathBuf;
use warpui::fonts::Weight;
use warpui::keymap::FixedBinding;
use warpui::platform::{FilePickerConfiguration, FileType};
use warpui::presenter::ChildView;
use warpui::ui_components::components::{Coords, UiComponentStyles};
use warpui::{
AppContext, Element, Entity, SingletonEntity as _, TypedActionView, View, ViewContext,
ViewHandle,
};
use crate::modal::Modal;
use crate::themes::theme::ThemeKind;
use crate::themes::theme_creator_body::{
@@ -5,16 +18,6 @@ use crate::themes::theme_creator_body::{
};
use crate::view_components::DismissibleToast;
use crate::workspace::ToastStack;
use galaxyui::fonts::Weight;
use galaxyui::keymap::FixedBinding;
use galaxyui::platform::{FilePickerConfiguration, FileType};
use galaxyui::presenter::ChildView;
use galaxyui::ui_components::components::{Coords, UiComponentStyles};
use galaxyui::ViewHandle;
use galaxyui::{AppContext, SingletonEntity as _};
use galaxyui::{Element, Entity, TypedActionView, View, ViewContext};
use std::default::Default;
use std::path::PathBuf;
const THEME_CREATOR_MODAL_HEADER: &str = "Create new theme from image";
+1 -2
View File
@@ -1,6 +1,5 @@
use crate::util::color::OPAQUE;
use super::*;
use crate::util::color::OPAQUE;
// TODO(CORE-3626): figure out why the colors returned on Windows are slightly different.
#[test]
+14 -15
View File
@@ -1,25 +1,24 @@
use crate::appearance::Appearance;
use crate::send_telemetry_from_ctx;
use crate::server::telemetry::TelemetryEvent;
use crate::settings::{active_theme_kind, ThemeSettings};
use crate::themes::theme::{GalaxyTheme, ThemeKind};
use crate::user_config;
use crate::user_config::util::from_yaml;
use std::default::Default;
use std::fs;
use std::fs::remove_file;
use galaxyui::assets::asset_cache::AssetSource;
use galaxyui::elements::{
Container, CornerRadius, CrossAxisAlignment, Flex, MainAxisSize, MouseStateHandle,
ParentElement, Radius, SavePosition, Shrinkable, Text,
};
use galaxyui::fonts::Weight;
use galaxyui::platform::Cursor;
use galaxyui::ui_components::button::ButtonVariant;
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use galaxyui::{
platform::Cursor, AppContext, Element, Entity, SingletonEntity, TypedActionView, View,
ViewContext,
};
use std::default::Default;
use std::fs;
use std::fs::remove_file;
use galaxyui::{AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext};
use crate::appearance::Appearance;
use crate::server::telemetry::TelemetryEvent;
use crate::settings::{active_theme_kind, ThemeSettings};
use crate::themes::theme::{ThemeKind, WarpTheme};
use crate::user_config::util::from_yaml;
use crate::{send_telemetry_from_ctx, user_config};
const BUTTON_PADDING: f32 = 12.;
const BUTTON_FONT_SIZE: f32 = 14.;
@@ -82,7 +81,7 @@ impl ThemeDeletionBody {
if let Some(image) = theme_from_yaml.background_image() {
// Only delete the image if it is in the ./warp/themes directory.
// We don't want to delete images from other parts of the user's filesystem.
if let AssetSource::LocalFile { path } = image.source() {
if let AssetSource::LocalFile { path, .. } = image.source() {
let image_path_in_themes_dir = dir.join(path.as_str());
let _ = remove_file(image_path_in_themes_dir);
} else {
+7 -7
View File
@@ -1,14 +1,14 @@
use crate::modal::Modal;
use crate::themes::theme::ThemeKind;
use crate::themes::theme_deletion_body::{ThemeDeletionBody, ThemeDeletionBodyEvent};
use std::default::Default;
use galaxyui::fonts::Weight;
use galaxyui::keymap::FixedBinding;
use galaxyui::presenter::ChildView;
use galaxyui::ui_components::components::{Coords, UiComponentStyles};
use galaxyui::AppContext;
use galaxyui::ViewHandle;
use galaxyui::{Element, Entity, TypedActionView, View, ViewContext};
use std::default::Default;
use galaxyui::{AppContext, Element, Entity, TypedActionView, View, ViewContext, ViewHandle};
use crate::modal::Modal;
use crate::themes::theme::ThemeKind;
use crate::themes::theme_deletion_body::{ThemeDeletionBody, ThemeDeletionBodyEvent};
const THEME_DELETION_MODAL_HEADER: &str = "Are you sure you want to delete this theme?";
+486
View File
@@ -0,0 +1,486 @@
use settings_value::SettingsValue as _;
use super::*;
use crate::user_config;
use crate::util::color::OPAQUE;
fn custom_theme_json(path: &str) -> serde_json::Value {
serde_json::json!({
"name": "My Theme",
"path": path,
})
}
fn custom_theme_from_serde_path(path: &str) -> CustomTheme {
serde_json::from_value(custom_theme_json(path)).unwrap()
}
fn custom_theme_from_file_value_path(path: &str) -> CustomTheme {
CustomTheme::from_file_value(&custom_theme_json(path)).unwrap()
}
fn assert_custom_theme_is_syncable(custom_theme: CustomTheme) {
assert!(ThemeKind::Custom(custom_theme).is_custom_theme_reference_syncable());
}
fn assert_custom_theme_is_not_syncable(custom_theme: CustomTheme) {
assert!(!ThemeKind::Custom(custom_theme).is_custom_theme_reference_syncable());
}
fn custom_theme_path_for_storage(path: &Path, theme_root: &Path) -> PathBuf {
if path_is_absolute_or_foreign_absolute(path) {
return portable_custom_theme_storage_string(path, theme_root)
.map(PathBuf::from)
.unwrap_or_else(|| path.to_path_buf());
}
path.to_str()
.filter(|path| portable_stored_raw_components(path).is_some())
.map(PathBuf::from)
.unwrap_or_else(|| path.to_path_buf())
}
fn custom_theme_path_from_storage(path: &Path, theme_root: &Path) -> PathBuf {
if path_is_absolute_or_foreign_absolute(path) {
return portable_custom_theme_storage_string(path, theme_root)
.map(|path| portable_custom_theme_path_from_stored_raw(&path, theme_root))
.unwrap_or_else(|| path.to_path_buf());
}
path.to_str()
.map(|path| portable_custom_theme_path_from_stored_raw(path, theme_root))
.unwrap_or_else(|| path.to_path_buf())
}
#[test]
fn custom_theme_path_under_theme_root_storage_helper_returns_relative_path() {
let root = PathBuf::from("/home/user/.local/share/warp-terminal/themes");
let path = root.join("catppuccin/catppuccin_mocha.yml");
assert_eq!(
custom_theme_path_for_storage(&path, &root),
PathBuf::from("catppuccin/catppuccin_mocha.yml")
);
}
#[test]
fn custom_theme_relative_path_resolves_under_local_theme_root() {
let root = PathBuf::from("/Users/example/.warp/themes");
let stored = PathBuf::from("catppuccin/catppuccin_latte.yml");
assert_eq!(
custom_theme_path_from_storage(&stored, &root),
root.join("catppuccin/catppuccin_latte.yml")
);
}
#[test]
fn custom_theme_relative_parent_dir_path_is_preserved() {
let root = PathBuf::from("/Users/example/.warp/themes");
let stored = PathBuf::from("../outside.yml");
assert_eq!(custom_theme_path_from_storage(&stored, &root), stored);
}
#[test]
fn custom_theme_relative_parent_dir_path_is_not_portable() {
let root = PathBuf::from("/Users/example/.warp/themes");
assert!(!custom_theme_path_is_portable(
&PathBuf::from("../outside.yml"),
&root
));
}
#[test]
fn custom_theme_absolute_parent_dir_path_under_theme_root_storage_helper_preserves_path_and_rejects_portability(
) {
let root = PathBuf::from("/Users/example/.warp/themes");
let path = root.join("../outside.yml");
assert_eq!(custom_theme_path_from_storage(&path, &root), path);
assert!(!custom_theme_path_is_portable(&path, &root));
assert_eq!(custom_theme_path_for_storage(&path, &root), path);
}
#[test]
fn custom_theme_legacy_macos_path_is_preserved_even_when_local_file_exists() {
let temp = tempfile::tempdir().unwrap();
let root = temp.path().join("warp-terminal/themes");
let local = root.join("catppuccin/catppuccin_mocha.yml");
std::fs::create_dir_all(local.parent().unwrap()).unwrap();
std::fs::write(&local, "").unwrap();
let stored = PathBuf::from("/Users/example/.warp/themes/catppuccin/catppuccin_mocha.yml");
assert_eq!(custom_theme_path_from_storage(&stored, &root), stored);
}
#[test]
fn custom_theme_legacy_parent_dir_path_is_preserved() {
let temp = tempfile::tempdir().unwrap();
let root = temp.path().join("warp-terminal/themes");
let local = root.join("outside.yml");
std::fs::create_dir_all(local.parent().unwrap()).unwrap();
std::fs::write(&local, "").unwrap();
let stored = PathBuf::from("/Users/example/.warp/themes/../outside.yml");
assert_eq!(custom_theme_path_from_storage(&stored, &root), stored);
}
#[test]
fn custom_theme_legacy_linux_path_is_preserved_even_when_local_file_exists() {
let temp = tempfile::tempdir().unwrap();
let root = temp.path().join(".warp/themes");
let local = root.join("catppuccin/catppuccin_latte.yml");
std::fs::create_dir_all(local.parent().unwrap()).unwrap();
std::fs::write(&local, "").unwrap();
let stored = PathBuf::from(
"/home/user/.local/share/warp-terminal/themes/catppuccin/catppuccin_latte.yml",
);
assert_eq!(custom_theme_path_from_storage(&stored, &root), stored);
}
#[test]
fn custom_theme_unmatched_legacy_absolute_path_is_preserved() {
let temp = tempfile::tempdir().unwrap();
let root = temp.path().join(".warp/themes");
let stored = PathBuf::from("/Users/example/.warp/themes/missing.yml");
assert_eq!(custom_theme_path_from_storage(&stored, &root), stored);
}
#[test]
fn custom_theme_windows_absolute_path_string_is_preserved() {
let root = PathBuf::from("/Users/example/.warp/themes");
let stored = PathBuf::from(r"C:\Users\example\AppData\Roaming\warp\Warp\data\themes\mocha.yml");
assert_eq!(custom_theme_path_from_storage(&stored, &root), stored);
}
#[test]
fn custom_theme_windows_absolute_path_string_is_not_portable() {
let root = PathBuf::from("/Users/example/.warp/themes");
let stored = PathBuf::from(r"C:\Users\example\AppData\Roaming\warp\Warp\data\themes\mocha.yml");
assert!(!custom_theme_path_is_portable(&stored, &root));
}
#[test]
fn custom_theme_windows_absolute_path_string_storage_helper_preserves_path() {
let root = PathBuf::from("/Users/example/.warp/themes");
let stored = PathBuf::from(r"C:\Users\example\AppData\Roaming\warp\Warp\data\themes\mocha.yml");
assert_eq!(custom_theme_path_for_storage(&stored, &root), stored);
}
#[test]
fn custom_theme_windows_unc_path_string_is_preserved() {
let root = PathBuf::from("/Users/example/.warp/themes");
let stored = PathBuf::from(r"\\server\share\warp\themes\mocha.yml");
assert_eq!(custom_theme_path_from_storage(&stored, &root), stored);
}
#[test]
#[cfg(not(windows))]
fn custom_theme_relative_backslash_path_is_preserved() {
let root = PathBuf::from("/Users/example/.warp/themes");
let stored = PathBuf::from(r"catppuccin\mocha.yml");
assert_eq!(custom_theme_path_from_storage(&stored, &root), stored);
}
#[test]
#[cfg(not(windows))]
fn custom_theme_relative_backslash_path_is_not_portable() {
let root = PathBuf::from("/Users/example/.warp/themes");
let stored = PathBuf::from(r"catppuccin\mocha.yml");
assert!(!custom_theme_path_is_portable(&stored, &root));
}
#[test]
#[cfg(not(windows))]
fn custom_theme_relative_backslash_path_storage_helper_preserves_path() {
let root = PathBuf::from("/Users/example/.warp/themes");
let stored = PathBuf::from(r"catppuccin\mocha.yml");
assert_eq!(custom_theme_path_for_storage(&stored, &root), stored);
}
#[test]
fn custom_theme_serde_reads_portable_raw_path_under_theme_root() {
let custom = custom_theme_from_serde_path("catppuccin/mocha.yml");
assert_eq!(
custom.path(),
user_config::themes_dir()
.join("catppuccin")
.join("mocha.yml")
);
assert_custom_theme_is_syncable(custom);
}
#[test]
fn custom_theme_serde_preserves_unportable_raw_paths() {
for raw_path in [
"",
".",
"./mocha.yml",
"../outside.yml",
"catppuccin/../mocha.yml",
r"catppuccin\mocha.yml",
"C:/Users/example/AppData/Roaming/warp/Warp/data/themes/mocha.yml",
"C:themes/mocha.yml",
] {
let custom = custom_theme_from_serde_path(raw_path);
assert_eq!(custom.path(), PathBuf::from(raw_path));
assert_custom_theme_is_not_syncable(custom);
}
}
#[test]
fn custom_theme_settings_value_reads_portable_raw_path_under_theme_root() {
let custom = custom_theme_from_file_value_path("catppuccin/mocha.yml");
assert_eq!(
custom.path(),
user_config::themes_dir()
.join("catppuccin")
.join("mocha.yml")
);
assert_custom_theme_is_syncable(custom);
}
#[test]
fn custom_theme_settings_value_preserves_unportable_raw_paths() {
for raw_path in [
"",
".",
"./mocha.yml",
"../outside.yml",
"catppuccin/../mocha.yml",
r"catppuccin\mocha.yml",
"C:/Users/example/AppData/Roaming/warp/Warp/data/themes/mocha.yml",
"C:themes/mocha.yml",
] {
let custom = custom_theme_from_file_value_path(raw_path);
assert_eq!(custom.path(), PathBuf::from(raw_path));
assert_custom_theme_is_not_syncable(custom);
}
}
#[test]
fn custom_theme_settings_value_writes_portable_path_for_theme_root_file() {
let root_path = user_config::themes_dir().join("my_theme.yml");
let custom = CustomTheme::new("My Theme".to_string(), root_path);
let value = custom.to_file_value();
assert_eq!(value["name"], "My Theme");
assert_eq!(value["path"], "my_theme.yml");
}
#[test]
fn custom_theme_serde_writes_portable_path_for_theme_root_file() {
let root_path = user_config::themes_dir().join("my_theme.yml");
let custom = CustomTheme::new("My Theme".to_string(), root_path);
let value = serde_json::to_value(custom).unwrap();
assert_eq!(value["name"], "My Theme");
assert_eq!(value["path"], "my_theme.yml");
}
#[test]
fn custom_base16_theme_kind_uses_custom_theme_settings_value_path_rules() {
let root_path = user_config::themes_dir().join("base16/ocean.yml");
let kind = ThemeKind::CustomBase16(CustomTheme::new("Base16 Ocean".to_string(), root_path));
assert_eq!(
kind.to_file_value(),
serde_json::json!({
"custom_base_16": {
"name": "Base16 Ocean",
"path": "base16/ocean.yml"
}
})
);
}
#[cfg(windows)]
mod windows_custom_theme_path_tests {
fn windows_theme_root() -> PathBuf {
PathBuf::from(r"C:\Users\example\AppData\Roaming\warp\Warp\data\themes")
}
#[test]
fn custom_theme_windows_theme_root_path_serializes_with_slashes() {
let root = windows_theme_root();
let path = root.join("catppuccin").join("mocha.yml");
assert_eq!(
custom_theme_path_for_storage(&path, &root),
PathBuf::from("catppuccin/mocha.yml")
);
assert_eq!(
portable_custom_theme_storage_string(&path, &root).as_deref(),
Some("catppuccin/mocha.yml")
);
}
#[test]
fn custom_theme_windows_slash_stored_path_resolves_under_theme_root() {
let root = windows_theme_root();
let stored = PathBuf::from("catppuccin/mocha.yml");
assert_eq!(
custom_theme_path_from_storage(&stored, &root),
root.join("catppuccin").join("mocha.yml")
);
assert_eq!(
portable_custom_theme_path_from_stored_raw("catppuccin/mocha.yml", &root),
root.join("catppuccin").join("mocha.yml")
);
}
#[test]
fn custom_theme_windows_theme_root_path_is_portable() {
let root = windows_theme_root();
let path = root.join("catppuccin").join("mocha.yml");
assert!(custom_theme_path_is_portable(&path, &root));
}
#[test]
fn custom_theme_windows_raw_unportable_stored_paths_are_preserved() {
let root = windows_theme_root();
for raw_path in [
r"catppuccin\mocha.yml",
"C:/Users/example/AppData/Roaming/warp/Warp/data/themes/mocha.yml",
"C:themes/mocha.yml",
] {
assert_eq!(
portable_custom_theme_path_from_stored_raw(raw_path, &root),
PathBuf::from(raw_path)
);
}
}
#[test]
fn custom_theme_windows_raw_unportable_paths_are_not_portable() {
let root = windows_theme_root();
for raw_path in [
r"catppuccin\mocha.yml",
"C:/Users/example/AppData/Roaming/warp/Warp/data/themes/mocha.yml",
"C:themes/mocha.yml",
] {
assert!(!custom_theme_path_is_portable(
&PathBuf::from(raw_path),
&root
));
}
}
#[test]
fn custom_theme_windows_settings_value_serializes_theme_root_file_with_slashes() {
let root_path = user_config::themes_dir()
.join("catppuccin")
.join("mocha.yml");
let custom = CustomTheme::new("Mocha".to_string(), root_path);
let value = custom.to_file_value();
assert_eq!(value["path"], "catppuccin/mocha.yml");
}
#[test]
fn custom_theme_windows_serde_serializes_theme_root_file_with_slashes() {
let root_path = user_config::themes_dir()
.join("catppuccin")
.join("mocha.yml");
let custom = CustomTheme::new("Mocha".to_string(), root_path);
let value = serde_json::to_value(custom).unwrap();
assert_eq!(value["path"], "catppuccin/mocha.yml");
}
}
#[test]
#[cfg(not(target_family = "wasm"))]
fn in_memory_theme_generation_test() {
let mountains_bg_path: PathBuf = [
env!("CARGO_MANIFEST_DIR"),
"assets",
"async",
"jpg",
"mountains.jpg",
]
.iter()
.collect();
let mut in_memory_theme = warpui::r#async::block_on(InMemoryThemeOptions::new(
"mountains".to_string(),
mountains_bg_path.clone(),
))
.unwrap();
let mountains_bg_path_string = mountains_bg_path.to_str().unwrap_or_default().to_owned();
assert_eq!(
in_memory_theme.theme(),
WarpTheme::new(
// the theme defaults to the 0th bg color
ColorU::new(35, 31, 44, OPAQUE).into(),
// this background color makes it a "dark" theme, so the foreground is white
ColorU::white(),
// the most distinct accent color is 3rd one
ColorU::new(238, 203, 111, OPAQUE).into(),
None,
Some(Details::Darker),
dark_mode_colors(),
Some(Image {
source: AssetSource::LocalFile {
path: mountains_bg_path_string.clone(),
content_version: None,
},
opacity: 30,
}),
Some("mountains".to_string()),
)
);
in_memory_theme.chosen_bg_color_index = 2;
assert_eq!(
in_memory_theme.theme(),
WarpTheme::new(
// now the background is the 2nd one
ColorU::new(229, 142, 113, OPAQUE).into(),
// changing the background color made this a light theme
ColorU::black(),
// now the 4th color is the most distinct color
ColorU::new(193, 217, 212, OPAQUE).into(),
None,
Some(Details::Lighter),
light_mode_colors(),
Some(Image {
source: AssetSource::LocalFile {
path: mountains_bg_path_string,
content_version: None,
},
opacity: 30,
}),
Some("mountains".to_string()),
)
);
}