1170 lines
48 KiB
Rust
1170 lines
48 KiB
Rust
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<ITermTheme>,
|
|
dark: Box<ITermTheme>,
|
|
},
|
|
Single(Box<ITermTheme>),
|
|
}
|
|
|
|
impl TryFrom<ITermThemeType> for ThemeType {
|
|
type Error = ThemeError;
|
|
fn try_from(theme_type: ITermThemeType) -> Result<Self, Self::Error> {
|
|
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<Option<Dictionary>>,
|
|
foreground: Option<Dictionary>,
|
|
background: Option<Dictionary>,
|
|
cursor: Option<Dictionary>,
|
|
}
|
|
|
|
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<GalaxyTheme, ThemeError> {
|
|
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<ColorU, ThemeError> {
|
|
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::<u8, u8>();
|
|
|
|
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<ITermWorkingDirectoryStrategy> 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<ITermWorkingDirectory> 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<ITermKeystroke> for Keystroke {
|
|
type Error = HotkeyError;
|
|
fn try_from(value: ITermKeystroke) -> Result<Self, Self::Error> {
|
|
// 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<ITermGlobalHotkeyWindow> for GlobalHotkey {
|
|
type Error = HotkeyError;
|
|
fn try_from(value: ITermGlobalHotkeyWindow) -> Result<Self, Self::Error> {
|
|
Ok(GlobalHotkey::QuakeMode(QuakeModeWindow {
|
|
keystroke: TryInto::<Keystroke>::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<String>,
|
|
left_option_as_meta: bool,
|
|
right_option_as_meta: bool,
|
|
mouse_reporting: bool,
|
|
scroll_reporting: bool,
|
|
font_name: Option<String>,
|
|
font_size: Option<String>,
|
|
default_shell: Option<String>,
|
|
working_directory: Option<ITermWorkingDirectoryStrategy>,
|
|
hotkey_windows: Vec<ITermGlobalHotkeyWindow>,
|
|
activation_keystroke: Option<ITermKeystroke>,
|
|
columns: Option<u64>,
|
|
rows: Option<u64>,
|
|
transparency: Option<f64>,
|
|
copy_on_select: Option<bool>,
|
|
blur_radius: Option<f64>,
|
|
}
|
|
|
|
impl ITermProfile {
|
|
pub fn from_dictionary(
|
|
mut dict: Dictionary,
|
|
mut hotkey_windows: Vec<ITermGlobalHotkeyWindow>,
|
|
activation_keystroke: Option<ITermKeystroke>,
|
|
copy_on_select: Option<bool>,
|
|
) -> 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<Vec<Self>, 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::<Option<Vec<_>>>()
|
|
})
|
|
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::<Vec<_>>();
|
|
|
|
// 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::<Vec<_>>())
|
|
}
|
|
|
|
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::<GlobalHotkey>::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::<Keystroke>::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::<f32>().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<PathBuf> {
|
|
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<ITermTheme>, Box<ITermTheme>) {
|
|
(default_light_theme(), default_dark_theme())
|
|
}
|
|
|
|
fn default_light_theme() -> Box<ITermTheme> {
|
|
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<ITermTheme> {
|
|
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)),
|
|
])),
|
|
})
|
|
}
|