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 -1
View File
@@ -32,7 +32,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- `build.rs`: on Windows, the icon into the executable as resource 1, through `assets/icons/elyterminal.rc`. `.cargo/config.toml`: Windows builds carry the C runtime. `rust-toolchain.toml`: the toolchain rustup installs. - `build.rs`: on Windows, the icon into the executable as resource 1, through `assets/icons/elyterminal.rc`. `.cargo/config.toml`: Windows builds carry the C runtime. `rust-toolchain.toml`: the toolchain rustup installs.
- `src/main.rs`: start. `src/lib.rs`: the modules, public so tests and examples reach them. `src/app.rs`: fonts, settings, integration, then the main window. `src/menus.rs`: the menu bar, the keys of Quit, the font's size and Minimize, and what they do. `src/blur.rs`: what the window shows of the desktop, from the opacity and blur settings, set as they change; `blur/macos` gives gpui's NSVisualEffectView a material that blurs; `blur/wayland` asks a Wayland compositor for blur behind the window's surface through `ext_background_effect_v1`; `blur/x11` names the window's pane in `_KDE_NET_WM_BLUR_BEHIND_REGION`. `src/workspace.rs`: the window's content, Ely's `ColumnShell` with the sessions, terminal and right columns, their toggles, the right column's Git and Files switch, the title, and the sessions' lifecycle. `src/sessions.rs`: a session and its row. `src/files.rs`: the Files panel, the shown session's folder as Ely's `Tree` with Git's marks. `src/git.rs`: git through its command line: Files' marks, and the Git panel's status, stage, unstage, discard, commit and log; `src/git/diff.rs` reads git's diff into Ely's stretches. `src/git/panel.rs`: the Git panel's state and actions; `src/git/panel/view.rs`: how it draws. - `src/main.rs`: start. `src/lib.rs`: the modules, public so tests and examples reach them. `src/app.rs`: fonts, settings, integration, then the main window. `src/menus.rs`: the menu bar, the keys of Quit, the font's size and Minimize, and what they do. `src/blur.rs`: what the window shows of the desktop, from the opacity and blur settings, set as they change; `blur/macos` gives gpui's NSVisualEffectView a material that blurs; `blur/wayland` asks a Wayland compositor for blur behind the window's surface through `ext_background_effect_v1`; `blur/x11` names the window's pane in `_KDE_NET_WM_BLUR_BEHIND_REGION`. `src/workspace.rs`: the window's content, Ely's `ColumnShell` with the sessions, terminal and right columns, their toggles, the right column's Git and Files switch, the title, and the sessions' lifecycle. `src/sessions.rs`: a session and its row. `src/files.rs`: the Files panel, the shown session's folder as Ely's `Tree` with Git's marks. `src/git.rs`: git through its command line: Files' marks, and the Git panel's status, stage, unstage, discard, commit and log; `src/git/diff.rs` reads git's diff into Ely's stretches. `src/git/panel.rs`: the Git panel's state and actions; `src/git/panel/view.rs`: how it draws.
- `src/settings`: the model, its ranges, and `SettingsStore`, the global that loads, checks and saves `settings.json`; `window` is the settings window, `window/appearance` its Appearance page, `window/terminal` its Terminal page. `tests/fonts.rs` brings its own main: gpui builds its Mac platform only on the main thread. - `src/settings`: the model, its ranges, and `SettingsStore`, the global that loads, checks and saves `settings.json`; `window` is the settings window, `window/appearance` its Appearance page, `window/terminal` its Terminal page. `tests/fonts.rs` brings its own main: gpui builds its Mac platform only on the main thread.
- `src/themes`: built-in terminal themes, the chrome palette each maps to, and `apply`, which dresses the app for the system's appearance. - `src/themes`: built-in terminal themes, the chrome palette each maps to, and `apply`, which dresses the app for the system's appearance; `omarchy`, on Linux, wears Omarchy 4's current theme and follows its switches.
- `src/term`: the terminal engine. `pty` starts a program through alacritty's event loop; `tap` wraps the pty so its reader scans OSC 7 and the mouse modes before the parser. `unix` starts the program itself, so a failed `chdir` fails the start; Windows uses alacritty's ConPTY. `shell` writes the integration scripts at start and builds each shell's launch. `frame` reads the grid into styled rows, `boxes` turns box drawing, block elements and powerline arrows into rects and polygons (`boxes/outline` strokes curves and diagonals into polygons), `ink` resolves colors, `paint` draws rows, decorations and the cursor, `view` is the entity: pty events, metrics, blink, resize. `zoom` holds the points Bigger and Smaller add to the Size setting, unsaved. `keys` turns key events into bytes, the kitty protocol and, in `keys/legacy`, xterm's encoding, with no gpui types; `input` is the view's key listeners, the bindings that keep Tab and F6 from Ely's focus moves, and its IME handler; `mac` reads the Option side and repeat flag of AppKit's current event, keeps held keys repeating, and hands Command key-ups to the focused view. `mouse` encodes mouse reports, with no gpui types; `links` finds the link at a cell; `pointer` is the view's mouse listeners: selection, reports, wheel, links, copy and paste. `find` is the find box: Ely's `FindWidget`, and Ely's `find_all` over the grid's shown text. - `src/term`: the terminal engine. `pty` starts a program through alacritty's event loop; `tap` wraps the pty so its reader scans OSC 7 and the mouse modes before the parser. `unix` starts the program itself, so a failed `chdir` fails the start; Windows uses alacritty's ConPTY. `shell` writes the integration scripts at start and builds each shell's launch. `frame` reads the grid into styled rows, `boxes` turns box drawing, block elements and powerline arrows into rects and polygons (`boxes/outline` strokes curves and diagonals into polygons), `ink` resolves colors, `paint` draws rows, decorations and the cursor, `view` is the entity: pty events, metrics, blink, resize. `zoom` holds the points Bigger and Smaller add to the Size setting, unsaved. `keys` turns key events into bytes, the kitty protocol and, in `keys/legacy`, xterm's encoding, with no gpui types; `input` is the view's key listeners, the bindings that keep Tab and F6 from Ely's focus moves, and its IME handler; `mac` reads the Option side and repeat flag of AppKit's current event, keeps held keys repeating, and hands Command key-ups to the focused view. `mouse` encodes mouse reports, with no gpui types; `links` finds the link at a cell; `pointer` is the view's mouse listeners: selection, reports, wheel, links, copy and paste. `find` is the find box: Ely's `FindWidget`, and Ely's `find_all` over the grid's shown text.
- `assets/shell-integration`: ELYTerminal's own scripts (not Ghostty's, which are GPL): zsh through `ZDOTDIR`, bash through `--rcfile`, fish through `XDG_DATA_DIRS`, PowerShell through `-NoExit -Command`. Each prompt sends OSC 7 and, except in fish, whose `fish_title` stays, a title. - `assets/shell-integration`: ELYTerminal's own scripts (not Ghostty's, which are GPL): zsh through `ZDOTDIR`, bash through `--rcfile`, fish through `XDG_DATA_DIRS`, PowerShell through `-NoExit -Command`. Each prompt sends OSC 7 and, except in fish, whose `fish_title` stays, a title.
- `assets/fonts`: JetBrains Mono 2.304's bold and italic faces (OFL; Ely bundles regular), and Nerd Fonts 3.5.1's JetBrains Mono NF Mono Regular, which draws icons a font lacks. - `assets/fonts`: JetBrains Mono 2.304's bold and italic faces (OFL; Ely bundles regular), and Nerd Fonts 3.5.1's JetBrains Mono NF Mono Regular, which draws icons a font lacks.
@@ -90,3 +90,4 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- 2026-10-15: the Linux tarball builds on Debian 12, in `rust:1.95.0-bookworm`: a binary asks for the glibc symbol versions it was linked against, so it is linked against an old glibc, not the builder's. `scripts/linux-tar.sh` prints the glibc its build asks for, and the README names it: 2.35 for v0.1.0, which runs on Ubuntu 22.04. - 2026-10-15: the Linux tarball builds on Debian 12, in `rust:1.95.0-bookworm`: a binary asks for the glibc symbol versions it was linked against, so it is linked against an old glibc, not the builder's. `scripts/linux-tar.sh` prints the glibc its build asks for, and the README names it: 2.35 for v0.1.0, which runs on Ubuntu 22.04.
- 2026-10-15: on Windows gpui answers the system's hit test from the last frame: under the pointer, a `WindowControlArea::Drag` hitbox that nothing occludes makes it the caption, and Windows keeps the press, so no click reaches the app. In v0.1.0 every title bar control was dead so. Ely now occludes the drag region under its window buttons, which then answer as close, maximize and minimize, and under each column header's item row, whose controls get plain presses; the row stays the scroll container, and its title is a drag region of its own. - 2026-10-15: on Windows gpui answers the system's hit test from the last frame: under the pointer, a `WindowControlArea::Drag` hitbox that nothing occludes makes it the caption, and Windows keeps the press, so no click reaches the app. In v0.1.0 every title bar control was dead so. Ely now occludes the drag region under its window buttons, which then answer as close, maximize and minimize, and under each column header's item row, whose controls get plain presses; the row stays the scroll container, and its title is a drag region of its own.
- 2026-10-15: a release's macOS disk image comes from the Release workflow, dispatched once the draft holds the Windows and Linux builds. It runs on `macos-signing`, the Mac mini's runner in the owner's login session, whose keychain holds the Developer ID identity; at the owner's choice that runner moved from AgentCompany alone to the owner's level, so every repository reaches it. The job reads the App Store Connect key from the owner's user-level secrets `APPLE_API_ISSUER`, `APPLE_API_KEY_ID` and `APPLE_API_KEY_P8`, writes the .p8 into the runner's temp folder for notarytool alone, and puts the stapled image on the draft through `scripts/release-asset.sh` with the job's own token. One notarization covers the image and the app inside it; the image carries the ticket, and the app copied out finds its own online on its first open. - 2026-10-15: a release's macOS disk image comes from the Release workflow, dispatched once the draft holds the Windows and Linux builds. It runs on `macos-signing`, the Mac mini's runner in the owner's login session, whose keychain holds the Developer ID identity; at the owner's choice that runner moved from AgentCompany alone to the owner's level, so every repository reaches it. The job reads the App Store Connect key from the owner's user-level secrets `APPLE_API_ISSUER`, `APPLE_API_KEY_ID` and `APPLE_API_KEY_P8`, writes the .p8 into the runner's temp folder for notarytool alone, and puts the stapled image on the draft through `scripts/release-asset.sh` with the job's own token. One notarization covers the image and the app inside it; the image carries the ticket, and the app copied out finds its own online on its first open.
- 2026-10-15: on Omarchy 4 (Linux) ELYTerminal follows the system's theme. Omarchy renders the current theme's `colors.toml` through its own resolver into `~/.local/state/omarchy/current/theme/ghostty.conf`, under `$HOME` whatever `XDG_STATE_HOME` says, so ELYTerminal reads that file and wears what Ghostty wears there: its color lines go through the strict parser the bundled themes use, and the rest, such as the glass block, pass by. Omarchy writes no `cursor-text`, so the background stands in, as in Ghostty and Omarchy's kitty template. A switch replaces the whole folder, so the file's device, inode, time and size change; ELYTerminal checks them twice a second, since Omarchy signals only Alacritty, kitty and Ghostty, by name. The Omarchy theme is the default wherever that file exists, and another pick stops following; settings that wear it with no readable file stop the start, and a failed read while running is logged and keeps the last theme. Omarchy 3's `~/.config/omarchy/current` gets no shim.
+2 -1
View File
@@ -36,6 +36,7 @@ Chapters run in build order. `TASKS.md` follows them line by line.
- A theme holds background, foreground, cursor, cursor text, selection background and foreground, and the 16 ANSI colors. Values come from the theme's upstream files (iTerm2-Color-Schemes' Ghostty export, or Ely's palette). - A theme holds background, foreground, cursor, cursor text, selection background and foreground, and the 16 ANSI colors. Values come from the theme's upstream files (iTerm2-Color-Schemes' Ghostty export, or Ely's palette).
- A theme maps onto Ely's `Palette` through `Theme::set_palette`, so panels, inputs and lists wear the theme. - A theme maps onto Ely's `Palette` through `Theme::set_palette`, so panels, inputs and lists wear the theme.
- Dark mode: one theme, or a second theme for when the system is dark. - Dark mode: one theme, or a second theme for when the system is dark.
- On Omarchy 4 (Linux), an Omarchy theme wears Omarchy's current theme and changes with each switch. It is the default where Omarchy runs; picking another theme stops following.
## 5. Terminal engine ## 5. Terminal engine
@@ -92,7 +93,7 @@ Ghostty is the bar for what the terminal does. ELYTerminal owns its engine on `a
Cmd-, opens it; on Windows and Linux, which have no menu bar, so does a gear in the title bar. Categories on the left, rows on the right, each side scrolling on its own, Ely's components. Changes apply at once to every session and save. Cmd-, opens it; on Windows and Linux, which have no menu bar, so does a gear in the title bar. Categories on the left, rows on the right, each side scrolling on its own, Ely's components. Changes apply at once to every session and save.
- Appearance - Appearance
- Theme: a grid of previews. - Theme: a grid of previews; on Omarchy, the Omarchy theme first.
- Separate dark theme: a switch; when on, a second grid for dark mode. - Separate dark theme: a switch; when on, a second grid for dark mode.
- Font family: a list of installed fonts. Font size. Line height. Ligatures. - Font family: a list of installed fonts. Font size. Line height. Ligatures.
- Cursor: style (block, bar, underline, hollow), blink, with a live preview. - Cursor: style (block, bar, underline, hollow), blink, with a live preview.
+1 -1
View File
@@ -47,7 +47,7 @@ The app writes its log to stderr. `RUST_LOG=elyterminal=debug` writes more; `tra
- A button at each end of the title bar hides or shows its side column; the window opens as it was left. On Windows and Linux a gear beside the right one opens Settings. - A button at each end of the title bar hides or shows its side column; the window opens as it was left. On Windows and Linux a gear beside the right one opens Settings.
- Files shows the session's folder as a tree with Git's marks. - Files shows the session's folder as a tree with Git's marks.
- Git shows the branch, stages, unstages, discards, commits, lists recent commits and opens diffs, all through the `git` command line. - Git shows the branch, stages, unstages, discards, commits, lists recent commits and opens diffs, all through the `git` command line.
- Themes: Ely Dark, Ely Light, Catppuccin Mocha, Macchiato, Frappé and Latte, Tokyo Night, Dracula, Nord, Gruvbox Dark, Rosé Pine, Solarized Light, One Light and Kanagawa Wave. A second theme can take over while the system is dark. - Themes: Ely Dark, Ely Light, Catppuccin Mocha, Macchiato, Frappé and Latte, Tokyo Night, Dracula, Nord, Gruvbox Dark, Rosé Pine, Solarized Light, One Light and Kanagawa Wave. A second theme can take over while the system is dark. On Omarchy 4, an Omarchy theme follows the system's theme as it switches, and is the default there.
- Background opacity and blur: NSVisualEffectView on macOS, acrylic on Windows, `ext_background_effect_v1` or `org_kde_kwin_blur` on Wayland, `_KDE_NET_WM_BLUR_BEHIND_REGION` on X11, which KWin reads. - Background opacity and blur: NSVisualEffectView on macOS, acrylic on Windows, `ext_background_effect_v1` or `org_kde_kwin_blur` on Wayland, `_KDE_NET_WM_BLUR_BEHIND_REGION` on X11, which KWin reads.
## Keys ## Keys
+4
View File
@@ -72,3 +72,7 @@ Progress truth. Top to bottom, one at a time. A task is ticked only after its co
## 16. Fixes from the owner's first run ## 16. Fixes from the owner's first run
- [x] T26 On Windows every title bar control takes its press, Ely's window buttons too: in Ely, window buttons and header items occlude the drag region; the Git and Files switch moves below the title bar, centered; on Windows and Linux a gear in the title bar opens Settings; on Windows Ctrl-V pastes, as in Windows Terminal; the pointer over the terminal is the arrow; the settings window drops General, whose Shell and Start in lead Terminal, which lists every shortcut, and its categories and page scroll apart; the release's macOS disk image signed with Developer ID and notarized on the signing runner; release v0.1.1 with the three builds. - [x] T26 On Windows every title bar control takes its press, Ely's window buttons too: in Ely, window buttons and header items occlude the drag region; the Git and Files switch moves below the title bar, centered; on Windows and Linux a gear in the title bar opens Settings; on Windows Ctrl-V pastes, as in Windows Terminal; the pointer over the terminal is the arrow; the settings window drops General, whose Shell and Start in lead Terminal, which lists every shortcut, and its categories and page scroll apart; the release's macOS disk image signed with Developer ID and notarized on the signing runner; release v0.1.1 with the three builds.
## 17. Omarchy
- [ ] T27 On Omarchy 4, an Omarchy theme wears the system's current theme and follows each switch, the default there; the v0.1.1 Linux tarball rebuilt with it, in place of the old one on the release.
+2
View File
@@ -64,6 +64,8 @@ pub fn start(cx: &mut App) -> anyhow::Result<()> {
mac::repeat_held_keys(); mac::repeat_held_keys();
mac::deliver_command_key_ups()?; mac::deliver_command_key_ups()?;
} }
#[cfg(target_os = "linux")]
themes::omarchy::start(cx)?;
themes::apply(cx); themes::apply(cx);
let chosen = |settings: &settings::Settings| { let chosen = |settings: &settings::Settings| {
let theme = (settings.theme.clone(), settings.dark_theme.clone()); let theme = (settings.theme.clone(), settings.dark_theme.clone());
+2 -5
View File
@@ -82,7 +82,7 @@ impl Default for Settings {
Self { Self {
shell: String::new(), shell: String::new(),
start_in: StartIn::Home, start_in: StartIn::Home,
theme: "catppuccin-mocha".into(), theme: themes::default_id().into(),
dark_theme: None, dark_theme: None,
font_family: "JetBrains Mono".into(), font_family: "JetBrains Mono".into(),
font_size: 13.0, font_size: 13.0,
@@ -124,10 +124,7 @@ impl Settings {
("dark_theme", self.dark_theme.as_ref()), ("dark_theme", self.dark_theme.as_ref()),
] { ] {
if let Some(id) = id { if let Some(id) = id {
anyhow::ensure!( anyhow::ensure!(themes::known(id), "{field} is {id:?}, no built-in theme");
themes::find(id).is_some(),
"{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");
+5 -4
View File
@@ -103,7 +103,7 @@ impl SettingsWindow {
settings.cursor_style, settings.cursor_style,
|settings, style| settings.cursor_style = 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 lit = self.lit || !settings.cursor_blink;
let cursor = SettingsSection::new("Cursor") let cursor = SettingsSection::new("Cursor")
.row(SettingsRow::new("Style").control(style)) .row(SettingsRow::new("Style").control(style))
@@ -113,7 +113,7 @@ impl SettingsWindow {
settings.cursor_blink, settings.cursor_blink,
|settings, on| settings.cursor_blink = on, |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 opacity = percent(settings.background_opacity);
let slider = Slider::new("background_opacity", opacity) let slider = Slider::new("background_opacity", opacity)
.range(percent(*OPACITY.start()), percent(*OPACITY.end())) .range(percent(*OPACITY.start()), percent(*OPACITY.end()))
@@ -207,7 +207,7 @@ fn fraction(percent: f64) -> f32 {
(percent / 100.) as 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( fn grid(
this: &Entity<SettingsWindow>, this: &Entity<SettingsWindow>,
title: Option<&'static str>, title: Option<&'static str>,
@@ -222,7 +222,8 @@ fn grid(
} else { } else {
("theme", Some(settings.theme.as_str())) ("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 (id, this) = (each.id, this.clone());
let key = SharedString::from(format!("{field}-{id}")); let key = SharedString::from(format!("{field}-{id}"));
RadioCard::new(key, chosen == Some(id), each.name) RadioCard::new(key, chosen == Some(id), each.name)
+4 -4
View File
@@ -243,9 +243,9 @@ impl TerminalView {
self.pty.pid self.pty.pid
), ),
Event::ColorRequest(index, reply) => { 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 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); drop(term);
self.pty.write(reply(rgb).into_bytes()); 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 { impl Render for TerminalView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = SettingsStore::get(cx).clone(); 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 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| { let lit = self.finder.as_ref().map_or_else(Vec::new, |finder| {
finder.lit(&term, cx.theme().colors.warning) 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. /// Dresses the app in the theme the settings choose for the system's appearance.
pub fn apply(cx: &mut App) { pub fn apply(cx: &mut App) {
let settings = SettingsStore::get(cx); 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 opacity = settings.background_opacity;
let mode = if theme.is_dark() { let mode = if theme.is_dark() {
Mode::Dark Mode::Dark
} else { } else {
Mode::Light 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); Theme::set_mode_now(mode, cx);
log::info!("theme: {} in {mode:?}, glass at {opacity}", theme.id); log::info!("theme: {} in {mode:?}, glass at {opacity}", theme.id);
} }
+39 -5
View File
@@ -1,10 +1,12 @@
mod chrome; mod chrome;
#[cfg(target_os = "linux")]
pub mod omarchy;
use std::sync::LazyLock; use std::sync::LazyLock;
use anyhow::Context as _; use anyhow::Context as _;
use ely_gpui_component::theme::Palette; use ely_gpui_component::theme::Palette;
use gpui::{Hsla, Rgba, WindowAppearance}; use gpui::{App, Global, Hsla, Rgba, WindowAppearance};
pub use chrome::{apply, palette}; 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. /// Upstream Ghostty files from iTerm2-Color-Schemes: id, shown name, text.
const FILES: [(&str, &str, &str); 12] = [ 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) 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. /// 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!( let dark = matches!(
appearance, appearance,
WindowAppearance::Dark | WindowAppearance::VibrantDark WindowAppearance::Dark | WindowAppearance::VibrantDark
); );
let id = match (&settings.dark_theme, dark) { match (&settings.dark_theme, dark) {
(Some(id), true) => id, (Some(id), true) => id,
_ => &settings.theme, _ => &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 { 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()); 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] #[test]
fn the_dark_theme_wins_only_in_dark_mode() { fn the_dark_theme_wins_only_in_dark_mode() {
let settings = Settings { let settings = Settings {
@@ -94,21 +140,18 @@ fn the_dark_theme_wins_only_in_dark_mode() {
..Settings::default() ..Settings::default()
}; };
assert_eq!( assert_eq!(
active(&settings, WindowAppearance::Light).id, chosen(&settings, WindowAppearance::Light),
"catppuccin-latte" "catppuccin-latte"
); );
assert_eq!( assert_eq!(
active(&settings, WindowAppearance::VibrantDark).id, chosen(&settings, WindowAppearance::VibrantDark),
"catppuccin-mocha" "catppuccin-mocha"
); );
let single = Settings { let single = Settings {
dark_theme: None, dark_theme: None,
..settings ..settings
}; };
assert_eq!( assert_eq!(chosen(&single, WindowAppearance::Dark), "catppuccin-latte");
active(&single, WindowAppearance::Dark).id,
"catppuccin-latte"
);
} }
#[test] #[test]
+2
View File
@@ -43,6 +43,7 @@ What no check reached: no one has looked at the window on Windows, which CI buil
| Background, foreground, cursor, cursor text, selection, 16 ANSI from upstream | T03 | `themes::tests` | Pass | | Background, foreground, cursor, cursor text, selection, 16 ANSI from upstream | T03 | `themes::tests` | Pass |
| A theme maps onto Ely's `Palette` through `Theme::set_palette` | T03, T19 | `the_chrome_wears_the_theme` | Pass | | A theme maps onto Ely's `Palette` through `Theme::set_palette` | T03, T19 | `the_chrome_wears_the_theme` | Pass |
| One theme, or a second for dark mode | T17 | T17 review | Pass | | One theme, or a second for dark mode | T17 | T17 review | Pass |
| On Omarchy 4, an Omarchy theme wears the current theme and follows each switch; the default there; another pick stops following | T27 | `omarchys_ghostty_conf_reads_without_its_glass`; on X11, with Omarchy v4.0.4's own renderer staging and swapping the folder as `omarchy-theme-set` does: with no theme saved the window wore Omarchy's, and through all 22 of its themes the terminal's ground matched each background within 1.5 s; Omarchy's glass lines changed nothing; a file without palette 7 was logged and the last theme stayed; a picked Catppuccin Mocha stayed through a switch; settings wearing it with no Omarchy stopped the start, naming the file | Pass |
## 5. Terminal engine ## 5. Terminal engine
@@ -110,6 +111,7 @@ What no check reached: no one has looked at the window on Windows, which CI buil
| On Windows and Linux, a gear in the title bar opens it too | T26 | on X11 under Xvfb with xfwm4, xdotool's click on the gear, just inside the right toggle, opened the 900×640 Settings window ("settings: the window opens"), after a click on + opened a session; sent through `App::dispatch_action`, the same click had opened nothing; the owner, given v0.1.1's build and the gear to click, reported it works on Windows | Pass | | On Windows and Linux, a gear in the title bar opens it too | T26 | on X11 under Xvfb with xfwm4, xdotool's click on the gear, just inside the right toggle, opened the 900×640 Settings window ("settings: the window opens"), after a click on + opened a session; sent through `App::dispatch_action`, the same click had opened nothing; the owner, given v0.1.1's build and the gear to click, reported it works on Windows | Pass |
| Terminal: Shell, Start in | T16, T26 | `settings::tests`; the General page is gone, and a capture on X11 shows its two rows leading Terminal | Pass | | Terminal: Shell, Start in | T16, T26 | `settings::tests`; the General page is gone, and a capture on X11 shows its two rows leading Terminal | Pass |
| Appearance: theme grids, dark theme, font, size, line height, ligatures, cursor and preview, opacity, blur, padding | T17 | T17 review | Pass | | Appearance: theme grids, dark theme, font, size, line height, ligatures, cursor and preview, opacity, blur, padding | T17 | T17 review | Pass |
| On Omarchy, the Omarchy theme first in the grid | T27 | on X11 with Omarchy's theme folder, the Omarchy card leads the grid, chosen, over a preview in its colors | Pass |
| Terminal: scrollback, Option as Alt (macOS), copy on select | T18 | T18 review | Pass | | Terminal: scrollback, Option as Alt (macOS), copy on select | T18 | T18 review | Pass |
| Terminal: every shortcut, with a search | T26 | read from the keymap, the bindings that apply in the main window's terminal; on X11 the list showed this platform's keys (Ctrl+Shift+T, Alt+1 and on), and typing "paste" left Edit's Paste, Ctrl+Shift+V | Pass | | Terminal: every shortcut, with a search | T26 | read from the keymap, the bindings that apply in the main window's terminal; on X11 the list showed this platform's keys (Ctrl+Shift+T, Alt+1 and on), and typing "paste" left Edit's Paste, Ctrl+Shift+V | Pass |