T03: built-in themes, chrome palette, dark-mode choice
This commit is contained in:
20 files changed
+732
-13
No files matched your search
@@ -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/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/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
|
## Rules
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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 +1,2 @@
|
|||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
pub mod themes;
|
||||||
+6
-7
@@ -1,9 +1,5 @@
|
|||||||
use ely_gpui_component::{
|
use ely_gpui_component::{Assets, primitives::FocusScope, theme::ActiveTheme};
|
||||||
Assets,
|
use elyterminal::{settings::SettingsStore, themes};
|
||||||
primitives::FocusScope,
|
|
||||||
theme::{ActiveTheme, Mode, Theme},
|
|
||||||
};
|
|
||||||
use elyterminal::settings::SettingsStore;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, Bounds, Context, FocusHandle, IntoElement, Render, Styled, TitlebarOptions,
|
App, AppContext, Bounds, Context, FocusHandle, IntoElement, Render, Styled, TitlebarOptions,
|
||||||
Window, WindowBounds, WindowOptions, px, size,
|
Window, WindowBounds, WindowOptions, px, size,
|
||||||
@@ -42,7 +38,7 @@ fn main() {
|
|||||||
log::error!("{error:#}");
|
log::error!("{error:#}");
|
||||||
std::process::exit(1);
|
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 bounds = Bounds::centered(None, size(px(1200.0), px(760.0)), cx);
|
||||||
let options = WindowOptions {
|
let options = WindowOptions {
|
||||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||||
@@ -54,6 +50,9 @@ fn main() {
|
|||||||
};
|
};
|
||||||
cx.open_window(options, |window, cx| {
|
cx.open_window(options, |window, cx| {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
|
window
|
||||||
|
.observe_window_appearance(|_, cx| themes::apply(cx))
|
||||||
|
.detach();
|
||||||
let focus = cx.focus_handle();
|
let focus = cx.focus_handle();
|
||||||
window.focus(&focus, cx);
|
window.focus(&focus, cx);
|
||||||
Workspace { focus }
|
Workspace { focus }
|
||||||
|
|||||||
+22
-6
@@ -4,6 +4,8 @@ use std::ops::RangeInclusive;
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::themes;
|
||||||
|
|
||||||
pub use store::{SettingsStore, load, save};
|
pub use store::{SettingsStore, load, save};
|
||||||
|
|
||||||
/// Where a new session starts.
|
/// 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;
|
pub const SCROLLBACK: RangeInclusive<u32> = 0..=100_000;
|
||||||
|
|
||||||
impl Settings {
|
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<()> {
|
pub fn validate(&self) -> anyhow::Result<()> {
|
||||||
within("font_size", self.font_size, &FONT_SIZE)?;
|
within("font_size", self.font_size, &FONT_SIZE)?;
|
||||||
within("line_height", self.line_height, &LINE_HEIGHT)?;
|
within("line_height", self.line_height, &LINE_HEIGHT)?;
|
||||||
@@ -115,11 +117,17 @@ impl Settings {
|
|||||||
within("padding_x", self.padding_x, &PADDING)?;
|
within("padding_x", self.padding_x, &PADDING)?;
|
||||||
within("padding_y", self.padding_y, &PADDING)?;
|
within("padding_y", self.padding_y, &PADDING)?;
|
||||||
within("scrollback", self.scrollback, &SCROLLBACK)?;
|
within("scrollback", self.scrollback, &SCROLLBACK)?;
|
||||||
anyhow::ensure!(!self.theme.is_empty(), "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!(
|
anyhow::ensure!(
|
||||||
self.dark_theme.as_ref().is_none_or(|name| !name.is_empty()),
|
themes::find(id).is_some(),
|
||||||
"dark_theme is empty"
|
"{field} is {id:?}, no built-in theme"
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
anyhow::ensure!(!self.font_family.is_empty(), "font_family is empty");
|
anyhow::ensure!(!self.font_family.is_empty(), "font_family is empty");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -177,14 +185,22 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn an_empty_name_fails() {
|
fn an_unknown_theme_fails() {
|
||||||
let settings = Settings {
|
let settings = Settings {
|
||||||
dark_theme: Some(String::new()),
|
dark_theme: Some(String::new()),
|
||||||
..Settings::default()
|
..Settings::default()
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
settings.validate().expect_err("empty").to_string(),
|
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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
Reference in new issue
Block a user