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
CI / platforms (ubuntu-24.04) (push) Successful in 1m35s
CI / macos (push) Successful in 11m19s
CI / platforms (windows-latest) (push) Canceled after 0s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-16 00:10:37 -04:00
1 parent 2e9ed4de75
commit 340c7a92ea
13 files changed
+233 -29

No files matched your search

+2
View File
@@ -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
View File
@@ -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");
+5 -4
View File
@@ -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
View File
@@ -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)
});
+2 -2
View File
@@ -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
View File
@@ -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 {
+119
View File
@@ -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
View File
@@ -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]