默认深色改用参照微信的炭灰,不再是近黑

- ShellRS Dark 的界面和终端:底色 #1e1e1f,标题栏、标签栏、状态栏 #2f2f30,边框和悬停 #3b3b3d,文字 #e0e0e4;gpui-kit 的每级灰按对照表换掉,彩色不动
- 次要文字 #929296,比微信的亮一些,对比度保持在 4.5 以上
- 终端选区和 ANSI 黑按新底色重算;「界面跟随主题」关闭时的深色界面同样生效

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
XuzandClaude Opus 5.5 committed 2026-10-07 20:04:38 +08:00
1 parent cd3d41c496
commit baa6745c1a
6 files changed
+105 -31

No files matched your search

+3 -2
View File
@@ -68,7 +68,8 @@ ShellRS(crate 与二进制都叫 `shellrs`)是 Xshell / WinSCP 式的 SSH
- 远程文件原地改写:截断后写原文件,不走 `.filepart` 加改名。保存前核对,被改过就问「覆盖」。
- **设置 › 应用 › 窗口**:「记住窗口大小」(含最大化)和「记住窗口位置」两个开关,默认都开,下次启动生效。屏幕不在了或窗口不在屏幕上就居中,比屏幕大就缩小;全屏不记。
- **终端通知**:只做程序请求的(OSC 9 / 777)和响铃,不做 OSC 133 长命令通知:那要每台服务器装 shell 集成,收益不抵成本。不在前台发系统通知,在前台而终端在别的标签发应用内通知;响铃在终端就在眼前时不提醒。
- **主题**:「应用外观」(浅色 / 深色 / 跟随系统)决定深浅;「外观 › 主题」左右两栏里浅色、深色各选一套,按当前外观生效。组里第一项「界面跟随主题」默认关闭:界面是中性的黑白灰(gpui-kit 默认主题),只有终端按主题配色;打开后界面也按主题配色。用户试过默认跟随,觉得中性界面更合适,别改回去。页面「重置」对卡片和开关各管各的。
- **主题**:「应用外观」(浅色 / 深色 / 跟随系统)决定深浅;「外观 › 主题」左右两栏里浅色、深色各选一套,按当前外观生效。组里第一项「界面跟随主题」默认关闭:界面是中性的灰(浅色是 gpui-kit 默认主题,深色见下条),只有终端按主题配色;打开后界面也按主题配色。用户试过默认跟随,觉得中性界面更合适,别改回去。
- 默认深色(ShellRS Dark)不用 gpui-kit 的近黑,用参照微信深色的炭灰:底色 `#1e1e1f`,标题栏、标签栏、状态栏 `#2f2f30`,边框和悬停 `#3b3b3d`,文字 `#e0e0e4`。用户嫌近黑刺眼。次要文字比微信的 `#7b7b7f` 亮一些(`#929296`),保持对比度 ≥ 4.5。页面「重置」对卡片和开关各管各的。
- 左栏只列浅色主题,右栏只列深色主题;卡片是名称加示例输出,选中的整张加底色,右上角是蓝底(主题的 `blue`)对勾。
- 只有内置的,浅色、深色各 10 套,默认 ShellRS Light / Dark(底色与界面一致),Solarized 第二,Tokyo Day / Tokyo Night 第三。同一主题的浅、深两版在两栏同一行,没有另一版的(Rosé Pine Dawn、Everforest Light、Dracula、Nord)排最后。只收宽松许可(MIT、Apache-2.0)的配色。不做自定义主题、导入、按主机指定。
- **键盘快捷键**:设置左栏单独一个分类,排在「关键字高亮」下面,分通用、标签、终端、编辑器四组。
@@ -294,7 +295,7 @@ cargo test -- --ignored # 会读写真实钥匙串的测试
- **清屏只在本地模拟器里做**,全屏程序运行时不做。终端里的 ⌘K 是清屏,比全局的 `FocusSearch` 深一层,所以优先。
- **终端字体走全局量 `TerminalFont`**(终端模块不依赖 settings),格子在下一帧 prepaint 时按新尺寸重排。
- **终端颜色走全局量 `TerminalColors`**(`theme.rs`):`settings::apply` 按当前外观设成那一栏选中的主题,系统外观变化时也重设。终端格子、边距、选区、链接、颜色查询(OSC 4 / 10 / 11 / 12)和设置里的两个预览都从它取,不用界面主题的 `background` / `foreground`;查找高亮仍用界面的 `warning`。主题的 `key` 存进 `settings.json`,发布后不能改;读不出或深浅不符的 key 退回该外观的默认。
- **界面配色由所选主题推出**(`settings/app_theme.rs`):从 gpui-kit 默认的浅色或深色主题出发重新着色。灰色按它在默认背景与文字之间的位置,换到该主题背景与文字之间;默认里用作可读文字的灰色(对比度 ≥ 4.5)至少保留该主题正文对比度的 80%(最高 4.5)。`base.*` 按名字换成对应的 ANSI 色,其余彩色按色相换;`.light` 和彩色的 `*foreground` 删掉,交给 gpui-kit 自己推。ShellRS Light / Dark 和「界面跟随主题」关闭时都直接用 gpui-kit 的默认主题(`TerminalThemeSettings::app_theme`)。`settings::apply` 按名字(`theme_name`)判断要不要重建,把结果放进 `Theme` 的浅色或深色槽再 `Theme::change`。
- **界面配色由所选主题推出**(`settings/app_theme.rs`):从 gpui-kit 默认的浅色或深色主题出发重新着色。灰色按它在默认背景与文字之间的位置,换到该主题背景与文字之间;默认里用作可读文字的灰色(对比度 ≥ 4.5)至少保留该主题正文对比度的 80%(最高 4.5)。`base.*` 按名字换成对应的 ANSI 色,其余彩色按色相换;`.light` 和彩色的 `*foreground` 删掉,交给 gpui-kit 自己推。ShellRS Light(以及「界面跟随主题」关闭时的浅色,`TerminalThemeSettings::app_theme`)直接用 gpui-kit 的默认浅色主题;ShellRS Dark(以及关闭时的深色)是 gpui-kit 默认深色主题按 `SOFT_DARK` 把每级灰换成炭灰,彩色不动。`settings::apply` 按名字(`theme_name`)判断要不要重建,把结果放进 `Theme` 的浅色或深色槽再 `Theme::change`。
- **链接**:正则找出的地址和 OSC 8 标出的都算,只认 http / https(`links.rs`);OSC 8 的格子优先于正则。一直画成蓝色(终端主题的 ANSI 蓝,悬停时亮蓝;界面主题的 `link` 和正文同色)加下划线,⌘ / Ctrl 单击才打开,普通单击照旧选文字。
- **鼠标上报**(`mouse.rs` 是纯编码):按住 Shift 不上报,右键始终是自己的菜单。写入走 `engine.write`,不算用户输入。
- **关键字高亮只叠加在显示上**(`highlight.rs`)。`snapshot()` 每帧对可见行把折行接成逻辑行,用 `regex` crate 匹配,结果写进 `TerminalCell.highlight`,prepaint 改文字色和粗细;不存坐标,所以缩放重排、回滚都不用失效处理。
+1 -1
View File
@@ -106,7 +106,7 @@ ShellRS 同一时间只运行一个:已经开着时再打开一次,会把已
- **外观 → 常规 → 界面语言**:跟随系统、简体中文、English,默认简体中文。英文界面尚在翻译中,目前只有 GPUI Kit 组件自带的文字(如搜索框提示)会切换。
- **外观 → 常规 → 应用外观**:深色、浅色、跟随系统,默认跟随系统;选「跟随系统」时随系统的深浅色切换。标题栏的主题按钮也会改写这一项。
- **外观 → 主题**:左栏是浅色主题,右栏是深色主题,各选一套:应用外观为浅色时用左栏选中的那套,深色时用右栏的;跟随系统时随系统一起切换,标题栏的主题按钮切换时也一样。默认只有终端按所选主题配色,界面保持中性的黑白灰;打开上方的「界面跟随主题」,界面也按主题配色:侧栏、标签栏、对话框等用主题的底色和文字色,成功、警告、错误等状态用主题的绿、黄、红。每张卡片用该主题的颜色显示一段示例,选中的右上角有蓝色对勾。默认是 ShellRS Light 和 ShellRS Dark,即原来的界面配色。内置浅色、深色各 10 套:浅色 ShellRS Light、Solarized Light、Tokyo Day、Catppuccin Latte、Flexoki Light、One Half Light、Gruvbox Light、Kanagawa Lotus、Rosé Pine Dawn、Everforest Light;深色 ShellRS Dark、Solarized Dark、Tokyo Night、Catppuccin Mocha、Flexoki Dark、One Half Dark、Gruvbox Dark、Kanagawa Wave、Dracula、Nord。同一主题的浅色版和深色版在两栏的同一行。改动立即作用于所有已打开的终端,字体和关键字高亮的预览也用当前主题;vim 等程序询问终端的背景色时,得到的是当前主题的颜色。**尚未实现:自定义主题、导入配色、按主机指定主题。**
- **外观 → 主题**:左栏是浅色主题,右栏是深色主题,各选一套:应用外观为浅色时用左栏选中的那套,深色时用右栏的;跟随系统时随系统一起切换,标题栏的主题按钮切换时也一样。默认只有终端按所选主题配色,界面保持中性的灰色调(深色是不刺眼的炭灰,不是纯黑);打开上方的「界面跟随主题」,界面也按主题配色:侧栏、标签栏、对话框等用主题的底色和文字色,成功、警告、错误等状态用主题的绿、黄、红。每张卡片用该主题的颜色显示一段示例,选中的右上角有蓝色对勾。默认是 ShellRS Light 和 ShellRS Dark,与界面同色。内置浅色、深色各 10 套:浅色 ShellRS Light、Solarized Light、Tokyo Day、Catppuccin Latte、Flexoki Light、One Half Light、Gruvbox Light、Kanagawa Lotus、Rosé Pine Dawn、Everforest Light;深色 ShellRS Dark、Solarized Dark、Tokyo Night、Catppuccin Mocha、Flexoki Dark、One Half Dark、Gruvbox Dark、Kanagawa Wave、Dracula、Nord。同一主题的浅色版和深色版在两栏的同一行。改动立即作用于所有已打开的终端,字体和关键字高亮的预览也用当前主题;vim 等程序询问终端的背景色时,得到的是当前主题的颜色。**尚未实现:自定义主题、导入配色、按主机指定主题。**
- **终端 → 字体配置**:字体(只列出本机安装的等宽字体,默认是系统的等宽字体,macOS 上为 Menlo)、字号(10–28 像素,默认 13)、行高(字号的 1.0–2.0 倍,默认 1.54,即原来的 20 像素行高)。改动立即作用于所有已打开的终端;在终端里按 ⌘= / ⌘-(其他平台 Ctrl)也能逐级调字号,⌘0 回到默认。下方的预览按终端的排法显示字母、数字、易混字符、符号、中文、制表符和一行命令。设置文件里的字体在本机没有安装时,使用默认字体。
- **关键字高亮**:总开关、预览和规则表,终端里按规则给文字上色、匹配时提醒,见「终端」一节。
- **键盘快捷键**:主要功能的快捷键,分通用、标签、终端、编辑器四组,每行是功能名和它的按键。点按键进入「按下组合键」,按下新的组合就换上;Esc 或点别处放弃。行尾两个按钮:禁用(之后显示「无」,再点一下恢复默认)、恢复默认;页面上方的「重置」也能逐项恢复。改动立即生效,工具栏提示和右键菜单里显示的按键跟着变。
+91 -18
View File
@@ -3,7 +3,8 @@
//! keeps its place between the default's background and text, now between
//! the theme's; each color becomes the theme's ANSI color of its hue. What
//! gpui-kit derives from these (hover, active, tab bar, status bar …)
//! follows. The theme's own default is gpui-kit's theme unchanged.
//! follows. ShellRS Light is gpui-kit's light theme unchanged, ShellRS Dark
//! its dark theme with the grays lifted off black.
use std::rc::Rc;
@@ -15,7 +16,7 @@ use crate::terminal::TerminalTheme;
/// The name the app's theme has while `theme` is chosen, to tell whether it
/// is already in effect without building it again.
pub(super) fn ui_theme_name(theme: &'static TerminalTheme, cx: &App) -> SharedString {
if is_default(theme) {
if is_gpui_kits(theme) {
default_config(theme.mode(), cx).name.clone()
} else {
theme.name().into()
@@ -25,14 +26,78 @@ pub(super) fn ui_theme_name(theme: &'static TerminalTheme, cx: &App) -> SharedSt
/// The app's theme while `theme` is chosen.
pub(super) fn ui_theme(theme: &'static TerminalTheme, cx: &App) -> Rc<ThemeConfig> {
let default = default_config(theme.mode(), cx);
if is_default(theme) {
return default.clone();
if is_gpui_kits(theme) {
default.clone()
} else if theme == TerminalTheme::default_for(ThemeMode::Dark) {
Rc::new(soften(default, theme))
} else {
Rc::new(recolor(default, theme))
}
Rc::new(recolor(default, theme))
}
fn is_default(theme: &'static TerminalTheme) -> bool {
theme == TerminalTheme::default_for(theme.mode())
/// ShellRS Light: the app is gpui-kit's light theme as it is.
fn is_gpui_kits(theme: &'static TerminalTheme) -> bool {
theme == TerminalTheme::default_for(ThemeMode::Light)
}
/// ShellRS Dark's grays, from WeChat's dark mode, which is easy on the eyes
/// where gpui-kit's near black glares: each of gpui-kit's neutral grays (by
/// value) and what it becomes. Grays between two are mixed from them.
const SOFT_DARK: [(u8, u32); 13] = [
// Overlays stay black.
(0x00, 0x000000),
// neutral-950: the background, sidebars, dialogs.
(0x0a, 0x1e1e1f),
// neutral-900: the title, tab and status bars.
(0x17, 0x2f2f30),
// neutral-800: borders, hover, selected tabs and rows.
(0x26, 0x3b3b3d),
(0x40, 0x48484a),
// neutral-600: scrollbars.
(0x52, 0x626263),
// neutral-500: the focus ring.
(0x73, 0x7b7b7f),
// neutral-400: secondary text, lighter than WeChat's to stay readable.
(0xa3, 0x929296),
(0xd4, 0xc4c4c8),
(0xe5, 0xd2d2d6),
(0xf5, 0xdcdce0),
// neutral-50: the text.
(0xfa, 0xe0e0e4),
(0xff, 0xe8e8ec),
];
/// ShellRS Dark: gpui-kit's dark theme with its grays moved onto
/// `SOFT_DARK`. Its colors (selection, success, danger …) stay gpui-kit's.
fn soften(default: &ThemeConfig, theme: &TerminalTheme) -> ThemeConfig {
let mut config = serde_json::to_value(default).expect("a theme is plain data");
config["name"] = theme.name().into();
config["is_default"] = false.into();
if let Some(colors) = config["colors"].as_object_mut() {
for value in colors.values_mut() {
let Some(color) = value.as_str().and_then(|text| try_parse_color(text).ok()) else {
continue;
};
let color = color.to_rgb();
if is_gray(color) {
*value = hex(soft_gray(color)).into();
}
}
}
serde_json::from_value(config).expect("written from a theme")
}
fn soft_gray(color: Rgba) -> Rgba {
let value = gray_value(color) * 255.;
let pair = SOFT_DARK
.windows(2)
.find(|pair| value <= f32::from(pair[1].0))
.unwrap_or(&SOFT_DARK[SOFT_DARK.len() - 2..]);
let ((from, from_color), (to, to_color)) = (pair[0], pair[1]);
let t = (value - f32::from(from)) / f32::from(to - from);
let mut soft = mix(rgb(from_color), rgb(to_color), t);
soft.a = color.a;
soft
}
fn default_config(mode: ThemeMode, cx: &App) -> &Rc<ThemeConfig> {
@@ -300,24 +365,32 @@ mod tests {
}
#[gpui_kit::test]
fn the_default_themes_are_gpui_kits_own(cx: &mut TestAppContext) {
fn the_light_default_is_gpui_kits_own(cx: &mut TestAppContext) {
cx.update(|cx| {
gpui_kit::component::init(cx);
let white = cx.theme().background;
apply(&TerminalTheme::all()[1], cx);
assert_ne!(cx.theme().background, white);
for mode in [ThemeMode::Light, ThemeMode::Dark] {
apply(TerminalTheme::default_for(mode), cx);
assert_eq!(
cx.theme().theme_name().as_ref(),
match mode {
ThemeMode::Light => "Default Light",
ThemeMode::Dark => "Default Dark",
}
);
}
apply(TerminalTheme::default_for(ThemeMode::Light), cx);
assert_eq!(cx.theme().theme_name().as_ref(), "Default Light");
assert_eq!(cx.theme().background, white);
});
}
#[gpui_kit::test]
fn the_dark_default_is_charcoal_with_gpui_kits_colors(cx: &mut TestAppContext) {
cx.update(|cx| {
gpui_kit::component::init(cx);
apply(TerminalTheme::default_for(ThemeMode::Dark), cx);
let app = cx.theme();
assert_eq!(app.theme_name().as_ref(), "ShellRS Dark");
// Bars a step lighter than the background, borders another.
assert_close(app.background.to_rgb(), color(0x1e1e1f), "background");
assert_close(app.title_bar.to_rgb(), color(0x2f2f30), "title bar");
assert_close(app.border.to_rgb(), color(0x3b3b3d), "border");
assert_close(app.foreground.to_rgb(), color(0xe0e0e4), "text");
assert_close(app.selection.to_rgb(), color(0x1d4ed8), "selection");
assert_close(app.danger.to_rgb(), color(0xf87171), "danger");
});
}
}
+1 -1
View File
@@ -86,7 +86,7 @@ impl Default for TerminalThemeSettings {
impl TerminalThemeSettings {
/// The theme the app's colors come from for `mode`: the chosen one, or
/// the default, which is gpui-kit's own, when the app does not follow.
/// the default, ShellRS Light or Dark, when the app does not follow.
pub fn app_theme(&self, mode: ThemeMode) -> &'static TerminalTheme {
if self.app_follows {
self.theme(mode)
+8 -8
View File
@@ -170,19 +170,19 @@ const SHELLRS_LIGHT: TerminalTheme = TerminalTheme {
],
};
/// The app's dark theme around a terminal: `neutral-950`, `neutral-50`
/// The app's dark theme around a terminal: its charcoal background and soft
/// text and cursor, the `selection` over the background, with ANSI colors
/// light enough to read on it.
/// light enough to read on it; black is the app's borders.
const SHELLRS_DARK: TerminalTheme = TerminalTheme {
key: "shellrs-dark",
name: "ShellRS Dark",
mode: ThemeMode::Dark,
foreground: 0xfafafa,
background: 0x0a0a0a,
cursor: 0xfafafa,
selection: 0x101e48,
foreground: 0xe0e0e4,
background: 0x1e1e1f,
cursor: 0xe0e0e4,
selection: 0x1e2c56,
ansi: [
0x262626, 0xf87171, 0x4ade80, 0xfacc15, 0x60a5fa, 0xe879f9, 0x22d3ee, 0xd4d4d4, 0x737373,
0x3b3b3d, 0xf87171, 0x4ade80, 0xfacc15, 0x60a5fa, 0xe879f9, 0x22d3ee, 0xd4d4d4, 0x737373,
0xfca5a5, 0x86efac, 0xfde047, 0x93c5fd, 0xf0abfc, 0x67e8f9, 0xfafafa,
],
};
@@ -610,7 +610,7 @@ mod tests {
let dark = TerminalTheme::default_for(ThemeMode::Dark);
assert_eq!(
dark.query_color(NamedColor::Background as usize),
rgb(0x0a0a0a)
rgb(0x1e1e1f)
);
}
}
+1 -1
View File
@@ -373,7 +373,7 @@ fn terminal_themes_follow_the_appearance_and_a_card_chooses_one(cx: &mut TestApp
let neutral = |cx: &mut TestAppContext| {
cx.update(|cx| {
let name = cx.theme().theme_name().clone();
name == "Default Light" || name == "Default Dark"
name == "Default Light" || name == "ShellRS Dark"
})
};
assert!(neutral(cx));