T03: built-in themes, chrome palette, dark-mode choice
CI / platforms (windows-latest) (push) Failing after 7s
CI / macos (push) Successful in 9m33s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-14 11:44:00 -04:00
1 parent 653f654b42
commit 7d4d53547e
20 files changed
+734 -15

No files matched your search

+2
View File
@@ -18,6 +18,8 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- `src/main.rs`: start, window. `src/lib.rs`: the modules, public so tests reach them.
- `src/settings`: the model, its ranges, and `SettingsStore`, the global that loads, checks and saves `settings.json`.
- `src/themes`: built-in terminal themes, the chrome palette each maps to, and `apply`, which dresses the app for the system's appearance.
- `assets/themes`: Ghostty theme files copied verbatim from iTerm2-Color-Schemes (MIT, its `LICENSE` beside them).
## Rules
+25
View File
@@ -0,0 +1,25 @@
MIT License
Copyright (c) 2011 to Present Mark Badolato
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This license covers the iTerm-Color-Schemes repository collection of themes.
The copyright/license for each individual theme belongs to the author of that theme.
+22
View File
@@ -0,0 +1,22 @@
palette = 0=#51576d
palette = 1=#e78284
palette = 2=#a6d189
palette = 3=#e5c890
palette = 4=#8caaee
palette = 5=#f4b8e4
palette = 6=#81c8be
palette = 7=#b5bfe2
palette = 8=#626880
palette = 9=#eda0a2
palette = 10=#b9dba2
palette = 11=#ecd7ae
palette = 12=#adc2f3
palette = 13=#f38ed8
palette = 14=#98d2ca
palette = 15=#a5adce
background = #303446
foreground = #c6d0f5
cursor-color = #f2d5cf
cursor-text = #303446
selection-background = #f2d5cf
selection-foreground = #303446
+22
View File
@@ -0,0 +1,22 @@
palette = 0=#bcc0cc
palette = 1=#d20f39
palette = 2=#40a02b
palette = 3=#df8e1d
palette = 4=#1e66f5
palette = 5=#ea76cb
palette = 6=#179299
palette = 7=#5c5f77
palette = 8=#acb0be
palette = 9=#e7103f
palette = 10=#46b02f
palette = 11=#e49931
palette = 12=#3878f6
palette = 13=#ef95d7
palette = 14=#19a1a8
palette = 15=#6c6f85
background = #eff1f5
foreground = #4c4f69
cursor-color = #dc8a78
cursor-text = #eff1f5
selection-background = #dc8a78
selection-foreground = #eff1f5
+22
View File
@@ -0,0 +1,22 @@
palette = 0=#494d64
palette = 1=#ed8796
palette = 2=#a6da95
palette = 3=#eed49f
palette = 4=#8aadf4
palette = 5=#f5bde6
palette = 6=#8bd5ca
palette = 7=#b8c0e0
palette = 8=#5b6078
palette = 9=#f2a7b2
palette = 10=#bde3b0
palette = 11=#f4e3c1
palette = 12=#adc5f7
palette = 13=#f493da
palette = 14=#a5ded6
palette = 15=#a5adcb
background = #24273a
foreground = #cad3f5
cursor-color = #f4dbd6
cursor-text = #24273a
selection-background = #f4dbd6
selection-foreground = #24273a
+22
View File
@@ -0,0 +1,22 @@
palette = 0=#45475a
palette = 1=#f38ba8
palette = 2=#a6e3a1
palette = 3=#f9e2af
palette = 4=#89b4fa
palette = 5=#f5c2e7
palette = 6=#94e2d5
palette = 7=#bac2de
palette = 8=#585b70
palette = 9=#f7aec2
palette = 10=#c2ecbf
palette = 11=#fcd682
palette = 12=#aeccfc
palette = 13=#f398da
palette = 14=#b1eae1
palette = 15=#a6adc8
background = #1e1e2e
foreground = #cdd6f4
cursor-color = #f5e0dc
cursor-text = #1e1e2e
selection-background = #f5e0dc
selection-foreground = #1e1e2e
+22
View File
@@ -0,0 +1,22 @@
palette = 0=#21222c
palette = 1=#ff5555
palette = 2=#50fa7b
palette = 3=#f1fa8c
palette = 4=#bd93f9
palette = 5=#ff79c6
palette = 6=#8be9fd
palette = 7=#f8f8f2
palette = 8=#6272a4
palette = 9=#ff6e6e
palette = 10=#69ff94
palette = 11=#ffffa5
palette = 12=#d6acff
palette = 13=#ff92df
palette = 14=#a4ffff
palette = 15=#ffffff
background = #282a36
foreground = #f8f8f2
cursor-color = #f8f8f2
cursor-text = #282a36
selection-background = #44475a
selection-foreground = #ffffff
+22
View File
@@ -0,0 +1,22 @@
palette = 0=#282828
palette = 1=#cc241d
palette = 2=#98971a
palette = 3=#d79921
palette = 4=#458588
palette = 5=#b16286
palette = 6=#689d6a
palette = 7=#a89984
palette = 8=#928374
palette = 9=#fb4934
palette = 10=#b8bb26
palette = 11=#fabd2f
palette = 12=#83a598
palette = 13=#d3869b
palette = 14=#8ec07c
palette = 15=#ebdbb2
background = #282828
foreground = #ebdbb2
cursor-color = #ebdbb2
cursor-text = #282828
selection-background = #665c54
selection-foreground = #ebdbb2
+22
View File
@@ -0,0 +1,22 @@
palette = 0=#090618
palette = 1=#c34043
palette = 2=#76946a
palette = 3=#c0a36e
palette = 4=#7e9cd8
palette = 5=#957fb8
palette = 6=#6a9589
palette = 7=#c8c093
palette = 8=#727169
palette = 9=#e82424
palette = 10=#98bb6c
palette = 11=#e6c384
palette = 12=#7fb4ca
palette = 13=#938aa9
palette = 14=#7aa89f
palette = 15=#dcd7ba
background = #1f1f28
foreground = #dcd7ba
cursor-color = #dcd7ba
cursor-text = #1f1f28
selection-background = #dcd7ba
selection-foreground = #1f1f28
+22
View File
@@ -0,0 +1,22 @@
palette = 0=#3b4252
palette = 1=#bf616a
palette = 2=#a3be8c
palette = 3=#ebcb8b
palette = 4=#81a1c1
palette = 5=#b48ead
palette = 6=#88c0d0
palette = 7=#e5e9f0
palette = 8=#596377
palette = 9=#bf616a
palette = 10=#a3be8c
palette = 11=#ebcb8b
palette = 12=#81a1c1
palette = 13=#b48ead
palette = 14=#8fbcbb
palette = 15=#eceff4
background = #2e3440
foreground = #d8dee9
cursor-color = #eceff4
cursor-text = #282828
selection-background = #eceff4
selection-foreground = #4c566a
+22
View File
@@ -0,0 +1,22 @@
palette = 0=#000000
palette = 1=#de3e35
palette = 2=#3f953a
palette = 3=#d2b67c
palette = 4=#2f5af3
palette = 5=#950095
palette = 6=#3f953a
palette = 7=#bbbbbb
palette = 8=#000000
palette = 9=#de3e35
palette = 10=#3f953a
palette = 11=#d2b67c
palette = 12=#2f5af3
palette = 13=#a00095
palette = 14=#3f953a
palette = 15=#ffffff
background = #f9f9f9
foreground = #2a2c33
cursor-color = #bbbbbb
cursor-text = #ffffff
selection-background = #d0dffc
selection-foreground = #2a2c33
+22
View File
@@ -0,0 +1,22 @@
palette = 0=#26233a
palette = 1=#eb6f92
palette = 2=#31748f
palette = 3=#f6c177
palette = 4=#9ccfd8
palette = 5=#c4a7e7
palette = 6=#ebbcba
palette = 7=#e0def4
palette = 8=#6e6a86
palette = 9=#eb6f92
palette = 10=#31748f
palette = 11=#f6c177
palette = 12=#9ccfd8
palette = 13=#c4a7e7
palette = 14=#ebbcba
palette = 15=#e0def4
background = #191724
foreground = #e0def4
cursor-color = #e0def4
cursor-text = #191724
selection-background = #403d52
selection-foreground = #e0def4
+22
View File
@@ -0,0 +1,22 @@
palette = 0=#073642
palette = 1=#dc322f
palette = 2=#859900
palette = 3=#b58900
palette = 4=#268bd2
palette = 5=#d33682
palette = 6=#2aa198
palette = 7=#bbb5a2
palette = 8=#002b36
palette = 9=#cb4b16
palette = 10=#586e75
palette = 11=#657b83
palette = 12=#839496
palette = 13=#6c71c4
palette = 14=#93a1a1
palette = 15=#fdf6e3
background = #fdf6e3
foreground = #657b83
cursor-color = #657b83
cursor-text = #eee8d5
selection-background = #eee8d5
selection-foreground = #586e75
+22
View File
@@ -0,0 +1,22 @@
palette = 0=#15161e
palette = 1=#f7768e
palette = 2=#9ece6a
palette = 3=#e0af68
palette = 4=#7aa2f7
palette = 5=#bb9af7
palette = 6=#7dcfff
palette = 7=#a9b1d6
palette = 8=#414868
palette = 9=#f7768e
palette = 10=#9ece6a
palette = 11=#e0af68
palette = 12=#7aa2f7
palette = 13=#bb9af7
palette = 14=#7dcfff
palette = 15=#c0caf5
background = #1a1b26
foreground = #c0caf5
cursor-color = #c0caf5
cursor-text = #15161e
selection-background = #33467c
selection-foreground = #c0caf5
+1
View File
@@ -1 +1,2 @@
pub mod settings;
pub mod themes;
+6 -7
View File
@@ -1,9 +1,5 @@
use ely_gpui_component::{
Assets,
primitives::FocusScope,
theme::{ActiveTheme, Mode, Theme},
};
use elyterminal::settings::SettingsStore;
use ely_gpui_component::{Assets, primitives::FocusScope, theme::ActiveTheme};
use elyterminal::{settings::SettingsStore, themes};
use gpui::{
App, AppContext, Bounds, Context, FocusHandle, IntoElement, Render, Styled, TitlebarOptions,
Window, WindowBounds, WindowOptions, px, size,
@@ -42,7 +38,7 @@ fn main() {
log::error!("{error:#}");
std::process::exit(1);
}
Theme::set_mode_now(Mode::Dark, cx);
themes::apply(cx);
let bounds = Bounds::centered(None, size(px(1200.0), px(760.0)), cx);
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
@@ -54,6 +50,9 @@ fn main() {
};
cx.open_window(options, |window, cx| {
cx.new(|cx| {
window
.observe_window_appearance(|_, cx| themes::apply(cx))
.detach();
let focus = cx.focus_handle();
window.focus(&focus, cx);
Workspace { focus }
+24 -8
View File
@@ -4,6 +4,8 @@ use std::ops::RangeInclusive;
use serde::{Deserialize, Serialize};
use crate::themes;
pub use store::{SettingsStore, load, save};
/// Where a new session starts.
@@ -107,7 +109,7 @@ pub const PADDING: RangeInclusive<f32> = 0.0..=64.0;
pub const SCROLLBACK: RangeInclusive<u32> = 0..=100_000;
impl Settings {
/// Every value inside its range, every name filled; the error names the field.
/// Every value inside its range, every theme built in; the error names the field.
pub fn validate(&self) -> anyhow::Result<()> {
within("font_size", self.font_size, &FONT_SIZE)?;
within("line_height", self.line_height, &LINE_HEIGHT)?;
@@ -115,11 +117,17 @@ impl Settings {
within("padding_x", self.padding_x, &PADDING)?;
within("padding_y", self.padding_y, &PADDING)?;
within("scrollback", self.scrollback, &SCROLLBACK)?;
anyhow::ensure!(!self.theme.is_empty(), "theme is empty");
anyhow::ensure!(
self.dark_theme.as_ref().is_none_or(|name| !name.is_empty()),
"dark_theme is empty"
);
for (field, id) in [
("theme", Some(&self.theme)),
("dark_theme", self.dark_theme.as_ref()),
] {
if let Some(id) = id {
anyhow::ensure!(
themes::find(id).is_some(),
"{field} is {id:?}, no built-in theme"
);
}
}
anyhow::ensure!(!self.font_family.is_empty(), "font_family is empty");
Ok(())
}
@@ -177,14 +185,22 @@ mod tests {
}
#[test]
fn an_empty_name_fails() {
fn an_unknown_theme_fails() {
let settings = Settings {
dark_theme: Some(String::new()),
..Settings::default()
};
assert_eq!(
settings.validate().expect_err("empty").to_string(),
"dark_theme is empty"
"dark_theme is \"\", no built-in theme"
);
let settings = Settings {
theme: "mocha".into(),
..Settings::default()
};
assert_eq!(
settings.validate().expect_err("unknown").to_string(),
"theme is \"mocha\", no built-in theme"
);
}
}
+84
View File
@@ -0,0 +1,84 @@
use ely_gpui_component::theme::{Mix, Mode, Palette, Syntax, Theme};
use gpui::App;
use super::{TerminalTheme, active};
use crate::settings::SettingsStore;
/// The chrome's palette for a terminal theme; none for Ely's own, which Ely draws.
pub fn palette(theme: &TerminalTheme) -> Option<Palette> {
if theme.ely {
return None;
}
let (bg, fg, ansi) = (theme.background, theme.foreground, theme.ansi);
let base = if theme.is_dark() {
Palette::dark(false)
} else {
Palette::light(false)
};
let tint = |share: f32| bg.mix(&fg, share);
let wash = |color: gpui::Hsla| bg.mix(&color, 0.18);
Some(Palette {
bg,
surface: bg,
sunken: tint(0.03),
overlay: tint(0.07),
hover: tint(0.1),
active: tint(0.15),
border: tint(0.14),
border_strong: tint(0.26),
fg,
fg_muted: tint(0.8),
fg_subtle: tint(0.62),
fg_disabled: tint(0.36),
accent: fg,
accent_hover: tint(0.85),
on_accent: bg,
focus: ansi[4],
link: ansi[12],
selection: theme.selection_background.alpha(0.4),
success: ansi[2],
warning: ansi[3],
danger: ansi[1],
info: ansi[4],
success_subtle: wash(ansi[2]),
warning_subtle: wash(ansi[3]),
danger_subtle: wash(ansi[1]),
info_subtle: wash(ansi[4]),
glass: bg.alpha(base.glass.a),
tooltip_bg: tint(0.2),
tooltip_fg: fg,
chart: [
ansi[4], ansi[6], ansi[3], ansi[5], ansi[2], ansi[1], ansi[12], ansi[14],
],
ansi,
syntax: Syntax {
keyword: ansi[5],
string: ansi[2],
number: ansi[3],
comment: tint(0.62),
function: ansi[4],
type_name: ansi[6],
constant: ansi[3],
property: ansi[1],
tag: ansi[1],
attribute: ansi[3],
operator: tint(0.8),
punctuation: tint(0.8),
variable: fg,
},
..base
})
}
/// Dresses the app in the theme the settings choose for the system's appearance.
pub fn apply(cx: &mut App) {
let theme = active(SettingsStore::get(cx), cx.window_appearance());
let mode = if theme.is_dark() {
Mode::Dark
} else {
Mode::Light
};
Theme::set_palette(mode, palette(theme), cx);
Theme::set_mode_now(mode, cx);
log::info!("theme: {} in {mode:?}", theme.id);
}
+204
View File
@@ -0,0 +1,204 @@
mod chrome;
use std::sync::LazyLock;
use anyhow::Context as _;
use ely_gpui_component::theme::Palette;
use gpui::{Hsla, Rgba, WindowAppearance};
pub use chrome::{apply, palette};
use crate::settings::Settings;
/// What a terminal paints with. Ely's own themes keep Ely's palette for the chrome.
#[derive(Clone, Debug, PartialEq)]
pub struct TerminalTheme {
pub id: &'static str,
pub name: &'static str,
pub background: Hsla,
pub foreground: Hsla,
pub cursor: Hsla,
pub cursor_text: Hsla,
pub selection_background: Hsla,
/// None keeps each cell's own color.
pub selection_foreground: Option<Hsla>,
pub ansi: [Hsla; 16],
pub ely: bool,
}
impl TerminalTheme {
pub fn is_dark(&self) -> bool {
self.background.l < 0.5
}
}
/// Upstream Ghostty files from iTerm2-Color-Schemes: id, shown name, text.
const FILES: [(&str, &str, &str); 12] = [
(
"catppuccin-mocha",
"Catppuccin Mocha",
include_str!("../../assets/themes/catppuccin-mocha"),
),
(
"catppuccin-macchiato",
"Catppuccin Macchiato",
include_str!("../../assets/themes/catppuccin-macchiato"),
),
(
"catppuccin-frappe",
"Catppuccin Frappé",
include_str!("../../assets/themes/catppuccin-frappe"),
),
(
"catppuccin-latte",
"Catppuccin Latte",
include_str!("../../assets/themes/catppuccin-latte"),
),
(
"tokyo-night",
"Tokyo Night",
include_str!("../../assets/themes/tokyo-night"),
),
(
"dracula",
"Dracula",
include_str!("../../assets/themes/dracula"),
),
("nord", "Nord", include_str!("../../assets/themes/nord")),
(
"gruvbox-dark",
"Gruvbox Dark",
include_str!("../../assets/themes/gruvbox-dark"),
),
(
"rose-pine",
"Rosé Pine",
include_str!("../../assets/themes/rose-pine"),
),
(
"solarized-light",
"Solarized Light",
include_str!("../../assets/themes/solarized-light"),
),
(
"one-light",
"One Light",
include_str!("../../assets/themes/one-light"),
),
(
"kanagawa-wave",
"Kanagawa Wave",
include_str!("../../assets/themes/kanagawa-wave"),
),
];
static THEMES: LazyLock<Vec<TerminalTheme>> = LazyLock::new(|| {
let mut themes = vec![
ely("ely-dark", "Ely Dark", Palette::dark(false)),
ely("ely-light", "Ely Light", Palette::light(false)),
];
themes.extend(FILES.iter().map(|(id, name, text)| {
parse(id, name, text).unwrap_or_else(|error| panic!("bundled theme {id}: {error:#}"))
}));
themes
});
/// Every built-in theme, Ely's first.
pub fn all() -> &'static [TerminalTheme] {
&THEMES
}
pub fn find(id: &str) -> Option<&'static TerminalTheme> {
all().iter().find(|theme| theme.id == id)
}
/// The theme the settings choose under the system's appearance.
pub fn active(settings: &Settings, appearance: WindowAppearance) -> &'static TerminalTheme {
let dark = matches!(
appearance,
WindowAppearance::Dark | WindowAppearance::VibrantDark
);
let id = match (&settings.dark_theme, dark) {
(Some(id), true) => id,
_ => &settings.theme,
};
find(id).unwrap_or_else(|| panic!("settings name theme {id}, which validation admits"))
}
fn ely(id: &'static str, name: &'static str, palette: Palette) -> TerminalTheme {
TerminalTheme {
id,
name,
background: palette.bg,
foreground: palette.fg,
cursor: palette.focus,
cursor_text: palette.bg,
selection_background: palette.selection,
selection_foreground: None,
ansi: palette.ansi,
ely: true,
}
}
/// Reads Ghostty's `key = value` theme format; every key it needs must be there.
fn parse(id: &'static str, name: &'static str, text: &str) -> anyhow::Result<TerminalTheme> {
let mut ansi: [Option<Hsla>; 16] = [None; 16];
let mut keys: Vec<(&str, Hsla)> = Vec::new();
for line in text.lines().map(str::trim).filter(|line| !line.is_empty()) {
let (key, value) = line
.split_once('=')
.with_context(|| format!("no `=` in `{line}`"))?;
let (key, value) = (key.trim(), value.trim());
if key == "palette" {
let (index, color) = value
.split_once('=')
.with_context(|| format!("no index in `{line}`"))?;
let index: usize = index.trim().parse().with_context(|| format!("`{line}`"))?;
anyhow::ensure!(index < 16, "palette index {index} past 15");
ansi[index] = Some(hex(color.trim())?);
} else {
keys.push((key, hex(value)?));
}
}
let get = |key: &str| {
keys.iter()
.find(|(name, _)| *name == key)
.map(|(_, color)| *color)
.with_context(|| format!("no `{key}`"))
};
let mut colors = [Hsla::default(); 16];
for (index, color) in ansi.iter().enumerate() {
colors[index] = color.with_context(|| format!("no palette {index}"))?;
}
Ok(TerminalTheme {
id,
name,
background: get("background")?,
foreground: get("foreground")?,
cursor: get("cursor-color")?,
cursor_text: get("cursor-text")?,
selection_background: get("selection-background")?,
selection_foreground: Some(get("selection-foreground")?),
ansi: colors,
ely: false,
})
}
fn hex(text: &str) -> anyhow::Result<Hsla> {
let digits = text
.strip_prefix('#')
.with_context(|| format!("`{text}` is not #rrggbb"))?;
anyhow::ensure!(digits.len() == 6, "`{text}` is not #rrggbb");
let value = u32::from_str_radix(digits, 16).with_context(|| format!("`{text}`"))?;
let channel = |shift: u32| ((value >> shift) & 0xff) as f32 / 255.0;
Ok(Rgba {
r: channel(16),
g: channel(8),
b: channel(0),
a: 1.0,
}
.into())
}
#[cfg(test)]
mod tests;
+124
View File
@@ -0,0 +1,124 @@
use gpui::{Rgba, WindowAppearance};
use super::*;
fn rgb(hex: u32) -> Hsla {
let channel = |shift: u32| ((hex >> shift) & 0xff) as f32 / 255.0;
Rgba {
r: channel(16),
g: channel(8),
b: channel(0),
a: 1.0,
}
.into()
}
#[test]
fn every_bundled_file_parses_with_unique_ids() {
assert_eq!(all().len(), 14);
let mut ids: Vec<_> = all().iter().map(|theme| theme.id).collect();
ids.sort();
ids.dedup();
assert_eq!(ids.len(), 14);
let folder = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/themes");
for entry in std::fs::read_dir(folder).expect("themes folder") {
let name = entry
.expect("entry")
.file_name()
.into_string()
.expect("utf-8");
if name != "LICENSE" {
assert!(find(&name).is_some(), "{name} is bundled but not listed");
}
}
}
#[test]
fn catppuccin_mocha_reads_its_upstream_colors() {
let mocha = find("catppuccin-mocha").expect("mocha");
assert_eq!(mocha.background, rgb(0x1e1e2e));
assert_eq!(mocha.foreground, rgb(0xcdd6f4));
assert_eq!(mocha.cursor, rgb(0xf5e0dc));
assert_eq!(mocha.ansi[1], rgb(0xf38ba8));
assert_eq!(mocha.ansi[15], rgb(0xa6adc8));
assert_eq!(mocha.selection_foreground, Some(rgb(0x1e1e2e)));
}
#[test]
fn light_and_dark_follow_the_background() {
let light: Vec<_> = all()
.iter()
.filter(|theme| !theme.is_dark())
.map(|theme| theme.id)
.collect();
assert_eq!(
light,
[
"ely-light",
"catppuccin-latte",
"solarized-light",
"one-light"
]
);
}
#[test]
fn a_missing_key_or_bad_color_names_it() {
let text = include_str!("../../assets/themes/nord");
let cut: String = text
.lines()
.filter(|line| !line.starts_with("cursor-text"))
.collect::<Vec<_>>()
.join("\n");
let error = parse("x", "X", &cut).expect_err("no cursor-text");
assert_eq!(error.to_string(), "no `cursor-text`");
let cut: String = text
.lines()
.filter(|line| !line.starts_with("palette = 7="))
.collect::<Vec<_>>()
.join("\n");
assert_eq!(
parse("x", "X", &cut).expect_err("no 7").to_string(),
"no palette 7"
);
let bad = text.replace("background = #", "background = ");
assert!(parse("x", "X", &bad).is_err());
assert!(parse("x", "X", "palette = 16=#000000").is_err());
}
#[test]
fn the_dark_theme_wins_only_in_dark_mode() {
let settings = Settings {
theme: "catppuccin-latte".into(),
dark_theme: Some("catppuccin-mocha".into()),
..Settings::default()
};
assert_eq!(
active(&settings, WindowAppearance::Light).id,
"catppuccin-latte"
);
assert_eq!(
active(&settings, WindowAppearance::VibrantDark).id,
"catppuccin-mocha"
);
let single = Settings {
dark_theme: None,
..settings
};
assert_eq!(
active(&single, WindowAppearance::Dark).id,
"catppuccin-latte"
);
}
#[test]
fn the_chrome_wears_the_theme() {
let dracula = find("dracula").expect("dracula");
let chrome = palette(dracula).expect("a mapped palette");
assert_eq!(chrome.bg, dracula.background);
assert_eq!(chrome.fg, dracula.foreground);
assert_eq!(chrome.ansi, dracula.ansi);
assert_eq!(chrome.danger, dracula.ansi[1]);
assert!(chrome.glass.a < 1.0);
assert!(palette(find("ely-dark").expect("ely")).is_none());
}