T19: the main window lets the desktop through while Background opacity is under 1, blurred or clear as Background blur says, set at open and as the settings change; Ely's new ColumnShell::glass grounds every column and the title bar in the palette's glass, which the opacity sets, and the Ely pin moves to it past 27 of Ely's own commits; dialogs stay solid; the terminal paints no second ground; shot names the window's AppKit views
CI / platforms (windows-latest) (push) Failing after 8s
CI / macos (push) Failing after 8m32s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-14 23:27:19 -04:00
1 parent d0162ef009
commit c98bbad6e0
11 files changed
+137 -28

No files matched your search

+3 -2
View File
@@ -12,7 +12,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
## Commands
- `cargo run` opens the app. `RUST_LOG=elyterminal=debug cargo run` logs more; `trace` logs each paint.
- `cargo run --example shot -- [--keys "SPECS"] <png> <program> [args…]` runs a program in the main window, plays SPECS on it, and writes a PNG of the window (macOS; no permission needed for the app's own window). It shows its window without activating the app, so gpui keeps drawing it and keystrokes or clicks meant for other apps stay there. Run it with `HOME` pointing at a temp folder to try settings without touching your own. SPECS, space-separated:
- `cargo run --example shot -- [--keys "SPECS"] <png> <program> [args…]` runs a program in the main window, plays SPECS on it, and writes a PNG of the window (macOS; no permission needed for the app's own window), naming the AppKit views under its content: gpui's blur is a `BlurredView`, and a capture of one window shows its alpha, not the blur behind it. It shows its window without activating the app, so gpui keeps drawing it and keystrokes or clicks meant for other apps stay there. Run it with `HOME` pointing at a temp folder to try settings without touching your own. SPECS, space-separated:
- keys as real AppKit events: `a`, `ctrl-c`, `lopt-f` and `ropt-f` (left and right Option), `cmd-v`, `repeat-up` (AppKit's repeat flag);
- input method calls: `mark:é` composes, `commit:é` types;
- `activate` and `deactivate` the app, which takes real keystrokes while active: keep the two specs adjacent;
@@ -22,7 +22,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
## Layout
- `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/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/blur.rs`: what the window shows of the desktop, from the opacity and blur settings, set as they change. `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/themes`: built-in terminal themes, the chrome palette each maps to, and `apply`, which dresses the app for the system's appearance.
- `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. `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.
@@ -69,3 +69,4 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- 2026-10-14: settings refuse unknown fields; a bad settings file stops the start and is never overwritten.
- 2026-10-14: Appearance shows every built-in theme as Ely's `RadioCard` in a `SimpleGrid`, each over a small window in the theme's colors. Separate dark theme copies the theme into `dark_theme`, so turning it on changes nothing until a dark card is picked. A theme change re-dresses the chrome at once. Font family comes from Ely's `FontPicker`; size, line height and padding from its `NumberInput`, which saves a number on Enter, on leaving the field and on each step. A font saves, and a settings file starts, only when gpui loads it as the terminal draws it: gpui falls back to another font without a word and loads none without an `m`, so the terminal draws with no font but its own. The cursor preview is a prompt in the terminal's font and theme, its cursor drawn as the terminal draws it, blinking on the terminal's beat. Opacity moves by whole percents, so the saved fraction carries no float noise. Blur saves here and reaches the window in T19. A change the store refuses leaves the setting as it was, and the page says why at its top; a number field drops its draft, and the font picker shows the saved family again. The settings window opens at 900 by 640, room for three theme columns.
- 2026-10-14: Terminal shows scrollback lines, Option as Alt, on macOS only, since elsewhere Alt is Alt, and copy on select. The engine reads each when it needs it, so a change applies at once to every session; a smaller scrollback drops the history past it.
- 2026-10-14: the main window lets the desktop through while Background opacity is under 1, blurred or clear as Background blur says, through gpui's `Blurred` (macOS's NSVisualEffectView, Windows' acrylic) or `Transparent`; at 1 it is `Opaque`. The workspace sets it as the settings change. Ely's `ColumnShell::glass`, added to Ely for this, grounds the columns in the palette's glass, which the opacity sets, so the title bar and every column let the desktop through alike; the palette's `bg` stays solid, so a dialog, the Git diff among them, hides what lies under it. The terminal paints no ground over the glass, or the two would stack; a program's own background (OSC 11) paints over it. Ely's own themes pass their palette too, Ely's colors with the glass set. The settings window stays opaque.
Generated
+2 -1
View File
@@ -1701,7 +1701,7 @@ checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
[[package]]
name = "ely-gpui-component"
version = "0.1.1"
source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git?rev=12512217bd9bc652ed6bbf10461b00cc26c01c21#12512217bd9bc652ed6bbf10461b00cc26c01c21"
source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git?rev=991ca7b548e0417146ec082dc89ccb8b8d0e09d6#991ca7b548e0417146ec082dc89ccb8b8d0e09d6"
dependencies = [
"alacritty_terminal",
"anyhow",
@@ -1738,6 +1738,7 @@ dependencies = [
"similar",
"smallvec",
"unicode-segmentation",
"unicode-width",
"usvg",
"vte",
"web-time",
+1 -1
View File
@@ -12,7 +12,7 @@ publish = false
alacritty_terminal = { version = "=0.26.0", default-features = false }
anyhow = "1"
dirs = "7"
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git", rev = "12512217bd9bc652ed6bbf10461b00cc26c01c21" }
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git", rev = "991ca7b548e0417146ec082dc89ccb8b8d0e09d6" }
env_logger = "0.11"
futures = "0.3"
gethostname = "1.1.0"
+24 -2
View File
@@ -1,4 +1,4 @@
//! Runs a program in the main window, plays keys and mouse gestures on it, and photographs it; `to:settings` and `to:main` aim what follows.
//! Runs a program in the main window, plays keys and mouse gestures on it, photographs it and names its AppKit views; `to:settings` and `to:main` aim what follows.
#[cfg(target_os = "macos")]
fn main() {
@@ -67,7 +67,8 @@ fn main() {
let title = aim.update(cx, |_, window, _| window.window_title());
let title = title.expect("the window lives");
capture::snapshot(number(aim, cx), std::path::Path::new(&out)).expect("a picture");
log::info!("shot: wrote {out}, titled {title:?}");
let views = capture::views(number(aim, cx)).expect("the window's views");
log::info!("shot: wrote {out}, titled {title:?}, over {views:?}");
cx.update(|cx| cx.quit());
})
.detach();
@@ -107,6 +108,27 @@ mod capture {
Ok(())
}
/// The class of each view under the window's content, as AppKit names it; gpui's blur is a `BlurredView`.
pub fn views(number: u32) -> anyhow::Result<Vec<String>> {
unsafe {
let app: *mut Object = msg_send![class!(NSApplication), sharedApplication];
let window: *mut Object = msg_send![app, windowWithWindowNumber: i64::from(number)];
anyhow::ensure!(!window.is_null(), "no window {number}");
let content: *mut Object = msg_send![window, contentView];
let views: *mut Object = msg_send![content, subviews];
let count: u64 = msg_send![views, count];
let named = (0..count).map(|ix| {
let view: *mut Object = msg_send![views, objectAtIndex: ix];
let class: *mut Object = msg_send![view, className];
let text: *const std::ffi::c_char = msg_send![class, UTF8String];
std::ffi::CStr::from_ptr(text)
.to_string_lossy()
.into_owned()
});
Ok(named.collect())
}
}
/// Writes the window `number` as a PNG; the app's own window needs no permission.
pub fn snapshot(number: u32, path: &Path) -> anyhow::Result<()> {
use core_graphics::{
+11 -3
View File
@@ -10,6 +10,7 @@ use gpui::{
#[cfg(target_os = "macos")]
use crate::term::mac;
use crate::{
blur,
settings::{self, SettingsStore},
term::{ICONS, Launch, MONO, config, input, shell::ShellIntegration, spawn},
themes,
@@ -60,8 +61,10 @@ pub fn start(cx: &mut App) -> anyhow::Result<()> {
mac::deliver_command_key_ups()?;
}
themes::apply(cx);
let chosen =
|settings: &settings::Settings| (settings.theme.clone(), settings.dark_theme.clone());
let chosen = |settings: &settings::Settings| {
let theme = (settings.theme.clone(), settings.dark_theme.clone());
(theme, settings.background_opacity)
};
let mut worn = chosen(SettingsStore::get(cx));
cx.observe_global::<SettingsStore>(move |cx| {
let now = chosen(SettingsStore::get(cx));
@@ -77,6 +80,7 @@ pub fn start(cx: &mut App) -> anyhow::Result<()> {
/// Opens the main window running `launch`.
pub fn open(launch: Launch, cx: &mut App) -> anyhow::Result<WindowHandle<Workspace>> {
let bounds = Bounds::centered(None, size(px(1200.0), px(760.0)), cx);
let background = blur::appearance(SettingsStore::get(cx));
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
titlebar: Some(TitlebarOptions {
@@ -86,6 +90,7 @@ pub fn open(launch: Launch, cx: &mut App) -> anyhow::Result<WindowHandle<Workspa
}),
// Ely's columns draw the controls; only Linux reads this, and must not draw its own.
window_decorations: Some(WindowDecorations::Client),
window_background: background,
..Default::default()
};
let (pty, events) = spawn(&launch, (80, 24), (8, 16), config(SettingsStore::get(cx)))?;
@@ -95,6 +100,9 @@ pub fn open(launch: Launch, cx: &mut App) -> anyhow::Result<WindowHandle<Workspa
.detach();
cx.new(|cx| Workspace::new(pty, events, launch, window, cx))
})?;
log::info!("app: main window open");
log::info!(
"app: main window open, blur: {background:?} on {}",
std::env::consts::OS
);
Ok(window)
}
+46
View File
@@ -0,0 +1,46 @@
use gpui::{Window, WindowBackgroundAppearance};
use crate::settings::Settings;
/// What the window shows of the desktop: blurred or clear while the background lets it through.
pub fn appearance(settings: &Settings) -> WindowBackgroundAppearance {
if settings.background_opacity >= 1.0 {
WindowBackgroundAppearance::Opaque
} else if settings.background_blur {
WindowBackgroundAppearance::Blurred
} else {
WindowBackgroundAppearance::Transparent
}
}
/// Sets `window` to what the settings ask when it differs from `shown`; logs the way taken.
pub fn follow(window: &Window, shown: &mut WindowBackgroundAppearance, settings: &Settings) {
let wanted = appearance(settings);
if wanted != *shown {
*shown = wanted;
window.set_background_appearance(wanted);
log::info!("blur: the window is {wanted:?} on {}", std::env::consts::OS);
}
}
#[cfg(test)]
mod tests {
use gpui::WindowBackgroundAppearance::{Blurred, Opaque, Transparent};
use super::*;
#[test]
fn the_desktop_shows_only_through_a_background_it_can_pass() {
let at = |opacity, blur| {
appearance(&Settings {
background_opacity: opacity,
background_blur: blur,
..Settings::default()
})
};
assert_eq!(at(1.0, true), Opaque);
assert_eq!(at(1.0, false), Opaque);
assert_eq!(at(0.88, true), Blurred);
assert_eq!(at(0.3, false), Transparent);
}
}
+1
View File
@@ -1,4 +1,5 @@
pub mod app;
pub mod blur;
pub mod files;
pub mod git;
pub mod sessions;
+4 -1
View File
@@ -357,7 +357,10 @@ impl Render for TerminalView {
.size_full()
.px(px(settings.padding_x))
.py(px(settings.padding_y))
.bg(ink.bg.alpha(settings.background_opacity));
// The window's glass grounds the theme's background; a program's own paints over it.
.when(ink.bg != theme.background, |root| {
root.bg(ink.bg.alpha(settings.background_opacity))
});
let root = [MouseButton::Left, MouseButton::Middle, MouseButton::Right]
.into_iter()
.fold(root, |root, button| {
+17 -12
View File
@@ -4,20 +4,23 @@ 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);
/// The chrome's palette for a terminal theme, its glass as see-through as the background; Ely's own keep Ely's colors.
pub fn palette(theme: &TerminalTheme, opacity: f32) -> Palette {
let base = if theme.is_dark() {
Palette::dark(false)
} else {
Palette::light(false)
};
if theme.ely {
return Palette {
glass: base.bg.alpha(opacity),
..base
};
}
let (bg, fg, ansi) = (theme.background, theme.foreground, theme.ansi);
let tint = |share: f32| bg.mix(&fg, share);
let wash = |color: gpui::Hsla| bg.mix(&color, 0.18);
Some(Palette {
Palette {
bg,
surface: bg,
sunken: tint(0.03),
@@ -44,7 +47,7 @@ pub fn palette(theme: &TerminalTheme) -> Option<Palette> {
warning_subtle: wash(ansi[3]),
danger_subtle: wash(ansi[1]),
info_subtle: wash(ansi[4]),
glass: bg.alpha(base.glass.a),
glass: bg.alpha(opacity),
tooltip_bg: tint(0.2),
tooltip_fg: fg,
chart: [
@@ -67,18 +70,20 @@ pub fn palette(theme: &TerminalTheme) -> Option<Palette> {
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 settings = SettingsStore::get(cx);
let theme = active(settings, cx.window_appearance());
let opacity = settings.background_opacity;
let mode = if theme.is_dark() {
Mode::Dark
} else {
Mode::Light
};
Theme::set_palette(mode, palette(theme), cx);
Theme::set_palette(mode, Some(palette(theme, opacity)), cx);
Theme::set_mode_now(mode, cx);
log::info!("theme: {} in {mode:?}", theme.id);
log::info!("theme: {} in {mode:?}, glass at {opacity}", theme.id);
}
+17 -3
View File
@@ -114,11 +114,25 @@ fn the_dark_theme_wins_only_in_dark_mode() {
#[test]
fn the_chrome_wears_the_theme() {
let dracula = find("dracula").expect("dracula");
let chrome = palette(dracula).expect("a mapped palette");
let chrome = palette(dracula, 1.0);
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());
let ely = palette(find("ely-dark").expect("ely"), 1.0);
assert_eq!(ely.bg, Palette::dark(false).bg, "Ely's own colors");
}
#[test]
fn the_glass_lets_through_what_the_opacity_leaves() {
for id in ["dracula", "ely-light"] {
let theme = find(id).expect("a theme");
let chrome = palette(theme, 0.4);
assert_eq!(chrome.glass, chrome.bg.alpha(0.4), "{id}");
assert_eq!(
chrome.bg.a, 1.0,
"{id}: the ground under dialogs stays solid"
);
assert_eq!(palette(theme, 1.0).glass, chrome.bg, "{id}");
}
}
+11 -3
View File
@@ -9,10 +9,12 @@ use ely_gpui_component::{
use gpui::{
Action, App, AppContext, Context, Entity, EntityId, FocusHandle, Focusable, InteractiveElement,
IntoElement, KeyBinding, ParentElement, Render, SharedString, StatefulInteractiveElement,
StyleRefinement, Styled, Subscription, Window, actions, div, prelude::FluentBuilder,
StyleRefinement, Styled, Subscription, Window, WindowBackgroundAppearance, actions, div,
prelude::FluentBuilder,
};
use crate::{
blur,
files::Files,
git::panel::{Acted, GitPanel},
sessions::{self, Session},
@@ -84,6 +86,8 @@ pub struct Workspace {
active: usize,
files: Entity<Files>,
git: Entity<GitPanel>,
/// What the window shows of the desktop, as last set.
background: WindowBackgroundAppearance,
_settings: Subscription,
_acted: Subscription,
}
@@ -109,7 +113,11 @@ impl Workspace {
active: 0,
files: cx.new(|_| Files::new(launch.cwd.clone())),
git,
_settings: cx.observe_global::<SettingsStore>(|_, cx| cx.notify()),
background: blur::appearance(SettingsStore::get(cx)),
_settings: cx.observe_global_in::<SettingsStore>(window, |this, window, cx| {
blur::follow(window, &mut this.background, SettingsStore::get(cx));
cx.notify();
}),
_acted: acted,
};
workspace.add(pty, events, launch, window, cx);
@@ -316,7 +324,7 @@ impl Render for Workspace {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = SettingsStore::get(cx);
let width = cx.theme().sidebar_width(false);
let mut shell = ColumnShell::new("workspace");
let mut shell = ColumnShell::new("workspace").glass();
if settings.show_sessions {
let (workspace, home) = (cx.entity(), dirs::home_dir());
let rows = self.sessions.iter().enumerate().map(|(index, session)| {