T27: on Omarchy 4 an Omarchy theme wears the system's current theme, read from the ghostty.conf Omarchy renders, and follows each switch; it is the default where Omarchy runs, and its card leads the Appearance grid
This commit is contained in:
13 files changed
+233
-29
No files matched your search
@@ -64,6 +64,8 @@ pub fn start(cx: &mut App) -> anyhow::Result<()> {
|
||||
mac::repeat_held_keys();
|
||||
mac::deliver_command_key_ups()?;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
themes::omarchy::start(cx)?;
|
||||
themes::apply(cx);
|
||||
let chosen = |settings: &settings::Settings| {
|
||||
let theme = (settings.theme.clone(), settings.dark_theme.clone());
|
||||
|
||||
+2
-5
@@ -82,7 +82,7 @@ impl Default for Settings {
|
||||
Self {
|
||||
shell: String::new(),
|
||||
start_in: StartIn::Home,
|
||||
theme: "catppuccin-mocha".into(),
|
||||
theme: themes::default_id().into(),
|
||||
dark_theme: None,
|
||||
font_family: "JetBrains Mono".into(),
|
||||
font_size: 13.0,
|
||||
@@ -124,10 +124,7 @@ impl Settings {
|
||||
("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!(themes::known(id), "{field} is {id:?}, no built-in theme");
|
||||
}
|
||||
}
|
||||
anyhow::ensure!(!self.font_family.is_empty(), "font_family is empty");
|
||||
|
||||
@@ -103,7 +103,7 @@ impl SettingsWindow {
|
||||
settings.cursor_style,
|
||||
|settings, style| settings.cursor_style = style,
|
||||
);
|
||||
let active = themes::active(settings, window.appearance());
|
||||
let active = themes::active(settings, window.appearance(), cx);
|
||||
let lit = self.lit || !settings.cursor_blink;
|
||||
let cursor = SettingsSection::new("Cursor")
|
||||
.row(SettingsRow::new("Style").control(style))
|
||||
@@ -113,7 +113,7 @@ impl SettingsWindow {
|
||||
settings.cursor_blink,
|
||||
|settings, on| settings.cursor_blink = on,
|
||||
)))
|
||||
.row(SettingsRow::new("Preview").control(prompt(settings, active, lit, cx)));
|
||||
.row(SettingsRow::new("Preview").control(prompt(settings, &active, lit, cx)));
|
||||
let opacity = percent(settings.background_opacity);
|
||||
let slider = Slider::new("background_opacity", opacity)
|
||||
.range(percent(*OPACITY.start()), percent(*OPACITY.end()))
|
||||
@@ -207,7 +207,7 @@ fn fraction(percent: f64) -> f32 {
|
||||
(percent / 100.) as f32
|
||||
}
|
||||
|
||||
/// Every built-in theme as a card over its preview; `dark` picks the one a dark system wears.
|
||||
/// Omarchy's theme where followed, then every built-in, as cards; `dark` picks the dark system's.
|
||||
fn grid(
|
||||
this: &Entity<SettingsWindow>,
|
||||
title: Option<&'static str>,
|
||||
@@ -222,7 +222,8 @@ fn grid(
|
||||
} else {
|
||||
("theme", Some(settings.theme.as_str()))
|
||||
};
|
||||
let cards = themes::all().iter().map(|each| {
|
||||
let omarchy = cx.try_global::<themes::Omarchy>().map(|omarchy| &omarchy.0);
|
||||
let cards = omarchy.into_iter().chain(themes::all()).map(|each| {
|
||||
let (id, this) = (each.id, this.clone());
|
||||
let key = SharedString::from(format!("{field}-{id}"));
|
||||
RadioCard::new(key, chosen == Some(id), each.name)
|
||||
|
||||
+4
-4
@@ -243,9 +243,9 @@ impl TerminalView {
|
||||
self.pty.pid
|
||||
),
|
||||
Event::ColorRequest(index, reply) => {
|
||||
let theme = themes::active(SettingsStore::get(cx), cx.window_appearance());
|
||||
let theme = themes::active(SettingsStore::get(cx), cx.window_appearance(), cx);
|
||||
let term = self.pty.term.lock();
|
||||
let rgb = Ink::new(theme, term.colors()).query(index, term.colors());
|
||||
let rgb = Ink::new(&theme, term.colors()).query(index, term.colors());
|
||||
drop(term);
|
||||
self.pty.write(reply(rgb).into_bytes());
|
||||
}
|
||||
@@ -322,9 +322,9 @@ fn look(settings: &Settings, points: f32, ink: &Ink, window: &Window) -> anyhow:
|
||||
impl Render for TerminalView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = SettingsStore::get(cx).clone();
|
||||
let theme = themes::active(&settings, window.appearance());
|
||||
let theme = themes::active(&settings, window.appearance(), cx);
|
||||
let term = self.pty.term.lock();
|
||||
let ink = Ink::new(theme, term.colors());
|
||||
let ink = Ink::new(&theme, term.colors());
|
||||
let lit = self.finder.as_ref().map_or_else(Vec::new, |finder| {
|
||||
finder.lit(&term, cx.theme().colors.warning)
|
||||
});
|
||||
|
||||
@@ -76,14 +76,14 @@ pub fn palette(theme: &TerminalTheme, opacity: f32) -> Palette {
|
||||
/// Dresses the app in the theme the settings choose for the system's appearance.
|
||||
pub fn apply(cx: &mut App) {
|
||||
let settings = SettingsStore::get(cx);
|
||||
let theme = active(settings, cx.window_appearance());
|
||||
let theme = active(settings, cx.window_appearance(), cx);
|
||||
let opacity = settings.background_opacity;
|
||||
let mode = if theme.is_dark() {
|
||||
Mode::Dark
|
||||
} else {
|
||||
Mode::Light
|
||||
};
|
||||
Theme::set_palette(mode, Some(palette(theme, opacity)), cx);
|
||||
Theme::set_palette(mode, Some(palette(&theme, opacity)), cx);
|
||||
Theme::set_mode_now(mode, cx);
|
||||
log::info!("theme: {} in {mode:?}, glass at {opacity}", theme.id);
|
||||
}
|
||||
+39
-5
@@ -1,10 +1,12 @@
|
||||
mod chrome;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod omarchy;
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use ely_gpui_component::theme::Palette;
|
||||
use gpui::{Hsla, Rgba, WindowAppearance};
|
||||
use gpui::{App, Global, Hsla, Rgba, WindowAppearance};
|
||||
|
||||
pub use chrome::{apply, palette};
|
||||
|
||||
@@ -32,6 +34,14 @@ impl TerminalTheme {
|
||||
}
|
||||
}
|
||||
|
||||
/// The settings' name for Omarchy's theme, on Linux.
|
||||
pub const OMARCHY: &str = "omarchy";
|
||||
|
||||
/// Omarchy's current theme, while ELYTerminal follows it.
|
||||
pub struct Omarchy(pub TerminalTheme);
|
||||
|
||||
impl Global for Omarchy {}
|
||||
|
||||
/// Upstream Ghostty files from iTerm2-Color-Schemes: id, shown name, text.
|
||||
const FILES: [(&str, &str, &str); 12] = [
|
||||
(
|
||||
@@ -112,17 +122,41 @@ pub fn find(id: &str) -> Option<&'static TerminalTheme> {
|
||||
all().iter().find(|theme| theme.id == id)
|
||||
}
|
||||
|
||||
/// A theme the settings may name: built in, or Omarchy's on Linux.
|
||||
pub fn known(id: &str) -> bool {
|
||||
find(id).is_some() || (cfg!(target_os = "linux") && id == OMARCHY)
|
||||
}
|
||||
|
||||
/// A fresh install wears Omarchy's theme where Omarchy runs.
|
||||
pub fn default_id() -> &'static str {
|
||||
#[cfg(target_os = "linux")]
|
||||
if omarchy::path().is_some_and(|path| path.is_file()) {
|
||||
return OMARCHY;
|
||||
}
|
||||
"catppuccin-mocha"
|
||||
}
|
||||
|
||||
/// The theme the settings choose under the system's appearance.
|
||||
pub fn active(settings: &Settings, appearance: WindowAppearance) -> &'static TerminalTheme {
|
||||
pub fn chosen(settings: &Settings, appearance: WindowAppearance) -> &str {
|
||||
let dark = matches!(
|
||||
appearance,
|
||||
WindowAppearance::Dark | WindowAppearance::VibrantDark
|
||||
);
|
||||
let id = match (&settings.dark_theme, dark) {
|
||||
match (&settings.dark_theme, dark) {
|
||||
(Some(id), true) => id,
|
||||
_ => &settings.theme,
|
||||
};
|
||||
find(id).unwrap_or_else(|| panic!("settings name theme {id}, which validation admits"))
|
||||
}
|
||||
}
|
||||
|
||||
/// The chosen theme's colors; Omarchy's come from its global.
|
||||
pub fn active(settings: &Settings, appearance: WindowAppearance, cx: &App) -> TerminalTheme {
|
||||
let id = chosen(settings, appearance);
|
||||
if id == OMARCHY {
|
||||
return cx.global::<Omarchy>().0.clone();
|
||||
}
|
||||
find(id)
|
||||
.unwrap_or_else(|| panic!("settings name theme {id}, which validation admits"))
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn ely(id: &'static str, name: &'static str, palette: Palette) -> TerminalTheme {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
use std::{
|
||||
os::unix::fs::MetadataExt,
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use gpui::App;
|
||||
|
||||
use super::{OMARCHY, Omarchy, TerminalTheme, apply, parse};
|
||||
use crate::settings::SettingsStore;
|
||||
|
||||
/// How often the theme folder is checked for a switch.
|
||||
const CHECK: Duration = Duration::from_millis(500);
|
||||
|
||||
/// A Ghostty theme file's keys; Omarchy's other lines pass by.
|
||||
const COLORS: [&str; 7] = [
|
||||
"palette",
|
||||
"background",
|
||||
"foreground",
|
||||
"cursor-color",
|
||||
"cursor-text",
|
||||
"selection-background",
|
||||
"selection-foreground",
|
||||
];
|
||||
|
||||
/// Omarchy 4's current theme for Ghostty, under $HOME whatever XDG_STATE_HOME says.
|
||||
pub fn path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|home| home.join(".local/state/omarchy/current/theme/ghostty.conf"))
|
||||
}
|
||||
|
||||
/// Follows Omarchy's theme; a failed read stops the start only if worn.
|
||||
pub fn start(cx: &mut App) -> anyhow::Result<()> {
|
||||
let settings = SettingsStore::get(cx);
|
||||
let worn = settings.theme == OMARCHY || settings.dark_theme.as_deref() == Some(OMARCHY);
|
||||
let path = path().context("omarchy: no home folder")?;
|
||||
match read(&path) {
|
||||
Ok(theme) => {
|
||||
log::info!("omarchy: the theme follows {}", path.display());
|
||||
cx.set_global(Omarchy(theme));
|
||||
watch(path, cx);
|
||||
Ok(())
|
||||
}
|
||||
Err(error) if worn => Err(error.context("settings wear the Omarchy theme")),
|
||||
Err(error) if path.exists() => {
|
||||
log::error!("{error:#}; no Omarchy theme to follow");
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => {
|
||||
log::info!("omarchy: none at {}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A switch replaces the folder, so the file's identity changes.
|
||||
fn stamp(path: &Path) -> Option<(u64, u64, i64, i64, u64)> {
|
||||
let meta = std::fs::metadata(path).ok()?;
|
||||
Some((
|
||||
meta.dev(),
|
||||
meta.ino(),
|
||||
meta.mtime(),
|
||||
meta.mtime_nsec(),
|
||||
meta.len(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Omarchy signals only Alacritty, kitty and Ghostty, so this checks.
|
||||
fn watch(path: PathBuf, cx: &mut App) {
|
||||
let mut seen = stamp(&path);
|
||||
cx.spawn(async move |cx| {
|
||||
loop {
|
||||
cx.background_executor().timer(CHECK).await;
|
||||
let now = stamp(&path);
|
||||
// Gone mid-switch: the next check finds the new folder.
|
||||
if now.is_none() || now == seen {
|
||||
continue;
|
||||
}
|
||||
seen = now;
|
||||
match read(&path) {
|
||||
Ok(theme) => cx.update(|cx| {
|
||||
log::info!("omarchy: the theme changed");
|
||||
cx.set_global(Omarchy(theme));
|
||||
apply(cx);
|
||||
}),
|
||||
Err(error) => log::error!("{error:#}; the last Omarchy theme stays"),
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn read(path: &Path) -> anyhow::Result<TerminalTheme> {
|
||||
let text = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("omarchy: reading {}", path.display()))?;
|
||||
colors(&text).with_context(|| format!("omarchy: {}", path.display()))
|
||||
}
|
||||
|
||||
/// Omarchy writes no cursor-text; the background stands in, as Ghostty's default.
|
||||
pub(super) fn colors(text: &str) -> anyhow::Result<TerminalTheme> {
|
||||
let key = |line: &str| {
|
||||
line.split_once('=')
|
||||
.map_or("", |(key, _)| key.trim())
|
||||
.to_owned()
|
||||
};
|
||||
let mut lines: Vec<&str> = text
|
||||
.lines()
|
||||
.filter(|line| COLORS.contains(&key(line).as_str()))
|
||||
.collect();
|
||||
let background = lines
|
||||
.iter()
|
||||
.find(|line| key(line) == "background")
|
||||
.and_then(|line| line.split_once('='))
|
||||
.map(|(_, value)| format!("cursor-text = {}", value.trim()));
|
||||
if !lines.iter().any(|line| key(line) == "cursor-text") {
|
||||
lines.extend(background.as_deref());
|
||||
}
|
||||
parse(OMARCHY, "Omarchy", &lines.join("\n"))
|
||||
}
|
||||
+49
-6
@@ -86,6 +86,52 @@ fn a_missing_key_or_bad_color_names_it() {
|
||||
assert!(parse("x", "X", "palette = 16=#000000").is_err());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn omarchys_ghostty_conf_reads_without_its_glass() {
|
||||
// Omarchy 4 renders Tokyo Night so; glassy windows append the last four lines.
|
||||
let rendered = [
|
||||
"background = #1a1b26",
|
||||
"foreground = #a9b1d6",
|
||||
"cursor-color = #c0caf5",
|
||||
"selection-background = #292e42",
|
||||
"selection-foreground = #c0caf5",
|
||||
"",
|
||||
"palette = 0=#1a1b26",
|
||||
"palette = 1=#f7768e",
|
||||
"palette = 2=#9ece6a",
|
||||
"palette = 3=#e0af68",
|
||||
"palette = 4=#7aa2f7",
|
||||
"palette = 5=#ad8ee6",
|
||||
"palette = 6=#449dab",
|
||||
"palette = 7=#a9b1d6",
|
||||
"palette = 8=#414868",
|
||||
"palette = 9=#ff7a93",
|
||||
"palette = 10=#b9f27c",
|
||||
"palette = 11=#ff9e64",
|
||||
"palette = 12=#7da6ff",
|
||||
"palette = 13=#bb9af7",
|
||||
"palette = 14=#0db9d7",
|
||||
"palette = 15=#c0caf5",
|
||||
"# omarchy-glass begin",
|
||||
"background-opacity = 0.65",
|
||||
"gtk-custom-css = /usr/share/omarchy/default/gtk/glass.css",
|
||||
"# omarchy-glass end",
|
||||
]
|
||||
.join("\n");
|
||||
let theme = omarchy::colors(&rendered).expect("Omarchy's file reads");
|
||||
assert_eq!(theme.id, OMARCHY);
|
||||
assert_eq!(theme.background, rgb(0x1a1b26));
|
||||
assert_eq!(theme.cursor, rgb(0xc0caf5));
|
||||
assert_eq!(theme.cursor_text, theme.background, "Ghostty's default");
|
||||
assert_eq!(theme.ansi[8], rgb(0x414868));
|
||||
let cut = rendered.replace("palette = 7=#a9b1d6\n", "");
|
||||
assert_eq!(
|
||||
omarchy::colors(&cut).expect_err("no 7").to_string(),
|
||||
"no palette 7"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_dark_theme_wins_only_in_dark_mode() {
|
||||
let settings = Settings {
|
||||
@@ -94,21 +140,18 @@ fn the_dark_theme_wins_only_in_dark_mode() {
|
||||
..Settings::default()
|
||||
};
|
||||
assert_eq!(
|
||||
active(&settings, WindowAppearance::Light).id,
|
||||
chosen(&settings, WindowAppearance::Light),
|
||||
"catppuccin-latte"
|
||||
);
|
||||
assert_eq!(
|
||||
active(&settings, WindowAppearance::VibrantDark).id,
|
||||
chosen(&settings, WindowAppearance::VibrantDark),
|
||||
"catppuccin-mocha"
|
||||
);
|
||||
let single = Settings {
|
||||
dark_theme: None,
|
||||
..settings
|
||||
};
|
||||
assert_eq!(
|
||||
active(&single, WindowAppearance::Dark).id,
|
||||
"catppuccin-latte"
|
||||
);
|
||||
assert_eq!(chosen(&single, WindowAppearance::Dark), "catppuccin-latte");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user