first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,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};
|
||||
|
||||
@@ -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.;
|
||||
|
||||
@@ -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,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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?";
|
||||
|
||||
|
||||
@@ -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()),
|
||||
)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user