T17: Appearance: every theme as a card over a small window in its colors, a separate dark theme with its own grid, font family, size, line height and ligatures, cursor style and blink over a live prompt, opacity by whole percents, blur, padding; a theme change re-dresses the chrome at once; a change the store refuses says why at the top of the page
CI / platforms (windows-latest) (push) Failing after 9s
CI / macos (push) Failing after 10m6s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-14 22:14:47 -04:00
1 parent 7ef2ea0106
commit f2cc16fd94
6 files changed
+491 -15

No files matched your search

+91 -12
View File
@@ -10,11 +10,14 @@ use ely_gpui_component::{
use gpui::{
App, AppContext, Bounds, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
KeyBinding, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled,
Subscription, TitlebarOptions, Window, WindowBounds, WindowDecorations, WindowOptions, actions,
div, px, size,
Subscription, Task, TitlebarOptions, Window, WindowBounds, WindowDecorations, WindowOptions,
actions, div, px, size,
};
use super::{SettingsStore, StartIn};
use super::{Settings, SettingsStore, StartIn};
use crate::term::BLINK;
mod appearance;
actions!(elyterminal, [OpenSettings, CloseSettings]);
@@ -22,7 +25,10 @@ actions!(elyterminal, [OpenSettings, CloseSettings]);
const CONTEXT: &str = "Settings";
/// Each category's key, name and icon.
const SECTIONS: [(&str, &str, IconName); 1] = [("general", "General", IconName::Settings)];
const SECTIONS: [(&str, &str, IconName); 2] = [
("general", "General", IconName::Settings),
("appearance", "Appearance", IconName::Palette),
];
/// Cmd-, opens the settings, Cmd-W closes them; off macOS Ctrl-, and Ctrl-Shift-W, as Ghostty's.
pub fn bind_keys(cx: &mut App) {
@@ -52,7 +58,7 @@ fn open_window(cx: &mut App) {
}
return;
}
let bounds = Bounds::centered(None, size(px(760.0), px(560.0)), cx);
let bounds = Bounds::centered(None, size(px(900.0), px(640.0)), cx);
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
titlebar: Some(TitlebarOptions {
@@ -79,8 +85,14 @@ pub struct SettingsWindow {
shell: Entity<TextInput>,
/// Why the typed shell is not saved.
refused: Option<SharedString>,
/// Why the last change did not save.
failed: Option<SharedString>,
font: Entity<TextInput>,
/// Whether the cursor preview shows, as it blinks.
lit: bool,
_typed: Subscription,
_settings: Subscription,
_blink: Task<()>,
}
impl SettingsWindow {
@@ -90,6 +102,7 @@ impl SettingsWindow {
input.set_text(SettingsStore::get(cx).shell.clone(), cx);
input
});
let font = cx.new(|cx| TextInput::new(window, cx));
let typed = cx.subscribe(&shell, |this, input, event: &InputEvent, cx| {
if let InputEvent::Changed = event {
let shell = input.read(cx).text().to_string();
@@ -104,11 +117,33 @@ impl SettingsWindow {
section: SECTIONS[0].0.into(),
shell,
refused: None,
failed: None,
font,
lit: true,
_typed: typed,
_settings: cx.observe_global::<SettingsStore>(|_, cx| cx.notify()),
_blink: Self::blink(cx),
}
}
/// Blinks the cursor preview as a terminal blinks its cursor, while it shows.
fn blink(cx: &mut Context<Self>) -> Task<()> {
cx.spawn(async move |this, cx| {
loop {
cx.background_executor().timer(BLINK).await;
let ticked = this.update(cx, |this, cx| {
if this.section == "appearance" && SettingsStore::get(cx).cursor_blink {
this.lit = !this.lit;
cx.notify();
}
});
if ticked.is_err() {
break;
}
}
})
}
/// Saves `shell` when it names a program; else says why and keeps the saved one.
fn save_shell(&mut self, shell: String, cx: &mut Context<Self>) {
let refused = match SettingsStore::update(cx, |settings| settings.shell = shell.clone()) {
@@ -143,7 +178,10 @@ impl SettingsWindow {
.segment("home", "Home", None)
.segment("session", "Session's folder", None)
.size(ControlSize::Sm)
.on_change(|value, _, cx| start_in(value, cx));
.on_change({
let this = cx.entity();
move |value, _, cx| start_in(&this, value, cx)
});
SettingsSection::new("General")
.row(SettingsRow::new("Shell").control(shell))
.row(SettingsRow::new("Start in").control(start))
@@ -151,21 +189,62 @@ impl SettingsWindow {
}
/// Where new sessions start, as the settings keep it.
fn start_in(value: &SharedString, cx: &mut App) {
fn start_in(this: &Entity<SettingsWindow>, value: &SharedString, cx: &mut App) {
let start = match value.as_ref() {
"home" => StartIn::Home,
"session" => StartIn::Session,
other => unreachable!("settings: no start {other}"),
};
match SettingsStore::update(cx, |settings| settings.start_in = start) {
Ok(()) => log::info!("settings: new sessions start in {start:?}"),
Err(error) => log::error!("settings: the start stays: {error:#}"),
set(
this,
format!("new sessions start in {start:?}"),
cx,
|settings| settings.start_in = start,
);
}
/// Saves one change and logs it; a refused one changes nothing, and the window says why.
fn set(
this: &Entity<SettingsWindow>,
change: String,
cx: &mut App,
edit: impl FnOnce(&mut Settings),
) {
let mut next = SettingsStore::get(cx).clone();
edit(&mut next);
if next == *SettingsStore::get(cx) {
return;
}
let failed = match SettingsStore::update(cx, |settings| *settings = next) {
Ok(()) => {
log::info!("settings: {change}");
None
}
Err(error) => {
log::error!("settings: refused {change}: {error:#}");
Some(format!("{error:#}").into())
}
};
this.update(cx, |this, cx| {
this.failed = failed;
cx.notify();
});
}
impl Render for SettingsWindow {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let pick = cx.entity();
let page = match self.section.as_ref() {
"general" => self.general(cx).into_any_element(),
"appearance" => self.appearance(window, cx).into_any_element(),
other => unreachable!("settings: no section {other}"),
};
let page = div()
.flex()
.flex_col()
.gap_4()
.children((self.failed.clone()).map(|why| InlineMessage::new(Severity::Danger, why)))
.child(page);
let layout = SettingsLayout::new("settings", SECTIONS, self.section.clone(), {
move |key, _, cx| {
pick.update(cx, |this, cx| {
@@ -174,7 +253,7 @@ impl Render for SettingsWindow {
})
}
})
.page(self.general(cx));
.page(page);
let body = div()
.id("settings-body")
.size_full()