use std::path::PathBuf; use async_trait::async_trait; use bitflags::bitflags; use galaxy_core::ui::theme::{AnsiColors, GalaxyTheme, TerminalColors}; use galaxyui::{ fonts::FontInfo, keymap::Keystroke, platform::mac::utils::unicode_char_to_key, DisplayIdx, }; use itertools::Itertools; use palette::Srgba; use pathfinder_color::ColorU; use plist::{Dictionary, Value}; use galaxy_core::ui::theme::{AnsiColors, TerminalColors, WarpTheme}; use galaxyui::fonts::FontInfo; use galaxyui::keymap::Keystroke; use galaxyui::platform::mac::utils::unicode_char_to_key; use galaxyui::DisplayIdx; use super::config::{ calculate_accent_color, Config, ConfigError, GlobalHotkey, ImportableSetting, ImportedFont, MouseAndScrollReporting, OpacitySettings, ParseableConfig, QuakeModeWindow, SettingType, ThemeError, ThemeType, }; use crate::root_view::QuakeModePinPosition; use crate::settings::import::config::HotkeyError; use crate::settings::{ExtraMetaKeys, DEFAULT_MONOSPACE_FONT_NAME, DEFAULT_MONOSPACE_FONT_SIZE}; use crate::terminal::local_tty::shell::is_valid_path_or_command_for_supported_shell; use crate::terminal::session_settings::{ StartupShell, WorkingDirectoryConfig, WorkingDirectoryMode, WorkingDirectoryPerSourceConfig, }; extern crate plist; const ITERM_DEFAULT_MONOSPACE_FONT_SIZE: &str = "12"; const ITERM_DEFAULT_MONOSPACE_FONT_FAMILY: &str = "Monaco"; const WARP_DEFAULT_WORKING_DIRECTORY: ITermWorkingDirectoryStrategy = ITermWorkingDirectoryStrategy::Simple(ITermWorkingDirectory::ReuseLast); const PIN_TOP: i64 = 2; const PIN_BOTTOM: i64 = 5; const PIN_LEFT: i64 = 6; const PIN_RIGHT: i64 = 7; bitflags! { /// Bit flags for modifier keys. Bit 17 = shift, bit 18 = ctrl, bit 19 = option, /// bit 20 = cmd, bit 21 = numpad (which Warp does not store as a modifier). #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Flags: u32 { const CTRL = 1 << 18; const ALT = 1 << 19; const SHIFT = 1 << 17; const CMD = 1 << 20; /// Bit mask for the non-modifier lower bits of iTerm's modifier flag. const LOWER_BIT_MASK = (!0u16) as u32 | 1 << 16; // Since we are using an external source, set all bits as "known." // See: https://docs.rs/bitflags/latest/bitflags/#externally-defined-flags const _ = !0; } } /// The iTerm theme data that we want to import. /// /// We [`Box`] the values here to keep this enum smaller (and the two variants) /// similarly-sized, as [`ITermTheme`] is 240 bytes (quite large). #[derive(Debug, PartialEq)] enum ITermThemeType { LightAndDark { light: Box, dark: Box, }, Single(Box), } impl TryFrom for ThemeType { type Error = ThemeError; fn try_from(theme_type: ITermThemeType) -> Result { let (default_light, default_dark) = default_iterm_themes(); match theme_type { ITermThemeType::LightAndDark { light, dark } => Ok(ThemeType::LightAndDark { light: light.into_warp_theme(" (Light)", &default_light)?, dark: dark.into_warp_theme(" (Dark)", &default_dark)?, }), ITermThemeType::Single(normal) => Ok(ThemeType::Single( normal.into_warp_theme("", &default_dark)?, )), } } } #[derive(Debug, Default, PartialEq)] struct ITermTheme { terminal_colors: Vec>, foreground: Option, background: Option, cursor: Option, } impl ITermTheme { fn from_dictionary(dict: &mut Dictionary, suffix: &'static str) -> Self { ITermTheme { terminal_colors: (0..16) .map(|color_idx| { dict.remove(format!("Ansi {color_idx} Color{suffix}").as_str()) .and_then(|value| value.into_dictionary()) }) .collect(), foreground: dict .remove(format!("Foreground Color{suffix}").as_str()) .and_then(|value| value.into_dictionary()), background: dict .remove(format!("Background Color{suffix}").as_str()) .and_then(|value| value.into_dictionary()), cursor: dict .remove(format!("Cursor Color{suffix}").as_str()) .and_then(|value| value.into_dictionary()), } } fn into_warp_theme( mut self, suffix: &'static str, default_theme: &ITermTheme, ) -> Result { if self.foreground == default_theme.foreground || self.background == default_theme.background { return Err(ThemeError::MissingValueError); } let background = color_dictionary_to_coloru(self.background.as_mut())?; let foreground = color_dictionary_to_coloru(self.foreground.as_mut())?; let cursor = color_dictionary_to_coloru(self.cursor.as_mut())?; let bright = AnsiColors { black: color_dictionary_to_coloru(self.terminal_colors[8].as_mut())?.into(), red: color_dictionary_to_coloru(self.terminal_colors[9].as_mut())?.into(), green: color_dictionary_to_coloru(self.terminal_colors[10].as_mut())?.into(), yellow: color_dictionary_to_coloru(self.terminal_colors[11].as_mut())?.into(), blue: color_dictionary_to_coloru(self.terminal_colors[12].as_mut())?.into(), magenta: color_dictionary_to_coloru(self.terminal_colors[13].as_mut())?.into(), cyan: color_dictionary_to_coloru(self.terminal_colors[14].as_mut())?.into(), white: color_dictionary_to_coloru(self.terminal_colors[15].as_mut())?.into(), }; let accent = calculate_accent_color(background, foreground, cursor, bright); Ok(GalaxyTheme::new( background.into(), foreground, accent.into(), Some(cursor.into()), None, TerminalColors { normal: AnsiColors { black: color_dictionary_to_coloru(self.terminal_colors[0].as_mut())?.into(), red: color_dictionary_to_coloru(self.terminal_colors[1].as_mut())?.into(), green: color_dictionary_to_coloru(self.terminal_colors[2].as_mut())?.into(), yellow: color_dictionary_to_coloru(self.terminal_colors[3].as_mut())?.into(), blue: color_dictionary_to_coloru(self.terminal_colors[4].as_mut())?.into(), magenta: color_dictionary_to_coloru(self.terminal_colors[5].as_mut())?.into(), cyan: color_dictionary_to_coloru(self.terminal_colors[6].as_mut())?.into(), white: color_dictionary_to_coloru(self.terminal_colors[7].as_mut())?.into(), }, bright, }, None, Some(format!("Imported iTerm Theme{suffix}")), )) } } fn color_dictionary_to_coloru(dict: Option<&mut Dictionary>) -> Result { let dict = dict.ok_or(ThemeError::MissingValueError)?; let srgb_color = Srgba::new( dict.remove("Red Component") .and_then(|value| value.as_real()) .ok_or(ThemeError::MissingValueError)?, dict.remove("Green Component") .and_then(|value| value.as_real()) .ok_or(ThemeError::MissingValueError)?, dict.remove("Blue Component") .and_then(|value| value.as_real()) .ok_or(ThemeError::MissingValueError)?, dict.remove("Alpha Component") .and_then(|value| value.as_real()) .unwrap_or(1.), ) .into_format::(); Ok(ColorU { r: srgb_color.red, g: srgb_color.green, b: srgb_color.blue, a: srgb_color.alpha, }) } /// How iTerm decides the working directory for new sessions. #[derive(Debug, PartialEq)] pub enum ITermWorkingDirectoryStrategy { /// Use the same working directory for all new sessions. Simple(ITermWorkingDirectory), /// Use a different working directory for different types of new sessions. Advanced { new_window: ITermWorkingDirectory, new_tab: ITermWorkingDirectory, new_pane: ITermWorkingDirectory, }, } impl From for WorkingDirectoryConfig { fn from(value: ITermWorkingDirectoryStrategy) -> Self { match value { ITermWorkingDirectoryStrategy::Simple(strategy) => WorkingDirectoryConfig { advanced_mode: false, global: strategy.into(), ..Default::default() }, ITermWorkingDirectoryStrategy::Advanced { new_window, new_tab, new_pane, } => WorkingDirectoryConfig { advanced_mode: true, new_window: new_window.into(), new_tab: new_tab.into(), split_pane: new_pane.into(), ..Default::default() }, } } } /// Choices for which working directory new sessions use. #[derive(Debug, PartialEq)] pub enum ITermWorkingDirectory { Home, ReuseLast, Custom(String), } impl ITermWorkingDirectory { pub fn from_str(strategy: String, directory: String) -> ITermWorkingDirectory { match strategy.as_str() { "No" => ITermWorkingDirectory::Home, "Recycle" => ITermWorkingDirectory::ReuseLast, "Yes" => ITermWorkingDirectory::Custom(directory), _ => ITermWorkingDirectory::Home, } } } impl From for WorkingDirectoryPerSourceConfig { fn from(value: ITermWorkingDirectory) -> Self { match value { ITermWorkingDirectory::Home => WorkingDirectoryPerSourceConfig { mode: WorkingDirectoryMode::HomeDir, custom_dir: "".to_string(), }, ITermWorkingDirectory::ReuseLast => WorkingDirectoryPerSourceConfig { mode: WorkingDirectoryMode::PreviousDir, custom_dir: "".to_string(), }, ITermWorkingDirectory::Custom(path) => WorkingDirectoryPerSourceConfig { mode: WorkingDirectoryMode::CustomDir, custom_dir: path, }, } } } #[derive(Clone, Debug, PartialEq)] pub struct ITermKeystroke { key: String, modifier: u64, } impl TryFrom for Keystroke { type Error = HotkeyError; fn try_from(value: ITermKeystroke) -> Result { // Because we have an "all" flag, this should always be Some(x). let modifier_flags = Flags::from_bits(value.modifier as u32).unwrap_or(Flags::empty()); if modifier_flags & (Flags::LOWER_BIT_MASK) != Flags::empty() { log::info!("Unknown modifier for keystroke {value:?}"); // Don't return an error, since iTerm uses lower bit flags for non-modifier related data. } let key = match unicode_char_to_key(value.key.chars().next().unwrap_or_default() as u16) { Some(key) => key.to_string(), None => value.key, }; Ok(Keystroke { ctrl: modifier_flags.contains(Flags::CTRL), alt: modifier_flags.contains(Flags::ALT), shift: modifier_flags.contains(Flags::SHIFT), cmd: modifier_flags.contains(Flags::CMD), // Neither Warp nor iTerm supports Meta in global hotkeys. meta: false, key, }) } } #[derive(Clone, Debug, PartialEq)] pub struct ITermGlobalHotkeyWindow { keystroke: ITermKeystroke, autohide: bool, /// Which screen the hotkey window should open on. -1 = any screen, /// -2 = screen with cursor (not supported in Warp), and >= 0 is the index of the screen. screen: i64, /// How the quake window displays. 2 is pin to top, 5 is bottom, 6 is left, and 7 is right. screen_type: i64, } impl ITermGlobalHotkeyWindow { pub fn new(hotkey_window: &Dictionary) -> Self { ITermGlobalHotkeyWindow { keystroke: ITermKeystroke { key: hotkey_window .get("HotKey Characters Ignoring Modifiers") .and_then(|characters| characters.as_string()) .unwrap_or_default() .to_string(), modifier: hotkey_window .get("HotKey Modifier Flags") .and_then(|modifier| modifier.as_unsigned_integer()) .unwrap_or_default(), }, autohide: hotkey_window .get("HotKey Window AutoHides") .and_then(|autohide| autohide.as_boolean()) .unwrap_or(true), screen: hotkey_window .get("Screen") .and_then(|screen| screen.as_signed_integer()) .unwrap_or(-1), screen_type: hotkey_window .get("Window Type") .and_then(|window_type| window_type.as_signed_integer()) .unwrap_or(-1), } } } impl TryFrom for GlobalHotkey { type Error = HotkeyError; fn try_from(value: ITermGlobalHotkeyWindow) -> Result { Ok(GlobalHotkey::QuakeMode(QuakeModeWindow { keystroke: TryInto::::try_into(value.keystroke)?, autohide: value.autohide, screen: if value.screen == 0 { Some(DisplayIdx::Primary) } else if value.screen > 0 { if let Ok(screen_idx) = (value.screen - 1).try_into() { Some(DisplayIdx::External(screen_idx)) } else { return Err(HotkeyError::UnsupportedWindowType); } } else if value.screen == -1 { None } else { return Err(HotkeyError::UnsupportedWindowType); }, pin_position: match value.screen_type { PIN_TOP => QuakeModePinPosition::Top, PIN_BOTTOM => QuakeModePinPosition::Bottom, PIN_LEFT => QuakeModePinPosition::Left, PIN_RIGHT => QuakeModePinPosition::Right, screen_type => { // We filter out dedicated hotkey windows that are not supported earlier, // so this profile must be the default profile. log::info!( "Imported quake mode profile has unsupported window type {screen_type}" ); QuakeModePinPosition::Top } }, })) } } #[derive(Debug, PartialEq)] pub struct ITermProfile { theme: ITermThemeType, profile_name: Option, left_option_as_meta: bool, right_option_as_meta: bool, mouse_reporting: bool, scroll_reporting: bool, font_name: Option, font_size: Option, default_shell: Option, working_directory: Option, hotkey_windows: Vec, activation_keystroke: Option, columns: Option, rows: Option, transparency: Option, copy_on_select: Option, blur_radius: Option, } impl ITermProfile { pub fn from_dictionary( mut dict: Dictionary, mut hotkey_windows: Vec, activation_keystroke: Option, copy_on_select: Option, ) -> Self { let theme_type = match dict .remove("Use Separate Colors for Light and Dark Mode") .and_then(|value| value.as_boolean()) .unwrap_or(false) { true => ITermThemeType::LightAndDark { light: Box::new(ITermTheme::from_dictionary(&mut dict, " (Light)")), dark: Box::new(ITermTheme::from_dictionary(&mut dict, " (Dark)")), }, false => ITermThemeType::Single(Box::new(ITermTheme::from_dictionary(&mut dict, ""))), }; let mouse_reporting_enabled = dict .remove("Mouse Reporting") .and_then(|reporting| reporting.as_boolean()) .unwrap_or(true); let (font_family, font_size) = dict .remove("Normal Font") .and_then(|font| font.into_string()) .and_then(|font| { // iTerm stores fonts in the format "{font-name-and-style} {font-size}". // Get (font_name, font_size) as a tuple. font.split(' ') .take(2) .map(|element| element.to_owned()) .collect_tuple::<(String, String)>() }) .unzip(); let working_directory = match dict .remove("Custom Directory") .and_then(|value| value.into_string()) .unwrap_or_default() .as_str() { strategy @ ("Yes" | "No" | "Recycle") => { ITermWorkingDirectoryStrategy::Simple(ITermWorkingDirectory::from_str( strategy.to_string(), dict.remove("Working Directory") .and_then(|value| value.into_string()) .unwrap_or_default(), )) } "Advanced" => ITermWorkingDirectoryStrategy::Advanced { new_window: ITermWorkingDirectory::from_str( dict.remove("AWDS Window Option") .and_then(|value| value.into_string()) .unwrap_or_default(), dict.remove("AWDS Window Directory") .and_then(|value| value.into_string()) .unwrap_or_default(), ), new_tab: ITermWorkingDirectory::from_str( dict.remove("AWDS Tab Option") .and_then(|value| value.into_string()) .unwrap_or_default(), dict.remove("AWDS Tab Directory") .and_then(|value| value.into_string()) .unwrap_or_default(), ), new_pane: ITermWorkingDirectory::from_str( dict.remove("AWDS Pane Option") .and_then(|value| value.into_string()) .unwrap_or_default(), dict.remove("AWDS Pane Directory") .and_then(|value| value.into_string()) .unwrap_or_default(), ), }, &_ => ITermWorkingDirectoryStrategy::Simple(ITermWorkingDirectory::Home), }; if hotkey_windows.is_empty() && dict .get("Has Hotkey") .and_then(|has_hotkey_value| has_hotkey_value.as_boolean()) .is_some_and(|has_hotkey| has_hotkey) { hotkey_windows.push(ITermGlobalHotkeyWindow::new(&dict)); } ITermProfile { theme: theme_type, profile_name: dict.remove("Name").and_then(|name| name.into_string()), left_option_as_meta: dict .remove("Option Key Sends") .map(|value| value.as_unsigned_integer() == Some(1)) .unwrap_or(false), right_option_as_meta: dict .remove("Right Option Key Sends") .map(|value| value.as_unsigned_integer() == Some(1)) .unwrap_or(false), mouse_reporting: mouse_reporting_enabled && dict .remove("Mouse Reporting allow clicks and drags") .and_then(|reporting| reporting.as_boolean()) .unwrap_or(true), // Allow scroll reporting to be set without mouse reporting scroll_reporting: dict .remove("Mouse Reporting allow mouse wheel") .and_then(|reporting| reporting.as_boolean()) .unwrap_or(true), font_name: font_family, font_size, default_shell: dict.remove("Custom Command").and_then(|custom_command| { if custom_command.into_string().is_some_and(|command_pref| { command_pref == "Custom Shell" || command_pref == "Yes" }) { dict.remove("Command") .and_then(|command| command.into_string()) } else { None } }), working_directory: Some(working_directory), hotkey_windows, activation_keystroke, copy_on_select, rows: dict .remove("Rows") .and_then(|rows| rows.as_unsigned_integer()), columns: dict .remove("Columns") .and_then(|cols| cols.as_unsigned_integer()), transparency: dict .remove("Transparency") .and_then(|transparency| transparency.as_real()), blur_radius: if dict .remove("Blur") .and_then(|blur| blur.as_boolean()) .unwrap_or_default() { dict.remove("Blur Radius") .and_then(|blur_radius| blur_radius.as_real()) } else { None }, } } } #[async_trait] impl ParseableConfig for ITermProfile { async fn from_file(path: PathBuf) -> Result, ConfigError> { let Some(mut dict) = Value::from_file(path.clone()) .map_err(|_| ConfigError::MalformattedFileError(path.clone()))? .into_dictionary() else { return Err(ConfigError::MalformattedFileError(path)); }; if dict .remove("LoadPrefsFromCustomFolder") .and_then(|value| value.as_boolean()) .unwrap_or(false) { let Some(custom_path) = dict .remove("PrefsCustomFolder") .and_then(|folder| folder.into_string()) else { return Err(ConfigError::MalformattedFileError(path)); }; let Some(custom_dict) = Value::from_file( PathBuf::from(custom_path.clone()).join("com.googlecode.iterm2.plist"), ) .map_err(|_| { ConfigError::MalformattedFileError( PathBuf::from(custom_path.clone()).join("com.googlecode.iterm2.plist"), ) })? .into_dictionary() else { return Err(ConfigError::MalformattedFileError(PathBuf::from( custom_path, ))); }; dict = custom_dict; } let Some(default_guid) = dict .remove("Default Bookmark Guid") .and_then(|guid| guid.into_string()) else { return Err(ConfigError::MalformattedFileError(path)); }; let Some(profiles) = dict .remove("New Bookmarks") .and_then(|profiles| profiles.into_array()) .and_then(|profiles| { profiles .into_iter() .map(|profile| profile.into_dictionary()) .collect::>>() }) else { return Err(ConfigError::MalformattedFileError(path)); }; // Get candidate hotkey windows. let hotkey_windows = profiles .iter() .filter_map(|hotkey_window| { if !(hotkey_window .get("Has Hotkey") .and_then(|has_hotkey_value| has_hotkey_value.as_boolean()) .is_some_and(|has_hotkey| has_hotkey) && hotkey_window .get("Window Type") .and_then(|window_type| window_type.as_signed_integer()) .is_some_and(|window_type| { [PIN_TOP, PIN_BOTTOM, PIN_LEFT, PIN_RIGHT].contains(&window_type) })) { None } else { Some(ITermGlobalHotkeyWindow::new(hotkey_window)) } }) .collect::>(); // Get the keystroke to show/hide all iTerm windows. let activation_keystroke = if dict .get("Hotkey") .and_then(|has_hotkey_value| has_hotkey_value.as_boolean()) .is_some_and(|has_hotkey| has_hotkey) { dict.remove("HotkeyChar") .and_then(|value| value.as_unsigned_integer()) .zip( dict.remove("HotkeyModifiers") .and_then(|value| value.as_unsigned_integer()), ) .map(|(char, modifier)| ITermKeystroke { key: char::from_u32(char as u32).unwrap_or_default().to_string(), modifier, }) } else { None }; let copy_on_select = dict .remove("CopySelection") .and_then(|value| value.as_boolean()); Ok(profiles .into_iter() .filter(|profile| { profile .get("Guid") .and_then(|guid| guid.as_string()) .is_some_and(|guid| guid == default_guid) }) .map(|dictionary| { ITermProfile::from_dictionary( dictionary, hotkey_windows.clone(), activation_keystroke.clone(), copy_on_select, ) }) .collect::>()) } fn parse(mut self, fonts: &[FontInfo]) -> Config { // iTerm stores its fonts with internal names and supports styles as default terminal text, whereas Warp changes fonts based on display name. // Only import a font if there is only one font whose iTerm name starts with the display name of a font Warp supports. let translated_font_name = fonts .iter() .find(|font_info| { self.font_name .as_ref() .is_some_and(|font_name| font_info.font_names.contains(font_name)) }) .map(|font_info: &FontInfo| font_info.family_name.to_owned()); let hotkey_window = if self.hotkey_windows.len() == 1 { TryInto::::try_into(self.hotkey_windows.remove(0)) } else if self.hotkey_windows.is_empty() { Err(HotkeyError::MissingHotkey) } else { Err(HotkeyError::MultipleHotkeys) }; let hotkey_mode = match ( hotkey_window, self.activation_keystroke .map(TryInto::::try_into), ) { (Ok(_), Some(Ok(activation_keystroke))) => { log::info!("Found an activation keystroke and at least one quake mode window!"); Ok(GlobalHotkey::Activation(activation_keystroke)) } (Ok(hotkey_window), _) => Ok(hotkey_window), // We had multiple quake mode windows, so propagate the error up. (Err(HotkeyError::MultipleHotkeys), _) => Err(HotkeyError::MultipleHotkeys), (Err(_), Some(Ok(activation_keystroke))) => { Ok(GlobalHotkey::Activation(activation_keystroke)) } (Err(_), _) => Err(HotkeyError::MissingHotkey), }; let mouse_and_scroll_reporting = match (self.mouse_reporting, self.scroll_reporting) { // Since this is the Warp default, return None. (true, true) => None, (mouse_reporting, scroll_reporting) => Some(MouseAndScrollReporting { mouse_reporting, scroll_reporting, }), }; let opacity_settings = OpacitySettings { opacity: self .transparency .map(|transparency| ((1. - transparency) * 100.) as u8), blur_radius: self.blur_radius.map(|blur_radius| blur_radius as u8), }; let default_shell = self .default_shell .filter(|shell| is_valid_path_or_command_for_supported_shell(shell)) .map(StartupShell::Custom); let font = ImportedFont { family: translated_font_name, size: self .font_size .and_then(|font_size| font_size.parse::().ok()), }; let window_size = ( self.columns.map(|cols| cols as u16), self.rows.map(|rows| rows as u16), ); let working_directory = self.working_directory.map(|workspace| workspace.into()); let option_as_meta = ExtraMetaKeys { left_alt: self.left_option_as_meta, right_alt: self.right_option_as_meta, }; Config { theme: ImportableSetting::new(self.theme.try_into(), SettingType::Theme), terminal_name: "iTerm2".to_string(), mouse_and_scroll_reporting: ImportableSetting::new( mouse_and_scroll_reporting, SettingType::MouseAndScrollReporting, ), option_as_meta: ImportableSetting::new(option_as_meta, SettingType::OptionAsMeta), description: self.profile_name.map(|name| format!("Profile: {name}")), font: ImportableSetting::new(font, SettingType::Font), default_shell: ImportableSetting::new(default_shell, SettingType::DefaultShell), working_directory: ImportableSetting::new( working_directory, SettingType::WorkingDirectory, ), hotkey_mode: ImportableSetting::new(hotkey_mode, SettingType::HotkeyMode), copy_on_select: ImportableSetting::new(self.copy_on_select, SettingType::CopyOnSelect), window_size: ImportableSetting::new(window_size, SettingType::WindowSize), cursor_blinking: ImportableSetting::new(None, SettingType::CursorBlinking), opacity: ImportableSetting::new(opacity_settings, SettingType::Opacity), } } fn default_paths() -> Vec { vec![PathBuf::from( shellexpand::tilde("~/Library/Preferences/com.googlecode.iterm2.plist") .into_owned() .to_string(), )] } fn remove_default_values(mut self) -> Self { let (default_light, default_dark) = default_iterm_themes(); self.theme = match self.theme { ITermThemeType::LightAndDark { light, dark } => { match (light == default_light, dark == default_dark) { (true, true) => ITermThemeType::Single(Box::default()), (true, false) => ITermThemeType::Single(dark), (false, true) => ITermThemeType::Single(light), (false, false) => { if light == dark { ITermThemeType::Single(dark) } else { ITermThemeType::LightAndDark { light, dark } } } } } ITermThemeType::Single(theme) => { if theme == default_dark { ITermThemeType::Single(Box::default()) } else { ITermThemeType::Single(theme) } } }; let default_profile = ITermProfile::default(); // Don't import font family if it is the default monospace font family. if self.font_name == default_profile.font_name || self.font_name == Some(DEFAULT_MONOSPACE_FONT_NAME.to_string()) { self.font_name = None; } // Don't import font size if it is the default monospace font size. if self.font_size == default_profile.font_size || self.font_size == Some((DEFAULT_MONOSPACE_FONT_SIZE as u32).to_string()) { self.font_size = None; } if self.working_directory == default_profile.working_directory || self.working_directory == Some(WARP_DEFAULT_WORKING_DIRECTORY) { self.working_directory = None; } // Warp's default is not to open windows with a custom size, // so there is nothing to check against. if self.rows == default_profile.rows { self.rows = None; } if self.columns == default_profile.columns { self.columns = None; } // iTerm's presets are the same as Warp's if self.transparency == default_profile.transparency { self.transparency = None; } if self.copy_on_select == default_profile.copy_on_select { self.copy_on_select = None; } self } } #[cfg(test)] #[path = "iterm_parser_tests.rs"] mod tests; impl Default for ITermProfile { fn default() -> Self { Self { right_option_as_meta: false, left_option_as_meta: false, mouse_reporting: true, scroll_reporting: true, font_name: Some(ITERM_DEFAULT_MONOSPACE_FONT_FAMILY.to_string()), font_size: Some(ITERM_DEFAULT_MONOSPACE_FONT_SIZE.to_string()), theme: ITermThemeType::LightAndDark { light: default_light_theme(), dark: default_dark_theme(), }, profile_name: Some("Default".to_string()), default_shell: None, working_directory: Some(ITermWorkingDirectoryStrategy::Simple( ITermWorkingDirectory::Home, )), hotkey_windows: vec![], activation_keystroke: None, rows: Some(25), columns: Some(80), copy_on_select: Some(true), transparency: Some(0.), blur_radius: None, } } } fn default_iterm_themes() -> (Box, Box) { (default_light_theme(), default_dark_theme()) } fn default_light_theme() -> Box { Box::new(ITermTheme { terminal_colors: vec![ Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.0784313753247261)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.11764705926179886)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.09803921729326248)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.7074432373046875)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.16300037503242493)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.23660069704055786)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.0)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.0)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.7607843279838562)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.7805864810943604)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.0)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.7695948481559753)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.15404300391674042)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.7821617722511292)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.2647435665130615)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.752197265625)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.7449436187744141)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.24931684136390686)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.0)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.7816620469093323)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.7742590308189392)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.7810397744178772)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.7810482978820801)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.7810582518577576)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.4078176021575928)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.4078223705291748)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.40782788395881653)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.8659515380859375)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.45833224058151245)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.4752407670021057)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.3450070321559906)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.5654193758964539)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.9042816162109375)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.9259033203125)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.0)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.8833775520324707)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.6534907817840576)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.9485321044921875)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.6704471707344055)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.8821563720703125)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.8821563720703125)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.4927266538143158)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.3759753108024597)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(1.0)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.9926329255104065)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.9999960064888)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(1.0)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(1.0)), ])), ], foreground: Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.06274509803921569)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.06274509803921569)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.06274509803921569)), ])), background: Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.98)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.98)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.98)), ])), cursor: Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.0)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.0)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.0)), ])), }) } fn default_dark_theme() -> Box { Box::new(ITermTheme { terminal_colors: vec![ Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.0784313753247261)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.11764705926179886)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.09803921729326248)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.7074432373046875)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.16300037503242493)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.23660069704055786)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.0)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.0)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.7607843279838562)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.7805864810943604)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.0)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.7695948481559753)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.15404300391674042)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.7821617722511292)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.2647435665130615)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.752197265625)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.7449436187744141)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.24931684136390686)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.0)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.7816620469093323)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.7742590308189392)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.7810397744178772)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.7810482978820801)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.7810582518577576)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.4078176021575928)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.4078223705291748)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.40782788395881653)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.8659515380859375)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.45833224058151245)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.4752407670021057)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.3450070321559906)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.5654193758964539)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.9042816162109375)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.9259033203125)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.0)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.8833775520324707)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.6534907817840576)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.9485321044921875)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.6704471707344055)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.8821563720703125)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.8821563720703125)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.4927266538143158)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.3759753108024597)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(1.0)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.9926329255104065)), ])), Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.9999960064888)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(1.0)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(1.0)), ])), ], foreground: Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.8619791269302368)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.8619885444641113)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.86199951171875)), ])), background: Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.0806884765625)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.12103271484375)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(0.09911105036735535)), ])), cursor: Some(Dictionary::from_iter([ ("Red Component", Value::Real(0.9999763369560242)), ("Color Space", Value::String("sRGB".to_string())), ("Blue Component", Value::Real(0.9999872446060181)), ("Alpha Component", Value::Real(1.0)), ("Green Component", Value::Real(1.0)), ])), }) }