From 2dd817d8cdfe0c4523a46a9f2eb17adc5371fce8 Mon Sep 17 00:00:00 2001
From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Date: Thu, 16 Jul 2026 16:30:59 +0800
Subject: [PATCH] =?UTF-8?q?fix(theme):=20review=20follow-ups=20=E2=80=94?=
=?UTF-8?q?=20diff=20overlay=20background,=20hot-reload=20window=20effects?=
=?UTF-8?q?,=20docs?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Diff overlay paints the gradient/opacity-aware window background instead
of the stale representative solid.
- Config hot-reload now re-runs apply_theme with the window (blur flip,
traffic-light re-pin) and re-syncs the Appearance opacity slider, so
hand-edits to config.json / theme files take effect fully.
- Document the theme background/window settings in features docs (EN + zh-CN).
---
docs/features.md | 4 +++-
docs/features.zh-CN.md | 4 +++-
src/main.rs | 9 ++++----
src/ui/app.rs | 47 +++++++++++++++++++++++++++++-------------
src/ui/diff_overlay.rs | 12 ++++++++++-
src/ui/settings.rs | 12 +++++++----
src/ui/theme.rs | 5 +----
7 files changed, 64 insertions(+), 29 deletions(-)
diff --git a/docs/features.md b/docs/features.md
index 00d8aa85..d854bfe7 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -17,7 +17,9 @@
- **Tabs & splits** — always open in the current directory
- **Command palette** ⌘ P · scrollback search ⌘ F
- **⌘-click links** · desktop notifications · copy on select (opt-in, Settings → Terminal → Clipboard)
-- **Eight themes** · CJK / IME input
+- **Eight themes, plus your own** — YAML seed themes with solid, gradient, or image backgrounds; iTerm2 `.itermcolors` import; in-app color editor with a background-image picker
+- **Window opacity & blur** — Settings → Appearance → Window; applies to every theme, *Follow theme* returns to the theme's own `opacity` / `blur`
+- **CJK / IME input**
## Coding agents
diff --git a/docs/features.zh-CN.md b/docs/features.zh-CN.md
index 48851018..cd66696b 100644
--- a/docs/features.zh-CN.md
+++ b/docs/features.zh-CN.md
@@ -17,7 +17,9 @@
- **标签页与分屏** —— 永远开在当前目录
- **命令面板** ⌘ P · 回滚搜索 ⌘ F
- **⌘ 点击打开链接** · 桌面通知 · 划选即复制(可选,设置 → 终端 → 剪贴板)
-- **8 套主题** · CJK / 输入法输入
+- **8 套主题,也能自定义** — YAML 种子主题,背景支持纯色、渐变或图片;可导入 iTerm2 `.itermcolors`;应用内颜色编辑器带背景图选择
+- **窗口透明与模糊** — 设置 → Appearance → Window;对所有主题生效,*Follow theme* 恢复主题自带的 `opacity` / `blur`
+- **CJK / 输入法输入**
## Coding agent
diff --git a/src/main.rs b/src/main.rs
index 788e7d11..13a48a18 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -137,8 +137,10 @@ fn spawn_config_watcher(cx: &mut App) {
// Re-paint theme + colors from the new config. We have no window
// handle in this global task, but `apply_theme` accepts `None`:
// it still updates the `Theme`/palette globals (what actually
- // repaints); the only thing it skips is re-pinning the macOS
- // traffic lights, which self-corrects on the next resize/activate.
+ // repaints). The window-bound effects — the Transparent↔Blurred
+ // background flip and traffic-light re-pinning — are covered by
+ // `Tty7App::reload_from_config`, which observes the `Config`
+ // global with its window and re-runs `apply_theme(Some(window))`.
crate::ui::theme::apply_theme(None, cx);
// Schedule every window to redraw so the new palette shows at once.
cx.refresh_windows();
@@ -380,8 +382,7 @@ fn main() {
WindowBounds::Fullscreen(bounds)
}
};
- let window_background =
- cx.update(|cx| crate::ui::theme::background_appearance(cx));
+ let window_background = cx.update(|cx| crate::ui::theme::background_appearance(cx));
let options = WindowOptions {
window_bounds: Some(window_bounds),
// Start from the component defaults but nudge the traffic lights
diff --git a/src/ui/app.rs b/src/ui/app.rs
index bb372f08..09dc39a6 100644
--- a/src/ui/app.rs
+++ b/src/ui/app.rs
@@ -482,10 +482,13 @@ impl Tty7App {
let mf_description = cx.new(|cx| InputState::new(window, cx).placeholder("description"));
let sidebar_width = cx.global::().sidebar_width;
// Live-apply hot-reloaded config: the watcher in `main.rs` swaps the
- // `Config` global on every `config.json` change, which fires this. Theme
- // and colors are handled separately by `apply_theme`; here we cover the
- // font knobs that live on `Tty7App`/the panes.
- let config_watch = cx.observe_global::(|this, cx| this.reload_from_config(cx));
+ // `Config` global on every `config.json` change, which fires this. The
+ // window-aware variant so the reload can re-run `apply_theme` with the
+ // window (blur flip, traffic-light pinning) and re-sync the Appearance
+ // opacity slider — the watcher task itself has no window handle.
+ let config_watch = cx.observe_global_in::(window, |this, window, cx| {
+ this.reload_from_config(window, cx)
+ });
// Repaint when any pane's git probe lands in the shared cache — the
// sidebar's branch/diff lines read from it, and the probing pane's own
// notify wouldn't re-render rows belonging to *other* panes. The open
@@ -1014,7 +1017,12 @@ impl Tty7App {
}
/// Set the global window-blur override from the Appearance switch.
- pub(crate) fn set_window_blur(&mut self, on: bool, window: &mut Window, cx: &mut Context) {
+ pub(crate) fn set_window_blur(
+ &mut self,
+ on: bool,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
cx.global_mut::().window_blur = Some(on);
apply_theme(Some(window), cx);
cx.global::().save();
@@ -1038,7 +1046,11 @@ impl Tty7App {
/// Snap the Appearance opacity slider's thumb to the value now in effect.
/// Needed whenever that value changes for a reason other than the user
/// dragging it (theme switch, "Follow theme" reset).
- pub(crate) fn sync_window_opacity_slider(&mut self, window: &mut Window, cx: &mut Context) {
+ pub(crate) fn sync_window_opacity_slider(
+ &mut self,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
let eff = Self::effective_window_opacity(cx);
if let Some(slider) = self
.active_settings()
@@ -2969,15 +2981,13 @@ impl Tty7App {
.step(0.01)
.default_value(eff)
});
- subs.push(cx.subscribe_in(
- &slider,
- window,
- |this, _s, ev: &SliderEvent, window, cx| {
+ subs.push(
+ cx.subscribe_in(&slider, window, |this, _s, ev: &SliderEvent, window, cx| {
if let SliderEvent::Change(v) = ev {
this.set_window_opacity(v.start(), window, cx);
}
- },
- ));
+ }),
+ );
slider
}
@@ -3135,7 +3145,14 @@ impl Tty7App {
/// *our own* code mutated the global (every font setter and `set_preset`
/// writes it), and — because we never write the global or `save()` from here
/// — closes the save → watch → reload loop that would otherwise oscillate.
- fn reload_from_config(&mut self, cx: &mut Context) {
+ fn reload_from_config(&mut self, window: &mut Window, cx: &mut Context) {
+ // Re-apply the theme with the window in hand: the watcher task calls
+ // `apply_theme(None)` (colors/palette), but the window-bound effects — the
+ // Transparent↔Blurred background flip and traffic-light re-pinning — only
+ // happen here. Also keeps the Appearance opacity slider's thumb on a value
+ // that was hand-edited in `config.json` or the theme file.
+ apply_theme(Some(window), cx);
+ self.sync_window_opacity_slider(window, cx);
let config = cx.global::().clone();
if config.cursor_style != self.terminal_cursor_style
|| config.scrollback_limit != self.terminal_scrollback_limit
@@ -3954,7 +3971,9 @@ impl Render for Tty7App {
// Same gradient-aware paint as the root, so a gradient theme's
// settings page doesn't snap to a flat color. A translucent
// theme's alpha rides along, letting the background image show
- // through here too.
+ // through here too. This second layer compounds the alpha over
+ // the root's paint — deliberate: the overlay must occlude the
+ // terminal behind it to stay readable.
.bg(window_bg)
.child(self.render_settings(cx))
});
diff --git a/src/ui/diff_overlay.rs b/src/ui/diff_overlay.rs
index 4f1a8f0f..65e0d413 100644
--- a/src/ui/diff_overlay.rs
+++ b/src/ui/diff_overlay.rs
@@ -219,7 +219,17 @@ impl Tty7App {
.inset_0()
// Blocks mouse from reaching the terminal underneath.
.occlude()
- .bg(cx.theme().background)
+ // Same gradient/opacity-aware paint as the root and the settings
+ // overlay, so a gradient or image theme doesn't snap to a flat
+ // color here. On a translucent theme this second layer compounds
+ // the alpha a little — deliberate: the overlay must occlude the
+ // terminal behind it to stay readable.
+ .bg(
+ match cx.try_global::() {
+ Some(bg) => crate::ui::theme::window_background(bg),
+ None => cx.theme().background.into(),
+ },
+ )
.text_color(cx.theme().foreground)
.track_focus(&overlay.focus_handle)
.on_key_down(cx.listener(|this, ev: &KeyDownEvent, window, cx| {
diff --git a/src/ui/settings.rs b/src/ui/settings.rs
index 9beed4a8..12960419 100644
--- a/src/ui/settings.rs
+++ b/src/ui/settings.rs
@@ -1263,9 +1263,9 @@ impl Tty7App {
.into_any_element();
let blur_switch = Switch::new("window-blur")
.checked(blur)
- .on_click(cx.listener(|this, on: &bool, window, cx| {
- this.set_window_blur(*on, window, cx)
- }))
+ .on_click(
+ cx.listener(|this, on: &bool, window, cx| this.set_window_blur(*on, window, cx)),
+ )
.into_any_element();
v_flex()
@@ -1341,7 +1341,11 @@ impl Tty7App {
.w(px(240.))
.child(
Button::new("pick-theme-image")
- .label(if image.is_some() { "Change…" } else { "Choose…" })
+ .label(if image.is_some() {
+ "Change…"
+ } else {
+ "Choose…"
+ })
.small()
.on_click(cx.listener(|this, _, _w, cx| this.pick_theme_image(cx))),
)
diff --git a/src/ui/theme.rs b/src/ui/theme.rs
index 8fb837d2..eee5e9e2 100644
--- a/src/ui/theme.rs
+++ b/src/ui/theme.rs
@@ -117,10 +117,7 @@ pub(crate) fn apply_theme(mut window: Option<&mut Window>, cx: &mut App) {
// Window opacity / blur: the global config override wins when set (so a
// chosen translucency survives theme switches); otherwise the theme's own
// values apply. Only an opacity below 1.0 makes the window translucent.
- let opacity = config
- .window_opacity
- .or(theme.opacity)
- .filter(|o| *o < 1.0);
+ let opacity = config.window_opacity.or(theme.opacity).filter(|o| *o < 1.0);
let blur = config.window_blur.unwrap_or(theme.blur);
// Force the native macOS chrome (traffic lights, system menus, scrollbars)
// into the theme's own light/dark mode regardless of the OS setting.