T27: Omarchy's theme file is read twice a second and worn whenever its text changes and parses, so a file made readable again is worn; a failed check is logged once while it lasts; only a missing file means no Omarchy: one that cannot be read stops the start, and a fresh install wears Omarchy's theme unless its file is surely absent
CI / platforms (ubuntu-24.04) (push) Successful in 1m2s
CI / macos (push) Successful in 12m7s
CI / platforms (windows-latest) (push) Canceled after 0s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-16 01:03:06 -04:00
1 parent d8ebadea2d
commit aae7fba6a9
4 files changed
+99 -49

No files matched your search

+2 -2
View File
@@ -127,10 +127,10 @@ 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.
/// A fresh install wears Omarchy's theme unless its file is surely absent.
pub fn default_id() -> &'static str {
#[cfg(target_os = "linux")]
if omarchy::path().is_some_and(|path| path.is_file()) {
if omarchy::path().is_some_and(|path| !matches!(path.try_exists(), Ok(false))) {
return OMARCHY;
}
"catppuccin-mocha"
+53 -41
View File
@@ -1,5 +1,6 @@
use std::{
os::unix::fs::MetadataExt,
fs,
io::{self, ErrorKind},
path::{Path, PathBuf},
time::Duration,
};
@@ -10,7 +11,7 @@ use gpui::App;
use super::{OMARCHY, Omarchy, TerminalTheme, apply, parse};
use crate::settings::SettingsStore;
/// How often the theme folder is checked for a switch.
/// How often Omarchy's theme file is read for a switch.
const CHECK: Duration = Duration::from_millis(500);
/// A Ghostty theme file's keys; Omarchy's other lines pass by.
@@ -29,71 +30,82 @@ 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.
/// Follows Omarchy's theme; an unreadable file, or a bad one worn, stops the start.
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) => {
Ok((text, theme)) => {
log::info!("omarchy: the theme follows {}", path.display());
cx.set_global(Omarchy(theme));
watch(path, cx);
watch(path, text, 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");
Err(error) if missing(&error) => {
log::info!("omarchy: none at {}", path.display());
Ok(())
}
Err(_) => {
log::info!("omarchy: none at {}", path.display());
Err(error) if error.is::<io::Error>() => Err(error),
Err(error) => {
log::error!("{error:#}; no Omarchy theme to follow");
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);
/// Omarchy signals only Alacritty, kitty and Ghostty, so this reads the file.
fn watch(path: PathBuf, mut shown: String, cx: &mut App) {
cx.spawn(async move |cx| {
let mut failed = String::new();
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"),
match check(&path, &shown) {
Ok(None) => failed.clear(),
Ok(Some((text, theme))) => {
shown = text;
failed.clear();
cx.update(|cx| {
log::info!("omarchy: the theme changed");
cx.set_global(Omarchy(theme));
apply(cx);
});
}
Err(error) => {
let error = format!("{error:#}");
if error != failed {
log::error!("{error}; the last Omarchy theme stays");
failed = error;
}
}
}
}
})
.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()))
/// The file's theme when its text is not `shown`'s; none while unchanged.
pub(super) fn check(path: &Path, shown: &str) -> anyhow::Result<Option<(String, TerminalTheme)>> {
match read(path) {
Ok((text, _)) if text == shown => Ok(None),
// Gone mid-switch: the next check finds the new folder.
Err(error) if missing(&error) => Ok(None),
next => next.map(Some),
}
}
fn read(path: &Path) -> anyhow::Result<(String, TerminalTheme)> {
let text =
fs::read_to_string(path).with_context(|| format!("omarchy: reading {}", path.display()))?;
let theme = colors(&text).with_context(|| format!("omarchy: {}", path.display()))?;
Ok((text, theme))
}
fn missing(error: &anyhow::Error) -> bool {
error
.downcast_ref::<io::Error>()
.is_some_and(|error| error.kind() == ErrorKind::NotFound)
}
/// Omarchy writes no cursor-text; the background stands in, as Ghostty's default.
+43 -5
View File
@@ -86,11 +86,10 @@ fn a_missing_key_or_bad_color_names_it() {
assert!(parse("x", "X", "palette = 16=#000000").is_err());
}
/// Omarchy 4 renders Tokyo Night so; glass mode appends the last four lines.
#[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 = [
fn tokyo_night() -> String {
[
"background = #1a1b26",
"foreground = #a9b1d6",
"cursor-color = #c0caf5",
@@ -118,7 +117,13 @@ fn omarchys_ghostty_conf_reads_without_its_glass() {
"gtk-custom-css = /usr/share/omarchy/default/gtk/glass.css",
"# omarchy-glass end",
]
.join("\n");
.join("\n")
}
#[cfg(target_os = "linux")]
#[test]
fn omarchys_ghostty_conf_reads_without_its_glass() {
let rendered = tokyo_night();
let theme = omarchy::colors(&rendered).expect("Omarchy's file reads");
assert_eq!(theme.id, OMARCHY);
assert_eq!(theme.background, rgb(0x1a1b26));
@@ -132,6 +137,39 @@ fn omarchys_ghostty_conf_reads_without_its_glass() {
);
}
#[cfg(target_os = "linux")]
#[test]
fn omarchys_file_is_worn_again_once_it_reads() {
use std::{fs, os::unix::fs::PermissionsExt};
let folder = std::env::temp_dir().join(format!("elyterminal-omarchy-{}", std::process::id()));
if folder.exists() {
fs::remove_dir_all(&folder).expect("a stale test folder goes");
}
fs::create_dir_all(&folder).expect("a test folder");
let path = folder.join("ghostty.conf");
let text = tokyo_night();
fs::write(&path, &text).expect("a theme");
assert!(omarchy::check(&path, &text).expect("reads").is_none());
fs::set_permissions(&path, fs::Permissions::from_mode(0o000)).expect("000");
// Root reads any mode.
if unsafe { libc::geteuid() } != 0 {
let error = omarchy::check(&path, "").expect_err("unreadable");
assert!(
format!("{error:#}").contains("Permission denied"),
"{error:#}"
);
}
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("644");
let (shown, theme) = omarchy::check(&path, "").expect("reads").expect("a theme");
assert_eq!((shown, theme.background), (text, rgb(0x1a1b26)));
fs::remove_dir_all(&folder).expect("the test folder goes");
assert!(
omarchy::check(&path, "")
.expect("gone mid-switch")
.is_none()
);
}
#[test]
fn the_dark_theme_wins_only_in_dark_mode() {
let settings = Settings {