From 1bf2445ecbd227e6a41b9b4b5381d294fa7e2aa1 Mon Sep 17 00:00:00 2001 From: kuddev Date: Sat, 12 Sep 2026 13:20:15 +0800 Subject: [PATCH] feat: complete Catppuccin themes and refine terminal rendering Add Frappe and Macchiato, unify themed pane surfaces, and default to opaque backgrounds without blur while preserving explicit appearance settings. Keep default dividers one physical pixel wide at high display scaling. Align rounded box strokes with adjacent cells and preserve modern Grok console rendering for new local Windows terminals unless overridden. Include the regression coverage and synchronized bilingual release notes. --- CHANGELOG.md | 12 +- docs/release-notes/v1.7.0.md | 12 +- nebula_app/src/display/command_palette.rs | 213 ++--------------- .../src/display/command_palette/catalog.rs | 215 ++++++++++++++++++ nebula_app/src/display/settings.rs | 20 +- nebula_app/src/display/ui/theme.rs | 16 ++ nebula_app/src/gpui_shell/settings_pane.rs | 3 +- .../settings_pane/appearance_picker_tests.rs | 50 +++- .../gpui_shell/settings_pane/navigation.rs | 2 +- .../gpui_shell/settings_pane/theme_picker.rs | 2 +- nebula_app/src/gpui_shell/terminal/session.rs | 5 +- nebula_app/src/gpui_shell/theme.rs | 86 ++++--- nebula_app/src/gpui_shell/workspace.rs | 50 ++-- .../src/gpui_shell/workspace/file_tree.rs | 9 +- .../src/gpui_shell/workspace/remote_files.rs | 3 - .../src/gpui_shell/workspace/sidebar.rs | 9 - nebula_app/src/gpui_shell/workspace/tests.rs | 16 +- .../src/gpui_shell/workspace/vcs_panel.rs | 6 +- .../gpui_shell/workspace/window_titlebar.rs | 102 ++++++++- nebula_app/src/platform/environment.rs | 123 +++++++++- nebula_app/src/window_context.rs | 5 +- nebula_settings/src/lib.rs | 67 +++--- nebula_settings/src/reset.rs | 4 +- nebula_settings/src/themes.rs | 53 ++++- nebula_terminal/src/render/boxdraw.rs | 96 ++++++-- scripts/platform_cfg_budget.txt | 2 +- 26 files changed, 793 insertions(+), 388 deletions(-) create mode 100644 nebula_app/src/display/command_palette/catalog.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index eedb68c..b61290c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,12 @@ Every release entry is provided in English and Simplified Chinese. - Added editable local and SSH text documents with save operations, changed-file checks, and preservation of unsaved text when a save fails. - Added reusable terminal layouts that open in a separate window and retain terminal launch settings without replaying command history. - Added dragging files from the system file manager into local and WSL terminals to insert quoted paths without executing them. Local files for SSH sessions can be uploaded through Remote Files. Addresses [#106](https://github.com/Kuddev/nebula/issues/106). -- Expanded the built-in theme picker to 13 palettes, including Breeze, Mint, Catppuccin Mocha/Latte, and Glass light/dark variants. Glass uses static colors and does not automatically enable background blur. +- Expanded the built-in theme picker to 15 palettes. New Catppuccin Frappé and Macchiato join Latte and Mocha to complete all four flavors, alongside Breeze, Mint, and Glass light/dark variants. Glass uses static colors and does not automatically enable background blur. #### Fixed +- Fixed rounded terminal box corners joining straight strokes one physical pixel off at some character sizes and display scales. +- Prevented Grok's Windows legacy-console fallback from hiding its dotted welcome logo in newly opened local terminals, while respecting explicit legacy-console overrides. - Fixed the Default Shell setting and the new-terminal menu recommending PowerShell on macOS when no shell is configured. Both now use the same host-default resolver as the terminal backend. Contributed by [@YinBuLiao](https://github.com/YinBuLiao) in [#110](https://github.com/Kuddev/pebrel/pull/110). - Fixed mixed shell icons giving settings-menu rows different heights. Shells without a brand image also receive an icon in the new-terminal menu. Contributed by [@YinBuLiao](https://github.com/YinBuLiao) in [#111](https://github.com/Kuddev/pebrel/pull/111). - Shortened long and multi-line notification previews while retaining the original notification text in logs. Plain persistent notifications can be dismissed by clicking the card. Contributed in part by [@YinBuLiao](https://github.com/YinBuLiao) in [#112](https://github.com/Kuddev/pebrel/pull/112). @@ -27,6 +29,8 @@ Every release entry is provided in English and Simplified Chinese. #### Improved +- Changed the default appearance to 100% opacity with background blur off, so themes use solid backgrounds. Explicit opacity and blur choices are preserved. +- Made default sidebar and file-pane separators stay one physical pixel wide at high display scaling, retaining each theme's divider color. - Improved Markdown reading with bounded preview work and shared background formula rendering; unsupported or oversized expressions retain their source. - Added in-place feedback to code and formula copy controls, and kept document focus mode separate from operating-system fullscreen. - Made Git-ignored files and directories distinguishable with italic text in the file tree. @@ -41,10 +45,12 @@ Windows packages include an installer and portable ZIP. Linux x64 packages and m - 新增本地和 SSH 文本文档编辑,支持保存、文件外部变更检查,并在保存失败时保留未保存的内容。 - 新增可复用的终端布局,在独立窗口中打开并保留终端启动设置,不重放命令历史。 - 新增从系统文件管理器向本地和 WSL 终端拖入文件,插入加引号的路径而不执行;SSH 会话所需的本地文件可通过“远程文件”上传。对应 [#106](https://github.com/Kuddev/nebula/issues/106)。 -- 将内置主题选择器扩展为 13 套配色,包含 Breeze、Mint、Catppuccin Mocha/Latte,以及 Glass 浅色/深色。Glass 使用静态配色,不会自动开启背景模糊。 +- 将内置主题选择器扩展为 15 套配色。新增 Catppuccin Frappé 和 Macchiato,与已有的 Latte、Mocha 补齐四款配色,同时包含 Breeze、Mint 及 Glass 浅色/深色主题。Glass 使用静态配色,不会自动开启背景模糊。 #### 修复 +- 修复终端圆角框线在部分字符尺寸和显示缩放下,与相邻直线错开一个物理像素的问题。 +- 避免 Grok 在新开的 Windows 本地终端中因错误启用旧控制台模式而隐藏点阵欢迎图案,同时保留用户显式设置的旧控制台覆盖值。 - 修复 macOS 未配置 Shell 时,“默认 Shell”设置和新建终端菜单推荐 PowerShell 的问题。两处现在与终端后端共用宿主默认 Shell 解析。由 [@YinBuLiao](https://github.com/YinBuLiao) 在 [#110](https://github.com/Kuddev/pebrel/pull/110) 中贡献。 - 修复不同 Shell 图标使设置菜单行高不一致的问题;没有品牌图片的 Shell 在新建终端菜单中也会显示图标。由 [@YinBuLiao](https://github.com/YinBuLiao) 在 [#111](https://github.com/Kuddev/pebrel/pull/111) 中贡献。 - 缩短长通知和多行通知的预览,同时在日志中保留通知原文。普通驻留通知可点击卡片关闭。部分修复由 [@YinBuLiao](https://github.com/YinBuLiao) 在 [#112](https://github.com/Kuddev/pebrel/pull/112) 中贡献。 @@ -54,6 +60,8 @@ Windows packages include an installer and portable ZIP. Linux x64 packages and m #### 改进 +- 默认关闭背景模糊,并使用 100% 不透明度,让主题按实色显示;保留用户已明确设置的模糊和不透明度。 +- 侧栏和文件区的默认分隔线在高缩放下保持单个物理像素宽度,并沿用主题分隔线颜色,让边界更轻。 - 改进 Markdown 阅读,限制预览处理量,并共享后台公式渲染;不支持或超限的表达式保留源码。 - 为代码和公式复制控件加入就地反馈,并将文档专注模式与系统全屏分开。 - 文件树中被 Git 忽略的文件和目录使用斜体显示,便于区分。 diff --git a/docs/release-notes/v1.7.0.md b/docs/release-notes/v1.7.0.md index 3aa0fd1..55a89d0 100644 --- a/docs/release-notes/v1.7.0.md +++ b/docs/release-notes/v1.7.0.md @@ -8,10 +8,12 @@ - Added editable local and SSH text documents with save operations, changed-file checks, and preservation of unsaved text when a save fails. - Added reusable terminal layouts that open in a separate window and retain terminal launch settings without replaying command history. - Added dragging files from the system file manager into local and WSL terminals to insert quoted paths without executing them. Local files for SSH sessions can be uploaded through Remote Files. Addresses [#106](https://github.com/Kuddev/nebula/issues/106). -- Expanded the built-in theme picker to 13 palettes, including Breeze, Mint, Catppuccin Mocha/Latte, and Glass light/dark variants. Glass uses static colors and does not automatically enable background blur. +- Expanded the built-in theme picker to 15 palettes. New Catppuccin Frappé and Macchiato join Latte and Mocha to complete all four flavors, alongside Breeze, Mint, and Glass light/dark variants. Glass uses static colors and does not automatically enable background blur. ### Fixed +- Fixed rounded terminal box corners joining straight strokes one physical pixel off at some character sizes and display scales. +- Prevented Grok's Windows legacy-console fallback from hiding its dotted welcome logo in newly opened local terminals, while respecting explicit legacy-console overrides. - Fixed the Default Shell setting and the new-terminal menu recommending PowerShell on macOS when no shell is configured. Both now use the same host-default resolver as the terminal backend. Contributed by [@YinBuLiao](https://github.com/YinBuLiao) in [#110](https://github.com/Kuddev/pebrel/pull/110). - Fixed mixed shell icons giving settings-menu rows different heights. Shells without a brand image also receive an icon in the new-terminal menu. Contributed by [@YinBuLiao](https://github.com/YinBuLiao) in [#111](https://github.com/Kuddev/pebrel/pull/111). - Shortened long and multi-line notification previews while retaining the original notification text in logs. Plain persistent notifications can be dismissed by clicking the card. Contributed in part by [@YinBuLiao](https://github.com/YinBuLiao) in [#112](https://github.com/Kuddev/pebrel/pull/112). @@ -21,6 +23,8 @@ ### Improved +- Changed the default appearance to 100% opacity with background blur off, so themes use solid backgrounds. Explicit opacity and blur choices are preserved. +- Made default sidebar and file-pane separators stay one physical pixel wide at high display scaling, retaining each theme's divider color. - Improved Markdown reading with bounded preview work and shared background formula rendering; unsupported or oversized expressions retain their source. - Added in-place feedback to code and formula copy controls, and kept document focus mode separate from operating-system fullscreen. - Made Git-ignored files and directories distinguishable with italic text in the file tree. @@ -35,10 +39,12 @@ Windows packages include an installer and portable ZIP. Linux x64 packages and m - 新增本地和 SSH 文本文档编辑,支持保存、文件外部变更检查,并在保存失败时保留未保存的内容。 - 新增可复用的终端布局,在独立窗口中打开并保留终端启动设置,不重放命令历史。 - 新增从系统文件管理器向本地和 WSL 终端拖入文件,插入加引号的路径而不执行;SSH 会话所需的本地文件可通过“远程文件”上传。对应 [#106](https://github.com/Kuddev/nebula/issues/106)。 -- 将内置主题选择器扩展为 13 套配色,包含 Breeze、Mint、Catppuccin Mocha/Latte,以及 Glass 浅色/深色。Glass 使用静态配色,不会自动开启背景模糊。 +- 将内置主题选择器扩展为 15 套配色。新增 Catppuccin Frappé 和 Macchiato,与已有的 Latte、Mocha 补齐四款配色,同时包含 Breeze、Mint 及 Glass 浅色/深色主题。Glass 使用静态配色,不会自动开启背景模糊。 ### 修复 +- 修复终端圆角框线在部分字符尺寸和显示缩放下,与相邻直线错开一个物理像素的问题。 +- 避免 Grok 在新开的 Windows 本地终端中因错误启用旧控制台模式而隐藏点阵欢迎图案,同时保留用户显式设置的旧控制台覆盖值。 - 修复 macOS 未配置 Shell 时,“默认 Shell”设置和新建终端菜单推荐 PowerShell 的问题。两处现在与终端后端共用宿主默认 Shell 解析。由 [@YinBuLiao](https://github.com/YinBuLiao) 在 [#110](https://github.com/Kuddev/pebrel/pull/110) 中贡献。 - 修复不同 Shell 图标使设置菜单行高不一致的问题;没有品牌图片的 Shell 在新建终端菜单中也会显示图标。由 [@YinBuLiao](https://github.com/YinBuLiao) 在 [#111](https://github.com/Kuddev/pebrel/pull/111) 中贡献。 - 缩短长通知和多行通知的预览,同时在日志中保留通知原文。普通驻留通知可点击卡片关闭。部分修复由 [@YinBuLiao](https://github.com/YinBuLiao) 在 [#112](https://github.com/Kuddev/pebrel/pull/112) 中贡献。 @@ -48,6 +54,8 @@ Windows packages include an installer and portable ZIP. Linux x64 packages and m ### 改进 +- 默认关闭背景模糊,并使用 100% 不透明度,让主题按实色显示;保留用户已明确设置的模糊和不透明度。 +- 侧栏和文件区的默认分隔线在高缩放下保持单个物理像素宽度,并沿用主题分隔线颜色,让边界更轻。 - 改进 Markdown 阅读,限制预览处理量,并共享后台公式渲染;不支持或超限的表达式保留源码。 - 为代码和公式复制控件加入就地反馈,并将文档专注模式与系统全屏分开。 - 文件树中被 Git 忽略的文件和目录使用斜体显示,便于区分。 diff --git a/nebula_app/src/display/command_palette.rs b/nebula_app/src/display/command_palette.rs index 4699cff..e211a8c 100644 --- a/nebula_app/src/display/command_palette.rs +++ b/nebula_app/src/display/command_palette.rs @@ -17,6 +17,10 @@ use crate::config::ui_config::Profile; use crate::shell_detect::DetectedShell; use unicode_width::UnicodeWidthChar; +#[path = "command_palette/catalog.rs"] +mod catalog; +use catalog::ITEMS; + /// A dynamic quick-launch row: a config profile (launched by index) or a /// detected shell (spec carried inline). Built fresh on every menu open. #[derive(Debug, Clone)] @@ -258,199 +262,6 @@ pub(crate) struct PaletteItem { pub(crate) action: PaletteAction, } -/// The full action table, in declaration order (also the tie-break order when -/// fuzzy scores are equal, and the order shown for an empty query). -const fn theme_item(theme: NebulaTheme, search: &'static str) -> PaletteItem { - PaletteItem { - label: theme.command_label(), - hint: "", - search, - action: PaletteAction::SelectTheme(theme), - } -} - -const ITEMS: &[PaletteItem] = &[ - PaletteItem { - label: "新建标签页", - hint: "Ctrl+Shift+T", - search: "新建标签页 new tab xinjian biaoqianye", - action: PaletteAction::NewTab, - }, - PaletteItem { - label: "复制路径", - hint: "", - search: "复制路径 copy path cwd working directory fuzhi lujing gongzuo mulu", - action: PaletteAction::CopyCwd, - }, - PaletteItem { - label: "在资源管理器中显示", - hint: "", - search: "在资源管理器中显示 reveal open explorer file manager folder ziyuan guanliqi \ - wenjianjia", - action: PaletteAction::RevealCwd, - }, - PaletteItem { - label: "在常用目录中新建终端…", - hint: "", - search: "在常用目录中新建终端 new terminal in frequent directory changyong mulu", - action: PaletteAction::OpenDirectoryPicker, - }, - PaletteItem { - // GPUI 已把 Tabs、Panes、目录、SSH 与 AI 会话投影进同一个母入口; - // `OpenAiSessionPicker` 这个旧 action 名暂留给 legacy shell 兼容,避免 - // 为两个壳复制第二份命令目录。 - label: "快速跳转…", - hint: "Ctrl+Shift+O", - search: "快速跳转 标签 分屏 目录 SSH AI 会话 open quickly jump tab pane directory ssh resume ai session claude codex kuaisu tiaozhuan biaoqian fenping mulu huifu", - action: PaletteAction::OpenAiSessionPicker, - }, - PaletteItem { - label: "关闭标签页", - hint: "Ctrl+Shift+W", - search: "关闭标签页 close tab guanbi", - action: PaletteAction::CloseTab, - }, - PaletteItem { - label: "下一个标签页", - hint: "Ctrl+Tab", - search: "下一个标签页 next tab xiayige", - action: PaletteAction::NextTab, - }, - PaletteItem { - label: "上一个标签页", - hint: "Ctrl+Shift+Tab", - search: "上一个标签页 previous prev tab shangyige", - action: PaletteAction::PrevTab, - }, - PaletteItem { - label: "新建窗口", - hint: "Ctrl+Shift+E", - search: "新建窗口 new window xinjian chuangkou", - action: PaletteAction::NewWindow, - }, - PaletteItem { - label: "左右分屏", - hint: "Ctrl+Shift+D", - search: "左右分屏 split right vertical zuoyou fenping", - action: PaletteAction::SplitRight, - }, - PaletteItem { - label: "上下分屏", - hint: "Ctrl+Shift+S", - search: "上下分屏 split down horizontal shangxia fenping", - action: PaletteAction::SplitDown, - }, - PaletteItem { - label: "导出工作区…", - hint: "", - search: "导出工作区 export workspace save session daochu gongzuoqu", - action: PaletteAction::ExportWorkspace, - }, - PaletteItem { - label: "打开工作区…", - hint: "", - search: "打开工作区 open import workspace load session dakai daoru gongzuoqu", - action: PaletteAction::ImportWorkspace, - }, - PaletteItem { - label: "目录树面板", - hint: "Ctrl+Shift+F", - search: "目录树面板 files tree explorer panel mulushu wenjian", - action: PaletteAction::ToggleFilesPanel, - }, - PaletteItem { - label: "显示标签侧栏", - hint: "", - search: "显示标签侧栏 toggle show hide tab sidebar xianshi biaoqian celan", - action: PaletteAction::ToggleSidebar, - }, - PaletteItem { - label: "拖拽调节侧栏宽度", - hint: "", - search: "拖拽调节侧栏宽度 drag resize sidebar drawer panel width tuozhuai tiaojie kuandu", - action: PaletteAction::TogglePanelResize, - }, - PaletteItem { - label: "Git 面板", - hint: "Ctrl+Shift+G", - search: "git 面板 status branch panel mianban", - action: PaletteAction::ToggleGitPanel, - }, - PaletteItem { - label: "打开设置", - hint: "", - search: "打开设置 open settings preferences dakai shezhi", - action: PaletteAction::OpenSettings, - }, - PaletteItem { - label: "打开配置文件", - hint: "", - search: "打开配置文件 open config file dakai peizhi wenjian", - action: PaletteAction::OpenSettingsFile, - }, - PaletteItem { - label: "同步:推送设置到云端", - hint: "", - search: "同步推送设置到云端 webdav sync push upload settings tongbu tuisong", - action: PaletteAction::SyncPush, - }, - PaletteItem { - label: "同步:从云端拉取设置", - hint: "", - search: "同步从云端拉取设置 webdav sync pull download settings tongbu laqu", - action: PaletteAction::SyncPull, - }, - PaletteItem { - label: "切换行内补全 (Ghost)", - hint: "", - search: "切换行内补全 toggle ghost completion qiehuan buquan", - action: PaletteAction::ToggleGhost, - }, - PaletteItem { - label: "切换补全接受键", - hint: "", - search: "切换补全接受键 cycle accept key completion jieshou", - action: PaletteAction::CycleAccept, - }, - PaletteItem { - label: "切换补全样式(行内 / 弹窗)", - hint: "", - search: "切换补全样式 行内 弹窗 completion style inline popup list buquan yangshi", - action: PaletteAction::CycleCompletionStyle, - }, - PaletteItem { - label: "选择背景图片…", - hint: "", - search: "选择背景图片 background image picture xuanze beijing tupian", - action: PaletteAction::PickBackgroundImage, - }, - PaletteItem { - label: "切换背景色", - hint: "", - search: "切换背景色 cycle background color qiehuan beijingse", - action: PaletteAction::CycleBackground, - }, - PaletteItem { - label: "恢复外观默认", - hint: "", - search: "恢复外观默认 reset appearance default huifu waiguan moren", - action: PaletteAction::ResetAppearance, - }, - theme_item(NebulaTheme::BreezeLight, "theme 主题 breeze light"), - theme_item(NebulaTheme::BreezeDark, "theme 主题 breeze dark"), - theme_item(NebulaTheme::MintLight, "theme 主题 mint light"), - theme_item(NebulaTheme::MintDark, "theme 主题 mint dark"), - theme_item(NebulaTheme::SilverLight, "theme 主题 silver light"), - theme_item(NebulaTheme::Nord, "theme 主题 nord dark"), - theme_item(NebulaTheme::Paper, "theme 主题 paper light"), - theme_item(NebulaTheme::LimestoneLight, "theme 主题 limestone light"), - theme_item(NebulaTheme::LinenLight, "theme 主题 linen light"), - theme_item(NebulaTheme::CatppuccinMocha, "theme 主题 catppuccin mocha dark"), - theme_item(NebulaTheme::CatppuccinLatte, "theme 主题 catppuccin latte light"), - theme_item(NebulaTheme::GlassLight, "theme 主题 glass light"), - theme_item(NebulaTheme::GlassDark, "theme 主题 glass dark"), -]; - /// Shared command catalog for alternate presentation layers. /// /// The old renderer and the GPUI overlay consume the same labels, aliases, @@ -3036,6 +2847,22 @@ mod tests { assert_eq!(palette.filtered.len(), full, "clearing the query restores the full list"); } + #[test] + fn catppuccin_flavor_search_confirms_the_matching_theme() { + for (query, theme) in [ + ("frappe", NebulaTheme::CatppuccinFrappe), + ("frappé", NebulaTheme::CatppuccinFrappe), + ("macchiato", NebulaTheme::CatppuccinMacchiato), + ] { + let mut palette = CommandPalette::new(); + palette.open(); + for ch in query.chars() { + palette.input_char(ch); + } + assert_eq!(palette.confirm(), Some(PaletteAction::SelectTheme(theme)), "{query}"); + } + } + #[test] fn move_selection_wraps_both_ends() { let mut palette = CommandPalette::new(); diff --git a/nebula_app/src/display/command_palette/catalog.rs b/nebula_app/src/display/command_palette/catalog.rs new file mode 100644 index 0000000..b77528f --- /dev/null +++ b/nebula_app/src/display/command_palette/catalog.rs @@ -0,0 +1,215 @@ +//! Static command metadata shared by both shell presentations. + +use super::{NebulaTheme, PaletteAction, PaletteItem}; + +/// Theme commands use the same labels as the theme metadata. +const fn theme_item(theme: NebulaTheme, search: &'static str) -> PaletteItem { + PaletteItem { + label: theme.command_label(), + hint: "", + search, + action: PaletteAction::SelectTheme(theme), + } +} + +/// The declaration order breaks equal fuzzy scores and orders empty queries. +pub(super) const ITEMS: &[PaletteItem] = &[ + PaletteItem { + label: "新建标签页", + hint: "Ctrl+Shift+T", + search: "新建标签页 new tab xinjian biaoqianye", + action: PaletteAction::NewTab, + }, + PaletteItem { + label: "复制路径", + hint: "", + search: "复制路径 copy path cwd working directory fuzhi lujing gongzuo mulu", + action: PaletteAction::CopyCwd, + }, + PaletteItem { + label: "在资源管理器中显示", + hint: "", + search: "在资源管理器中显示 reveal open explorer file manager folder ziyuan guanliqi \ + wenjianjia", + action: PaletteAction::RevealCwd, + }, + PaletteItem { + label: "在常用目录中新建终端…", + hint: "", + search: "在常用目录中新建终端 new terminal in frequent directory changyong mulu", + action: PaletteAction::OpenDirectoryPicker, + }, + PaletteItem { + // GPUI 已把 Tabs、Panes、目录、SSH 与 AI 会话投影进同一个母入口; + // `OpenAiSessionPicker` 这个旧 action 名暂留给 legacy shell 兼容,避免 + // 为两个壳复制第二份命令目录。 + label: "快速跳转…", + hint: "Ctrl+Shift+O", + search: "快速跳转 标签 分屏 目录 SSH AI 会话 open quickly jump tab pane directory ssh resume ai session claude codex kuaisu tiaozhuan biaoqian fenping mulu huifu", + action: PaletteAction::OpenAiSessionPicker, + }, + PaletteItem { + label: "关闭标签页", + hint: "Ctrl+Shift+W", + search: "关闭标签页 close tab guanbi", + action: PaletteAction::CloseTab, + }, + PaletteItem { + label: "下一个标签页", + hint: "Ctrl+Tab", + search: "下一个标签页 next tab xiayige", + action: PaletteAction::NextTab, + }, + PaletteItem { + label: "上一个标签页", + hint: "Ctrl+Shift+Tab", + search: "上一个标签页 previous prev tab shangyige", + action: PaletteAction::PrevTab, + }, + PaletteItem { + label: "新建窗口", + hint: "Ctrl+Shift+E", + search: "新建窗口 new window xinjian chuangkou", + action: PaletteAction::NewWindow, + }, + PaletteItem { + label: "左右分屏", + hint: "Ctrl+Shift+D", + search: "左右分屏 split right vertical zuoyou fenping", + action: PaletteAction::SplitRight, + }, + PaletteItem { + label: "上下分屏", + hint: "Ctrl+Shift+S", + search: "上下分屏 split down horizontal shangxia fenping", + action: PaletteAction::SplitDown, + }, + PaletteItem { + label: "导出工作区…", + hint: "", + search: "导出工作区 export workspace save session daochu gongzuoqu", + action: PaletteAction::ExportWorkspace, + }, + PaletteItem { + label: "打开工作区…", + hint: "", + search: "打开工作区 open import workspace load session dakai daoru gongzuoqu", + action: PaletteAction::ImportWorkspace, + }, + PaletteItem { + label: "目录树面板", + hint: "Ctrl+Shift+F", + search: "目录树面板 files tree explorer panel mulushu wenjian", + action: PaletteAction::ToggleFilesPanel, + }, + PaletteItem { + label: "显示标签侧栏", + hint: "", + search: "显示标签侧栏 toggle show hide tab sidebar xianshi biaoqian celan", + action: PaletteAction::ToggleSidebar, + }, + PaletteItem { + label: "拖拽调节侧栏宽度", + hint: "", + search: "拖拽调节侧栏宽度 drag resize sidebar drawer panel width tuozhuai tiaojie kuandu", + action: PaletteAction::TogglePanelResize, + }, + PaletteItem { + label: "Git 面板", + hint: "Ctrl+Shift+G", + search: "git 面板 status branch panel mianban", + action: PaletteAction::ToggleGitPanel, + }, + PaletteItem { + label: "打开设置", + hint: "", + search: "打开设置 open settings preferences dakai shezhi", + action: PaletteAction::OpenSettings, + }, + PaletteItem { + label: "打开配置文件", + hint: "", + search: "打开配置文件 open config file dakai peizhi wenjian", + action: PaletteAction::OpenSettingsFile, + }, + PaletteItem { + label: "同步:推送设置到云端", + hint: "", + search: "同步推送设置到云端 webdav sync push upload settings tongbu tuisong", + action: PaletteAction::SyncPush, + }, + PaletteItem { + label: "同步:从云端拉取设置", + hint: "", + search: "同步从云端拉取设置 webdav sync pull download settings tongbu laqu", + action: PaletteAction::SyncPull, + }, + PaletteItem { + label: "切换行内补全 (Ghost)", + hint: "", + search: "切换行内补全 toggle ghost completion qiehuan buquan", + action: PaletteAction::ToggleGhost, + }, + PaletteItem { + label: "切换补全接受键", + hint: "", + search: "切换补全接受键 cycle accept key completion jieshou", + action: PaletteAction::CycleAccept, + }, + PaletteItem { + label: "切换补全样式(行内 / 弹窗)", + hint: "", + search: "切换补全样式 行内 弹窗 completion style inline popup list buquan yangshi", + action: PaletteAction::CycleCompletionStyle, + }, + PaletteItem { + label: "选择背景图片…", + hint: "", + search: "选择背景图片 background image picture xuanze beijing tupian", + action: PaletteAction::PickBackgroundImage, + }, + PaletteItem { + label: "切换背景色", + hint: "", + search: "切换背景色 cycle background color qiehuan beijingse", + action: PaletteAction::CycleBackground, + }, + PaletteItem { + label: "恢复外观默认", + hint: "", + search: "恢复外观默认 reset appearance default huifu waiguan moren", + action: PaletteAction::ResetAppearance, + }, + theme_item(NebulaTheme::BreezeLight, "theme 主题 breeze light"), + theme_item(NebulaTheme::BreezeDark, "theme 主题 breeze dark"), + theme_item(NebulaTheme::MintLight, "theme 主题 mint light"), + theme_item(NebulaTheme::MintDark, "theme 主题 mint dark"), + theme_item(NebulaTheme::SilverLight, "theme 主题 silver light"), + theme_item(NebulaTheme::Nord, "theme 主题 nord dark"), + theme_item(NebulaTheme::Paper, "theme 主题 paper light"), + theme_item(NebulaTheme::LimestoneLight, "theme 主题 limestone light"), + theme_item(NebulaTheme::LinenLight, "theme 主题 linen light"), + theme_item(NebulaTheme::CatppuccinMocha, "theme 主题 catppuccin mocha dark"), + theme_item(NebulaTheme::CatppuccinLatte, "theme 主题 catppuccin latte light"), + theme_item(NebulaTheme::CatppuccinFrappe, "theme 主题 catppuccin frappe frappé dark"), + theme_item(NebulaTheme::CatppuccinMacchiato, "theme 主题 catppuccin macchiato dark"), + theme_item(NebulaTheme::GlassLight, "theme 主题 glass light"), + theme_item(NebulaTheme::GlassDark, "theme 主题 glass dark"), +]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn theme_commands_match_the_selectable_catalog() { + let themes: Vec<_> = ITEMS + .iter() + .filter_map(|item| match item.action { + PaletteAction::SelectTheme(theme) => Some(theme.prompt_name()), + _ => None, + }) + .collect(); + assert_eq!(themes, nebula_settings::ThemeName::BUILTIN_NAMES); + } +} diff --git a/nebula_app/src/display/settings.rs b/nebula_app/src/display/settings.rs index b46932e..650eae0 100644 --- a/nebula_app/src/display/settings.rs +++ b/nebula_app/src/display/settings.rs @@ -862,10 +862,8 @@ pub(super) fn nebula_settings_load(config: &UiConfig) -> NebulaRuntimeSettings { // 默认开:托盘是 agent 等待提醒的常驻出口(任务栏闪烁会被忽略、 // toast 会过期);不想要常驻图标的人在设置里关。 tray: true, - // 2026-07-31 用户裁定:默认开。纯 alpha 下背景的高频细节压着文字, - // 那正是"透明度调低就看不清"的物理来源;模糊把它拍成低频色块。想 - // 真透出后面窗口内容的人可以关掉。 - blur: true, + // Both shells use the shared opt-in material default. + blur: nebula_settings::BlurModeName::default().enabled(), opacity: config.window_opacity(), background: None, background_image: None, @@ -1083,19 +1081,9 @@ fn parse_bool(value: &str, default: bool) -> bool { } } -/// `blur` 键与 GPUI 壳共用,但那边是五档枚举(`nebula_settings::BlurModeName`: -/// none / mica / mica-alt / aero / acrylic),这边只有开关。 -/// -/// 不能直接套 [`parse_bool`]:它对认不出的值回落到 `default`,而这里的 -/// default 是 `true`——于是 GPUI 写下的 `blur=none` 会被旧壳读成"开",用户 -/// 关掉的模糊一开旧壳就自己回来了。这里显式认五个枚举名,档位信息丢掉但 -/// 开关语义保住。 +/// Project the shared material parser into the legacy shell's boolean switch. fn parse_blur_enabled(value: &str) -> bool { - match value.trim().to_ascii_lowercase().as_str() { - "none" => false, - "aero" | "mica" | "mica-alt" | "acrylic" => true, - other => parse_bool(other, true), - } + nebula_settings::BlurModeName::from_settings(value).unwrap_or_default().enabled() } fn parse_cursor_shape(value: &str) -> Option { diff --git a/nebula_app/src/display/ui/theme.rs b/nebula_app/src/display/ui/theme.rs index 5cb6bea..d043279 100644 --- a/nebula_app/src/display/ui/theme.rs +++ b/nebula_app/src/display/ui/theme.rs @@ -48,6 +48,8 @@ pub enum NebulaTheme { MintDark, CatppuccinMocha, CatppuccinLatte, + CatppuccinFrappe, + CatppuccinMacchiato, GlassLight, GlassDark, } @@ -91,6 +93,8 @@ impl NebulaTheme { (Self::MintLight | Self::MintDark, false) => Self::MintDark, (Self::CatppuccinMocha | Self::CatppuccinLatte, true) => Self::CatppuccinLatte, (Self::CatppuccinMocha | Self::CatppuccinLatte, false) => Self::CatppuccinMocha, + (Self::CatppuccinFrappe | Self::CatppuccinMacchiato, true) => Self::CatppuccinLatte, + (Self::CatppuccinFrappe | Self::CatppuccinMacchiato, false) => self, (Self::GlassLight | Self::GlassDark, true) => Self::GlassLight, (Self::GlassLight | Self::GlassDark, false) => Self::GlassDark, } @@ -113,6 +117,8 @@ impl NebulaTheme { Self::MintDark => "Mint Dark", Self::CatppuccinMocha => "Catppuccin Mocha", Self::CatppuccinLatte => "Catppuccin Latte", + Self::CatppuccinFrappe => "Catppuccin Frappé", + Self::CatppuccinMacchiato => "Catppuccin Macchiato", Self::GlassLight => "Glass Light", Self::GlassDark => "Glass Dark", } @@ -136,6 +142,8 @@ impl NebulaTheme { Self::MintDark => "Theme: Mint Dark", Self::CatppuccinMocha => "Theme: Catppuccin Mocha", Self::CatppuccinLatte => "Theme: Catppuccin Latte", + Self::CatppuccinFrappe => "Theme: Catppuccin Frappé", + Self::CatppuccinMacchiato => "Theme: Catppuccin Macchiato", Self::GlassLight => "Theme: Glass Light", Self::GlassDark => "Theme: Glass Dark", } @@ -158,6 +166,8 @@ impl NebulaTheme { Self::MintDark => "MintDark", Self::CatppuccinMocha => "CatppuccinMocha", Self::CatppuccinLatte => "CatppuccinLatte", + Self::CatppuccinFrappe => "CatppuccinFrappe", + Self::CatppuccinMacchiato => "CatppuccinMacchiato", Self::GlassLight => "GlassLight", Self::GlassDark => "GlassDark", } @@ -182,6 +192,8 @@ impl NebulaTheme { "MintDark" => Self::MintDark, "CatppuccinMocha" => Self::CatppuccinMocha, "CatppuccinLatte" => Self::CatppuccinLatte, + "CatppuccinFrappe" => Self::CatppuccinFrappe, + "CatppuccinMacchiato" => Self::CatppuccinMacchiato, "GlassLight" => Self::GlassLight, "GlassDark" => Self::GlassDark, @@ -218,6 +230,8 @@ impl NebulaTheme { | Self::MintDark | Self::CatppuccinMocha | Self::CatppuccinLatte + | Self::CatppuccinFrappe + | Self::CatppuccinMacchiato | Self::GlassLight | Self::GlassDark => rgb8(self.fresh_palette().unwrap().accent), Self::Nebula => Rgb::new(82, 168, 255), @@ -375,6 +389,8 @@ impl NebulaTheme { | Self::MintDark | Self::CatppuccinMocha | Self::CatppuccinLatte + | Self::CatppuccinFrappe + | Self::CatppuccinMacchiato | Self::GlassLight | Self::GlassDark => nebula_settings::ThemeName::from_prompt_name(self.prompt_name()) .unwrap() diff --git a/nebula_app/src/gpui_shell/settings_pane.rs b/nebula_app/src/gpui_shell/settings_pane.rs index 8d6c22f..5d99305 100644 --- a/nebula_app/src/gpui_shell/settings_pane.rs +++ b/nebula_app/src/gpui_shell/settings_pane.rs @@ -416,8 +416,7 @@ impl SettingsPane { // 成一次重绘,所以每帧调是安全的。 cx.refresh_windows(); cx.notify(); - // 用户拖动后 opacity 已是显式选择;同时把旧 blur=1 规范成枚举值, - // 否则共享迁移层会再次把 blur=1 + opacity=1.00 解读成默认 0.82。 + // 保存显式不透明度,并把旧的模糊布尔值规范为当前枚举值。 self.schedule_slider_persist( vec![ ("opacity", format!("{opacity:.2}")), diff --git a/nebula_app/src/gpui_shell/settings_pane/appearance_picker_tests.rs b/nebula_app/src/gpui_shell/settings_pane/appearance_picker_tests.rs index 39d25a2..bf8f660 100644 --- a/nebula_app/src/gpui_shell/settings_pane/appearance_picker_tests.rs +++ b/nebula_app/src/gpui_shell/settings_pane/appearance_picker_tests.rs @@ -24,12 +24,14 @@ fn appearance_theme_catalog_matches_the_approved_gallery_order() { "Linen", "Catppuccin Mocha", "Catppuccin Latte", + "Catppuccin Frappé", + "Catppuccin Macchiato", "Glass Light", "Glass Dark" ] ); assert_eq!(themes.choices(1).len(), 8); - assert_eq!(themes.choices(2).len(), 5); + assert_eq!(themes.choices(2).len(), 7); for choice in themes.choices(0) { assert_ne!(themes.choices(1).contains(&choice), themes.choices(2).contains(&choice)); } @@ -48,15 +50,43 @@ fn appearance_icon_filters_cover_each_catalog_entry_once() { #[test] fn appearance_theme_confirmation_preserves_icon_and_other_settings() { let initial = "theme=Nebula\nfollow_system_theme=1\napp_icon=graphite-violet\nbackground=#111111\nfont_size=17\nshell=cmd\n"; - let updates = AppearanceSelection::Theme(ThemeName::LinenLight).updates(); - let updated = nebula_settings::apply_updates(initial, &updates); - let runtime = RuntimeSettings::from_raw(&nebula_settings::RawSettings::from_text(&updated)); - assert_eq!(runtime.theme, ThemeName::LinenLight); - assert!(!runtime.follow_system_theme); - assert_eq!(runtime.background, Some(ThemeName::LinenLight.term_theme().background)); - assert_eq!(runtime.app_icon, AppIconName::GraphiteViolet); - assert!(updated.contains("font_size=17")); - assert!(updated.contains("shell=cmd")); + for theme in + [ThemeName::LinenLight, ThemeName::CatppuccinFrappe, ThemeName::CatppuccinMacchiato] + { + let updates = AppearanceSelection::Theme(theme).updates(); + let updated = nebula_settings::apply_updates(initial, &updates); + let runtime = RuntimeSettings::from_raw(&nebula_settings::RawSettings::from_text(&updated)); + assert_eq!(runtime.theme, theme); + assert!(!runtime.follow_system_theme); + assert_eq!(runtime.background, Some(theme.term_theme().background)); + assert_eq!(runtime.app_icon, AppIconName::GraphiteViolet); + assert!(updated.contains("font_size=17")); + assert!(updated.contains("shell=cmd")); + } +} + +#[test] +fn catppuccin_system_appearance_restores_the_saved_dark_flavor() { + use crate::gpui_shell::theme::resolve_theme_name; + + for theme in + [ThemeName::CatppuccinMocha, ThemeName::CatppuccinFrappe, ThemeName::CatppuccinMacchiato] + { + let initial = format!("theme={}\nfollow_system_theme=1\n", theme.prompt_name()); + let runtime = RuntimeSettings::from_raw(&nebula_settings::RawSettings::from_text(&initial)); + for is_light in [false, true, false] { + assert_eq!( + resolve_theme_name(runtime.theme, runtime.follow_system_theme, is_light), + if is_light { ThemeName::CatppuccinLatte } else { theme } + ); + assert_eq!(runtime.theme, theme); + } + assert_eq!(resolve_theme_name(runtime.theme, false, true), theme); + } + assert_eq!( + resolve_theme_name(ThemeName::CatppuccinLatte, true, false), + ThemeName::CatppuccinMocha + ); } #[test] diff --git a/nebula_app/src/gpui_shell/settings_pane/navigation.rs b/nebula_app/src/gpui_shell/settings_pane/navigation.rs index 6c3dc66..7cad2b7 100644 --- a/nebula_app/src/gpui_shell/settings_pane/navigation.rs +++ b/nebula_app/src/gpui_shell/settings_pane/navigation.rs @@ -1,7 +1,7 @@ use super::*; /// 主题下拉(展示名 = 持久化名,与旧壳一致)。 -pub(super) const THEME_VALUES: [&str; 13] = ThemeName::BUILTIN_NAMES; +pub(super) const THEME_VALUES: [&str; ThemeName::BUILTIN.len()] = ThemeName::BUILTIN_NAMES; pub(super) const REPOSITORY_URL: &str = "https://github.com/Kuddev/pebrel"; pub(super) const BUG_REPORT_TEMPLATE: &str = "bug_report.yml"; diff --git a/nebula_app/src/gpui_shell/settings_pane/theme_picker.rs b/nebula_app/src/gpui_shell/settings_pane/theme_picker.rs index 1c4482e..e475ba6 100644 --- a/nebula_app/src/gpui_shell/settings_pane/theme_picker.rs +++ b/nebula_app/src/gpui_shell/settings_pane/theme_picker.rs @@ -1,7 +1,7 @@ use super::appearance_picker::{AppearanceColors, AppearanceSelection, picker_columns}; use super::*; -pub(super) const THEME_ORDER: [ThemeName; 13] = ThemeName::BUILTIN; +pub(super) const THEME_ORDER: [ThemeName; ThemeName::BUILTIN.len()] = ThemeName::BUILTIN; fn theme_foreground(name: ThemeName) -> [u8; 3] { let theme = name.term_theme(); diff --git a/nebula_app/src/gpui_shell/terminal/session.rs b/nebula_app/src/gpui_shell/terminal/session.rs index 254c34b..2560eb5 100644 --- a/nebula_app/src/gpui_shell/terminal/session.rs +++ b/nebula_app/src/gpui_shell/terminal/session.rs @@ -93,10 +93,7 @@ pub(super) fn local_options( if let Err(error) = crate::platform::shell_integration::prepare(&mut options) { log::warn!("Could not prepare shell integration: {error}"); } - #[cfg(windows)] - if let Err(error) = tty::refresh_environment(&mut options) { - log::warn!("Could not refresh the Windows environment for a new pane: {error}"); - } + crate::platform::environment::prepare_local_pty(&mut options); // 终端网络代理开启时,把当前系统代理同步给新会话(HTTP(S)_PROXY 变量), // 让会话里的 curl/git/npm 等不必重复设。系统代理探不到则什么都不做。 // 放在环境重建之后:注入值作为 pane 专属覆盖,不被注册表快照冲掉。 diff --git a/nebula_app/src/gpui_shell/theme.rs b/nebula_app/src/gpui_shell/theme.rs index 3d6bc44..4208b98 100644 --- a/nebula_app/src/gpui_shell/theme.rs +++ b/nebula_app/src/gpui_shell/theme.rs @@ -84,12 +84,6 @@ pub(crate) fn theme_card_persist_updates(name: ThemeName) -> [(&'static str, Str ] } -/// 当前生效主题的终端底色(给「跟随主题」取色器同步用,不是壳色)。 -pub(crate) fn theme_term_background(cx: &App) -> Hsla { - let c = chrome_theme(effective_theme_name(cx)).palette().term_bg; - to_hsla(c.r, c.g, c.b) -} - fn to_hsla(r: u8, g: u8, b: u8) -> Hsla { GpuiRgba { r: f32::from(r) / 255.0, g: f32::from(g) / 255.0, b: f32::from(b) / 255.0, a: 1.0 } .into() @@ -195,15 +189,14 @@ fn shell_color(theme: NebulaTheme) -> Hsla { /// 与「铺满到窗口边」由此成为同一条渲染路径的两组取值,不是两套代码、更不是 /// 两套页面。 /// -/// 取值分两层:主题自带的 [`nebula_settings::ThemeCardGeometry`] 打底,用户在 -/// settings.txt 里的显式值覆盖它。于是切主题就换形态,而手调过的人不会被主题 -/// 夺回控制权。 +/// 主题默认叠加用户配置后,按半径确定形态:直角铺满、圆角留缝。 +/// 只调整生效值,不改写保存的卡缝、投影和竖线;切回对应形态时恢复这些配置。 /// /// 卡内壁到网格的内间距**不在这里**:那是 `SizeInfo` 的 `padding.x/y` 与 /// `padding_right()`(`config/window.rs`),本来就可配。两处都给会双份叠加。 #[derive(Clone, Copy, Debug, PartialEq)] pub struct PaneCardStyle { - /// 卡圆角。0 与 `margin` 全零搭配即铺满形态。 + /// 卡圆角。0 时外间距和投影同时停用,终端铺满可用区域。 pub radius: f32, /// 卡与周围 chrome 的外间距,**逐边给**——不能假设对称,理由见 /// [`paint_shell_around_card`]。GPUI 侧它落在父容器的 padding 上(卡是子 @@ -211,7 +204,7 @@ pub struct PaneCardStyle { pub margin: gpui::Edges, /// 卡投影。 pub shadow: bool, - /// 侧栏与终端之间的竖线宽度,0 = 不画。 + /// 侧栏与终端之间生效的竖线宽度;圆角卡不画,直角卡沿用配置。 pub divider: f32, } @@ -229,14 +222,21 @@ impl PaneCardStyle { /// settings 就会在跟随系统主题时和 chrome 分家。 pub fn resolve(theme: ThemeName, runtime: &nebula_settings::RuntimeSettings) -> Self { let geometry = theme.card_geometry(); - let gutter = runtime.pane_card_gutter.unwrap_or(geometry.gutter); + let radius = runtime.pane_card_radius.unwrap_or(geometry.radius); + let rounded = radius > 0.0; + let gutter = + if rounded { runtime.pane_card_gutter.unwrap_or(geometry.gutter) } else { 0.0 }; Self { - radius: runtime.pane_card_radius.unwrap_or(geometry.radius), + radius, // 上边恒零:08-26 裁定侧栏 / 终端卡 / 右侧抽屉三列顶边都贴 chrome // 下沿。可配的那个数只作用于左 / 右 / 下三边。 margin: gpui::Edges { top: 0.0, right: gutter, bottom: gutter, left: gutter }, - shadow: runtime.pane_card_shadow.unwrap_or(geometry.shadow), - divider: runtime.pane_card_divider.unwrap_or(geometry.divider), + shadow: rounded && runtime.pane_card_shadow.unwrap_or(geometry.shadow), + divider: if rounded { + 0.0 + } else { + runtime.pane_card_divider.unwrap_or(geometry.divider) + }, } } @@ -248,20 +248,8 @@ impl PaneCardStyle { } } -/// A flush pane extends its background through the titlebar; explicit custom -/// gutters/radii retain a surrounding shell instead. -pub(crate) fn pane_is_flush(cx: &App) -> bool { - let card = PaneCardStyle::current(cx); - card.radius == 0.0 - && card.margin.top == 0.0 - && card.margin.right == 0.0 - && card.margin.bottom == 0.0 - && card.margin.left == 0.0 -} - -/// 侧栏与终端之间那条竖线的颜色。从壳色推导而不新增 palette 色位:9 个主题 -/// 一个都不用改,而且亮暗自动反向——浅色主题压暗、深色主题提亮,两边都能 -/// 读作一条分界而不是一道亮缝。 +/// 侧栏与正文的弱分界直接使用 HTML 对应的 `line` RGBA 令牌。 +/// 它与外窗描边、强调色分别取值,不额外合成一套灰色。 pub fn card_divider_color(cx: &App) -> Hsla { wash(chrome_theme_resolved(cx).skin().hairline) } @@ -709,8 +697,44 @@ fn apply_skin_tokens(chrome: NebulaTheme, cx: &mut App) { #[cfg(test)] mod tests { - use super::{resolve_theme_name, theme_card_persist_updates}; - use nebula_settings::ThemeName; + use super::{PaneCardStyle, resolve_theme_name, theme_card_persist_updates}; + use nebula_settings::{RawSettings, RuntimeSettings, ThemeName}; + + #[test] + fn square_panes_fill_the_column_despite_saved_card_spacing_and_shadow() { + let runtime = RuntimeSettings::from_raw(&RawSettings::from_text( + "pane_card_radius=0\npane_card_gutter=8\npane_card_shadow=1\n", + )); + for theme in [ThemeName::Nord, ThemeName::Paper, ThemeName::SilverLight] { + let card = PaneCardStyle::resolve(theme, &runtime); + assert_eq!(card.margin, gpui::Edges::default(), "{theme:?}"); + assert!(!card.shadow, "{theme:?}"); + assert_eq!(card.divider, 1.0, "{theme:?}"); + } + } + + #[test] + fn changing_pane_shape_restores_spacing_and_dividers_without_rewriting_preferences() { + let mut runtime = RuntimeSettings::from_raw(&RawSettings::from_text( + "pane_card_radius=14\npane_card_gutter=8\npane_card_shadow=1\npane_card_divider=2\n", + )); + let rounded = PaneCardStyle::resolve(ThemeName::Paper, &runtime); + assert_eq!(rounded.margin, gpui::Edges { top: 0.0, right: 8.0, bottom: 8.0, left: 8.0 }); + assert!(rounded.shadow); + assert_eq!(rounded.divider, 0.0); + + runtime.pane_card_radius = Some(0.0); + let square = PaneCardStyle::resolve(ThemeName::Paper, &runtime); + assert_eq!(square.margin, gpui::Edges::default()); + assert!(!square.shadow); + assert_eq!(square.divider, 2.0); + + runtime.pane_card_radius = Some(14.0); + assert_eq!(PaneCardStyle::resolve(ThemeName::Paper, &runtime), rounded); + assert_eq!(runtime.pane_card_gutter, Some(8.0)); + assert_eq!(runtime.pane_card_shadow, Some(true)); + assert_eq!(runtime.pane_card_divider, Some(2.0)); + } #[test] fn follow_system_remaps_theme_family_and_manual_mode_keeps_preference() { diff --git a/nebula_app/src/gpui_shell/workspace.rs b/nebula_app/src/gpui_shell/workspace.rs index 765fd03..9a7cf81 100644 --- a/nebula_app/src/gpui_shell/workspace.rs +++ b/nebula_app/src/gpui_shell/workspace.rs @@ -520,7 +520,9 @@ fn pane_card_divider_bounds( } let scale = scale_factor.max(1.0); - let width = (divider * scale).round().max(1.0) / scale; + // Keep the default a physical hairline; wider custom dividers retain logical scaling. + let physical_width = if divider <= 1.0 { 1.0 } else { (divider * scale).round() }; + let width = physical_width.max(1.0) / scale; let height = bounds.size.height + bounds.origin.y; let mut origin = bounds.origin; origin.y = px(0.0); @@ -902,6 +904,8 @@ pub struct NebulaWorkspace { split_bounds: SplitBoundsStore, /// pane 矩形的帧记录(ctrl+alt+方向 的最近邻导航用)。 pane_bounds: PaneBoundsStore, + /// 标题栏背景使用本帧实际终端列边界,与两侧面板和卡缝连续衔接。 + titlebar_background: window_titlebar::TitleBarBackground, /// GPUI presentation over the shared old-shell command catalog. command_palette_open: bool, command_palette_input: Entity, @@ -1180,6 +1184,7 @@ impl NebulaWorkspace { reader_focus: None, split_bounds: Rc::new(RefCell::new(HashMap::new())), pane_bounds: Rc::new(RefCell::new(HashMap::new())), + titlebar_background: window_titlebar::TitleBarBackground::default(), command_palette_open: false, command_palette_input, command_palette_selected: 0, @@ -3204,15 +3209,13 @@ impl NebulaWorkspace { .justify_end() .flex_shrink_0() .overflow_hidden() - // 槽位自己铺壳色:底部那 8px 间距与抽屉圆角的缺口露出来的必须是壳色, - // 和终端卡四周的卡缝同一底。workspace 根是透明的(Acrylic 要透到 DWM), - // 不铺这层的话缝里露的是系统背板——实测比壳色亮一档且靠窗边越亮 - // (44,46,53 → 62,64,71 vs 壳色 33,37,46),就是"底部颜色不对"。 + // 本地文件、SFTP 和 Git 共用这一层壳色,与左侧栏、卡缝融为一体。 + // 子视图只画内容;再次铺半透明底色会叠加 alpha,变成更亮的独立浮卡。 .bg(cx.theme().background) .child( // 抽屉整体从右缘推进来,而不是原地被擦出来。旧壳 // (side_panel.rs:1718)把 x 插值成 - // `rest_x + (1-eased) * (w + margin)`——整块浮板在动;只动槽位宽度 + // `rest_x + (1-eased) * (w + margin)`——整列内容在动;只动槽位宽度 // 的话内容一动不动,只有裁剪窗口在变宽,那就是"擦除"的观感来源。 // 槽位宽度仍然同步收放,正文(终端卡)才会跟着让位。 // @@ -3852,6 +3855,7 @@ impl Render for NebulaWorkspace { // 终端卡几何取一次,布局与壳色带共用同一个实例——两处各取一次也算 // 「各写一份」,主题在这一帧中途换掉就会出现半旧半新的卡缝。 let card_style = crate::gpui_shell::theme::PaneCardStyle::current(cx); + let titlebar_background = self.titlebar_background.clone(); let draw_file_divider = self.side_panel.open; let sidebar_logo_target_px = (TAB_LABEL_ICON_SIZE * window.scale_factor()).round().max(1.0) as u32; @@ -4093,31 +4097,13 @@ impl Render for NebulaWorkspace { .inset_0(), ) .child( - // 侧栏模式保留旧壳的侧栏开关与齿轮。组件默认 34px 标题栏会让 - // 32px 按钮几乎贴边,因此显式设为 48px、上下各留 8px;右侧 - // 窗口控制仍共享同一标题带。 - window_titlebar::settings_aware_title_bar(settings_active, cx) - .when(top_tabs, |bar| { - // 正文卡在顶部模式保留 8px 左缝;标题栏默认 12px, - // 这里覆写后首个 tab 才与卡内 powerline 严格同轴。 - bar.pl(px(top_tabs::TOP_TAB_LEFT_INSET)).child( - self.render_top_title_bar( - files_active, - git_active, - settings_active, - window, - cx, - ), - ) - }) - .when(!top_tabs, |bar| { - bar.child(self.render_sidebar_title_bar( - files_active, - git_active, - settings_active, - cx, - )) - }), + self.render_window_title_bar( + files_active, + git_active, + settings_active, + window, + cx, + ), ) .child( // 不用 h_flex:它默认 items_center,会把子项高度压成内容高度。 @@ -4174,7 +4160,7 @@ impl Render for NebulaWorkspace { .relative() .child( gpui::canvas( - |_, _, _| (), + move |bounds, _, _| titlebar_background.record_pane(bounds), |bounds, _, window, cx| { // 卡缝、圆角、竖线全部读同一份 style 真源: // 布局的 padding 与这里的壳色带必须逐边对上, diff --git a/nebula_app/src/gpui_shell/workspace/file_tree.rs b/nebula_app/src/gpui_shell/workspace/file_tree.rs index fbd8bc7..24216b3 100644 --- a/nebula_app/src/gpui_shell/workspace/file_tree.rs +++ b/nebula_app/src/gpui_shell/workspace/file_tree.rs @@ -32,9 +32,8 @@ pub(super) const DRAWER_TEXT_INSET: f32 = 4.0; /// 文件树右键菜单的宿主:必须挂在 workspace 根上,不能当抽屉行的 child。 /// -/// `ContextMenuExt` 会把 `deferred(anchored(PopupMenu))` 挂回触发行;行在带 -/// `shadow` 的抽屉里面。菜单翻到抽屉左缘时,抽屉投影会垫在菜单周围,看起来 -/// 比侧栏 Tab 右键厚一截。 +/// `ContextMenuExt` 会把 `deferred(anchored(PopupMenu))` 挂回触发行;提升到 +/// workspace 后,菜单不受抽屉自身的裁剪与推出动画影响。 pub(super) struct FileTreeContextMenu { menu: Entity, position: Point, @@ -447,10 +446,6 @@ impl NebulaWorkspace { .flex_shrink_0() .p_2() .gap_2() - // Flat presets share the tab sidebar surface; the workspace paints the seam. - .rounded_tl(crate::gpui_shell::theme::card_radius(cx)) - .rounded_bl(crate::gpui_shell::theme::card_radius(cx)) - .bg(theme.sidebar) .occlude() .child(view_switch) .child( diff --git a/nebula_app/src/gpui_shell/workspace/remote_files.rs b/nebula_app/src/gpui_shell/workspace/remote_files.rs index 4427201..fb96536 100644 --- a/nebula_app/src/gpui_shell/workspace/remote_files.rs +++ b/nebula_app/src/gpui_shell/workspace/remote_files.rs @@ -476,9 +476,6 @@ impl NebulaWorkspace { .flex_shrink_0() .p_2() .gap_2() - .rounded_tl(crate::gpui_shell::theme::card_radius(cx)) - .rounded_bl(crate::gpui_shell::theme::card_radius(cx)) - .bg(cx.theme().sidebar) .occlude() .drag_over::(move |style, _, _, _| style.bg(drop_highlight)) .on_drop(cx.listener(|this, paths: &ExternalPaths, window, cx| { diff --git a/nebula_app/src/gpui_shell/workspace/sidebar.rs b/nebula_app/src/gpui_shell/workspace/sidebar.rs index e0c99ec..4b114c7 100644 --- a/nebula_app/src/gpui_shell/workspace/sidebar.rs +++ b/nebula_app/src/gpui_shell/workspace/sidebar.rs @@ -922,8 +922,6 @@ impl NebulaWorkspace { let settings_active_bg = cx.theme().sidebar_accent; let settings_active_fg = cx.theme().sidebar_accent_foreground; let sidebar_visible = !self.sidebar_collapsed && !self.reader_focus_active(cx); - let flush_sidebar = - !settings_active && sidebar_visible && crate::gpui_shell::theme::pane_is_flush(cx); h_flex() .size_full() .items_center() @@ -934,13 +932,6 @@ impl NebulaWorkspace { // 32px,`.small()` 会把热区缩成 24px。 .gap_2() .items_center() - .when(flush_sidebar, |left| { - // Keep the sidebar-colored strip aligned with the actual - // sidebar, including the title bar's leading padding. - let inset = if cfg!(target_os = "macos") { 80.0 } else { 12.0 }; - left.h_full().w(px(self.sidebar_width)).flex_shrink_0() - .ml(px(-inset)).pl(px(inset)).bg(cx.theme().background) - }) .occlude() .child( Button::new("toggle-sidebar") diff --git a/nebula_app/src/gpui_shell/workspace/tests.rs b/nebula_app/src/gpui_shell/workspace/tests.rs index d478d93..67bad94 100644 --- a/nebula_app/src/gpui_shell/workspace/tests.rs +++ b/nebula_app/src/gpui_shell/workspace/tests.rs @@ -354,13 +354,15 @@ fn pane_card_divider_reaches_the_window_top_without_moving_its_bottom() { f32::from(card.origin.y + card.size.height), "向上延伸不能越过原来的正文底边" ); - assert!( - (f32::from(divider.size.width) * 1.5 - 2.0).abs() < f32::EPSILON, - "150% 缩放下 1px 线应吸附为两个物理像素" - ); - - let scale_one = pane_card_divider_bounds(card, 1.0, 1.0).unwrap(); - assert_eq!(f32::from(scale_one.size.width), 1.0); + for scale in [1.0, 1.25, 1.5, 1.75, 2.0, 3.0] { + let line = pane_card_divider_bounds(card, 1.0, scale).unwrap(); + assert!( + (f32::from(line.size.width) * scale - 1.0).abs() < f32::EPSILON, + "default divider must remain one physical pixel at scale {scale}" + ); + } + let wide = pane_card_divider_bounds(card, 2.0, 1.5).unwrap(); + assert_eq!(f32::from(wide.size.width), 2.0, "explicit wider dividers retain their size"); } #[test] diff --git a/nebula_app/src/gpui_shell/workspace/vcs_panel.rs b/nebula_app/src/gpui_shell/workspace/vcs_panel.rs index 1ae224a..39cdce0 100644 --- a/nebula_app/src/gpui_shell/workspace/vcs_panel.rs +++ b/nebula_app/src/gpui_shell/workspace/vcs_panel.rs @@ -883,11 +883,7 @@ impl NebulaWorkspace { .flex_shrink_0() .p_2() .gap_2() - // 与文件树共用抽屉接缝合同(见 `render_file_tree`):圆角只给左侧两角, - // 右缘贴窗口边框不倒角,四边无描边。 - .rounded_tl(crate::gpui_shell::theme::card_radius(cx)) - .rounded_bl(crate::gpui_shell::theme::card_radius(cx)) - .bg(theme.sidebar) + // 与文件树共用父容器的壳色,不在内容层叠加背景或圆角。 .occlude() .child(view_switch) // VCS 状态现在跟着侧栏定位走(`SidePanel::vcs_root`),所以必须在 diff --git a/nebula_app/src/gpui_shell/workspace/window_titlebar.rs b/nebula_app/src/gpui_shell/workspace/window_titlebar.rs index 880daf8..2648b65 100644 --- a/nebula_app/src/gpui_shell/workspace/window_titlebar.rs +++ b/nebula_app/src/gpui_shell/workspace/window_titlebar.rs @@ -1,5 +1,57 @@ use super::*; +/// Prepaint records the actual pane column before any titlebar paint runs. Keep +/// this cell for the workspace lifetime, so resizing and panel animations do not +/// allocate a new shared slot or duplicate the body's layout calculations. +#[derive(Clone, Default)] +pub(super) struct TitleBarBackground(Rc>>>); + +impl TitleBarBackground { + pub(super) fn record_pane(&self, bounds: Bounds) { + self.0.set(Some(bounds)); + } + + fn element(&self) -> gpui::Canvas<()> { + let pane_bounds = self.0.clone(); + canvas( + |_, _, _| (), + move |bounds, _, window, cx| { + let card = crate::gpui_shell::theme::PaneCardStyle::current(cx); + let shell = cx.theme().background; + let Some(pane) = pane_bounds.get().filter(|_| card.radius == 0.0) else { + window.paint_quad(fill(bounds, shell)); + return; + }; + let title_left = f32::from(bounds.origin.x); + let title_right = title_left + f32::from(bounds.size.width); + let pane_left = f32::from(pane.origin.x).clamp(title_left, title_right); + let pane_right = f32::from(pane.right()).clamp(pane_left, title_right); + let content = crate::gpui_shell::theme::card_content_bg(cx); + + // Adjacent bands receive alpha once. The actual pane bounds also + // account for a hidden sidebar and an open file tree. + for (left, right, color) in [ + (title_left, pane_left, shell), + (pane_left, pane_right, content), + (pane_right, title_right, shell), + ] { + if right > left { + window.paint_quad(fill( + Bounds::new( + gpui::point(px(left), bounds.origin.y), + size(px(right - left), bounds.size.height), + ), + color, + )); + } + } + }, + ) + .absolute() + .inset_0() + } +} + /// Paint tab and file-tree seams after the terminal, including the titlebar span. /// Both use the same pixel snapping and theme color; the right seam stays inside /// the terminal edge so the subsequently painted drawer cannot cover it. @@ -25,13 +77,45 @@ pub(super) fn paint_pane_dividers( } } -pub(super) fn settings_aware_title_bar(settings_active: bool, cx: &App) -> TitleBar { - TitleBar::new() - .h(px(48.0)) - .when(!settings_active && crate::gpui_shell::theme::pane_is_flush(cx), |bar| { - bar.bg(crate::gpui_shell::theme::theme_term_background(cx)).border_b_0() - }) - .when(settings_active, |bar| { - bar.border_b_1().border_color(crate::gpui_shell::theme::settings_hairline(cx)) - }) +impl NebulaWorkspace { + pub(super) fn render_window_title_bar( + &self, + files_active: bool, + git_active: bool, + settings_active: bool, + window: &mut Window, + cx: &mut Context, + ) -> gpui::Div { + let top_tabs = self.tabs_position == nebula_settings::TabsPositionName::Top; + let bar = TitleBar::new() + // Leave 8px above and below the existing 32px controls. + .h(px(48.0)) + .when(!settings_active, |bar| bar.bg(gpui::transparent_black()).border_b_0()) + .when(settings_active, |bar| { + bar.border_b_1().border_color(crate::gpui_shell::theme::settings_hairline(cx)) + }) + .when(top_tabs, |bar| { + bar.pl(px(top_tabs::TOP_TAB_LEFT_INSET)).child(self.render_top_title_bar( + files_active, + git_active, + settings_active, + window, + cx, + )) + }) + .when(!top_tabs, |bar| { + bar.child(self.render_sidebar_title_bar( + files_active, + git_active, + settings_active, + cx, + )) + }); + + div() + .relative() + .flex_shrink_0() + .when(!settings_active, |title| title.child(self.titlebar_background.element())) + .child(bar) + } } diff --git a/nebula_app/src/platform/environment.rs b/nebula_app/src/platform/environment.rs index c25eb74..66070a1 100644 --- a/nebula_app/src/platform/environment.rs +++ b/nebula_app/src/platform/environment.rs @@ -1,4 +1,39 @@ -//! Startup compatibility for platform environment variable names. +//! Startup aliases and platform compatibility for local terminal environments. + +/// Refresh local pane environments before adding identity and other pane overrides. +pub(crate) fn prepare_local_pty(options: &mut nebula_terminal::tty::Options) { + #[cfg(windows)] + { + if let Err(error) = nebula_terminal::tty::refresh_environment(options) { + log::warn!("Could not refresh the Windows environment for a new pane: {error}"); + } + let inherited_override = + !options.env_is_complete && std::env::var_os(GROK_LEGACY_CONSOLE).is_some(); + apply_windows_terminal_defaults(options, inherited_override); + } + #[cfg(not(windows))] + let _ = options; +} + +#[cfg(windows)] +const GROK_LEGACY_CONSOLE: &str = "GROK_FORCE_LEGACY_CONSOLE"; + +#[cfg(windows)] +fn apply_windows_terminal_defaults( + options: &mut nebula_terminal::tty::Options, + inherited_override: bool, +) { + // Grok 1.0.25 treats unknown Windows terminal names as legacy consoles and + // omits its Braille logo. Keep our real identity and use its capability override. + // Remove this default when Grok recognizes Pebrel's terminal capabilities. + // A complete refreshed environment is authoritative, including deleted keys. + if (!options.env_is_complete && inherited_override) + || options.env.keys().any(|name| name.eq_ignore_ascii_case(GROK_LEGACY_CONSOLE)) + { + return; + } + options.env.insert(GROK_LEGACY_CONSOLE.to_owned(), "0".to_owned()); +} /// Must run before any threads start: environment mutation is process-global. pub unsafe fn import_environment_aliases() { @@ -48,6 +83,92 @@ fn configuration_override(suffix: &str) -> bool { #[cfg(test)] mod tests { + #[cfg(windows)] + #[test] + fn local_terminal_defaults_enable_grok_unicode_without_changing_identity() { + let mut options = nebula_terminal::tty::Options { + env: [("TERM_PROGRAM", "pebrel"), ("WSLENV", "KEEP/p")] + .into_iter() + .map(|(name, value)| (name.to_owned(), value.to_owned())) + .collect(), + env_is_complete: true, + ..Default::default() + }; + super::apply_windows_terminal_defaults(&mut options, false); + assert_eq!(options.env[super::GROK_LEGACY_CONSOLE], "0"); + assert_eq!(options.env["TERM_PROGRAM"], "pebrel"); + assert_eq!(options.env["WSLENV"], "KEEP/p"); + assert!(!options.env.contains_key("WT_SESSION")); + let once = options.clone(); + super::apply_windows_terminal_defaults(&mut options, false); + assert_eq!(options, once); + } + + #[cfg(windows)] + #[test] + fn local_terminal_defaults_preserve_explicit_grok_overrides_case_insensitively() { + for name in [super::GROK_LEGACY_CONSOLE, "grok_force_legacy_console"] { + for value in ["1", "true", "0", "false", ""] { + let mut options = nebula_terminal::tty::Options { + env: [(name.to_owned(), value.to_owned())].into_iter().collect(), + env_is_complete: true, + ..Default::default() + }; + let original = options.clone(); + super::apply_windows_terminal_defaults(&mut options, false); + assert_eq!(options, original); + } + } + } + + #[cfg(windows)] + #[test] + fn inherited_grok_override_only_applies_to_incomplete_environments() { + let mut options = nebula_terminal::tty::Options::default(); + super::apply_windows_terminal_defaults(&mut options, true); + assert!(!options.env.contains_key(super::GROK_LEGACY_CONSOLE)); + + // A successful refresh can remove a stale value from the parent's registry + // snapshot. Do not resurrect it when the new complete environment omits it. + options.env_is_complete = true; + super::apply_windows_terminal_defaults(&mut options, true); + assert_eq!(options.env[super::GROK_LEGACY_CONSOLE], "0"); + + let mut fallback = nebula_terminal::tty::Options::default(); + super::apply_windows_terminal_defaults(&mut fallback, false); + assert_eq!(fallback.env[super::GROK_LEGACY_CONSOLE], "0"); + assert!(!fallback.env_is_complete); + } + + #[cfg(windows)] + #[test] + fn refreshed_local_terminal_preserves_the_pane_grok_override() { + let mut options = nebula_terminal::tty::Options { + env: [("grok_force_legacy_console".to_owned(), "1".to_owned())].into_iter().collect(), + ..Default::default() + }; + super::prepare_local_pty(&mut options); + let overrides: Vec<_> = options + .env + .iter() + .filter(|(name, _)| name.eq_ignore_ascii_case(super::GROK_LEGACY_CONSOLE)) + .collect(); + assert_eq!(overrides.len(), 1); + assert_eq!(overrides[0].1, "1"); + } + + #[cfg(not(windows))] + #[test] + fn local_terminal_preparation_preserves_non_windows_environments() { + let mut options = nebula_terminal::tty::Options { + env: [("TERM_PROGRAM".to_owned(), "pebrel".to_owned())].into_iter().collect(), + ..Default::default() + }; + let original = options.clone(); + super::prepare_local_pty(&mut options); + assert_eq!(options, original); + } + #[test] fn environment_aliases_preserve_legacy_inputs_and_prefer_explicit_pebrel_values() { let aliases = super::environment_aliases( diff --git a/nebula_app/src/window_context.rs b/nebula_app/src/window_context.rs index 4ee2a27..1332f50 100644 --- a/nebula_app/src/window_context.rs +++ b/nebula_app/src/window_context.rs @@ -489,10 +489,7 @@ impl WindowContext { proxy: &EventLoopProxy, pane_id: PaneId, ) -> Result> { - #[cfg(windows)] - if let Err(error) = tty::refresh_environment(&mut pty_config) { - log::warn!("Could not refresh the Windows environment for a new pane: {error}"); - } + crate::platform::environment::prepare_local_pty(&mut pty_config); // Per-pane identity for AI-CLI lifecycle hooks: nebula-hook.exe reads // it and stamps its pipe messages, so turn state lands on the right // tab dot (see `ai_hook`). The same call also exports the terminal diff --git a/nebula_settings/src/lib.rs b/nebula_settings/src/lib.rs index 4c5f323..e097f80 100644 --- a/nebula_settings/src/lib.rs +++ b/nebula_settings/src/lib.rs @@ -206,6 +206,8 @@ pub enum ThemeName { MintDark, CatppuccinMocha, CatppuccinLatte, + CatppuccinFrappe, + CatppuccinMacchiato, GlassLight, GlassDark, } @@ -228,6 +230,8 @@ impl ThemeName { "MintDark" => Self::MintDark, "CatppuccinMocha" => Self::CatppuccinMocha, "CatppuccinLatte" => Self::CatppuccinLatte, + "CatppuccinFrappe" => Self::CatppuccinFrappe, + "CatppuccinMacchiato" => Self::CatppuccinMacchiato, "GlassLight" => Self::GlassLight, "GlassDark" => Self::GlassDark, @@ -252,6 +256,8 @@ impl ThemeName { Self::MintDark => "MintDark", Self::CatppuccinMocha => "CatppuccinMocha", Self::CatppuccinLatte => "CatppuccinLatte", + Self::CatppuccinFrappe => "CatppuccinFrappe", + Self::CatppuccinMacchiato => "CatppuccinMacchiato", Self::GlassLight => "GlassLight", Self::GlassDark => "GlassDark", } @@ -268,6 +274,8 @@ impl ThemeName { | Self::MintDark | Self::CatppuccinMocha | Self::CatppuccinLatte + | Self::CatppuccinFrappe + | Self::CatppuccinMacchiato | Self::GlassLight | Self::GlassDark => themes::fresh_terminal(self), Self::Nebula => TermTheme { @@ -832,8 +840,8 @@ impl BellModeName { /// - `Aero`:实时模糊窗口**后方的真实内容**,并叠加 Win32 深色玻璃色调。 /// - `Acrylic`:实时模糊 + tint/噪点/饱和度,最贵。 pub enum BlurModeName { - None, #[default] + None, Mica, MicaAlt, Aero, @@ -1051,31 +1059,15 @@ pub const DEFAULT_PANE_CARD_GUTTER: f32 = 8.0; pub const MIN_PANE_CARD_GUTTER: f32 = 0.0; pub const MAX_PANE_CARD_GUTTER: f32 = 32.0; pub const MAX_PANE_CARD_DIVIDER: f32 = 4.0; -/// 系统材质启用且用户未明确设置透明度时,让基础层适度透出。 -const SYSTEM_MATERIAL_OPACITY: f32 = 0.82; - impl RuntimeSettings { pub fn load() -> Self { Self::from_raw(&RawSettings::load()) } pub fn from_raw(raw: &RawSettings) -> Self { - let raw_blur = raw.value("blur"); - let blur = raw_blur.and_then(BlurModeName::from_settings).unwrap_or_default(); - let configured_opacity = raw.f32("opacity").map(|opacity| opacity.clamp(0.0, 1.0)); - // 旧壳会把缺省值完整写成 `blur=1` + `opacity=1.00`。那不是用户主动 - // 选择的实色覆盖,却会把迁移后的 Mica 完全盖住;只迁移这组旧记号, - // 新枚举值下显式设置的 1.00 仍保持全不透明。 - let legacy_material_default = raw_blur.is_some_and(|value| { - matches!(value.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on") - }) && configured_opacity == Some(1.0); - let material_default = matches!(blur, BlurModeName::Mica | BlurModeName::MicaAlt) - && (configured_opacity.is_none() || legacy_material_default); - let opacity = if material_default { - SYSTEM_MATERIAL_OPACITY - } else { - configured_opacity.unwrap_or(1.0) - }; + let blur = raw.value("blur").and_then(BlurModeName::from_settings).unwrap_or_default(); + // Theme colors are opaque by default. Material selection never lowers opacity. + let opacity = raw.f32("opacity").unwrap_or(1.0).clamp(0.0, 1.0); Self { language: raw @@ -1410,8 +1402,8 @@ mod tests { assert!(settings.restore_session); assert!(settings.resume_ai); assert!(settings.tray); - assert_eq!(settings.blur, BlurModeName::Mica); - assert!((settings.opacity - SYSTEM_MATERIAL_OPACITY).abs() < 1e-6); + assert_eq!(settings.blur, BlurModeName::None); + assert_eq!(settings.opacity, 1.0); assert_eq!(settings.background, None); assert!(!settings.panel_resize); assert_eq!(settings.sidebar_width, DEFAULT_SIDEBAR_WIDTH); @@ -1499,21 +1491,19 @@ mod tests { assert_eq!(BlurModeName::from_settings("false"), Some(BlurModeName::None)); assert_eq!(BlurModeName::from_settings("1"), Some(BlurModeName::Mica)); assert_eq!(BlurModeName::from_settings("true"), Some(BlurModeName::Mica)); - assert!( - (RuntimeSettings::from_raw(&RawSettings::from_text("blur=1\nopacity=1.00\n")).opacity - - SYSTEM_MATERIAL_OPACITY) - .abs() - < 1e-6 + assert_eq!( + RuntimeSettings::from_raw(&RawSettings::from_text("blur=1\nopacity=1.00\n")).opacity, + 1.0 ); assert_eq!( RuntimeSettings::from_raw(&RawSettings::from_text("blur=mica\nopacity=1.00\n")).opacity, 1.0 ); - // 认不出的值回落到缺省档,而不是把窗口留在无模糊状态。 + // 认不出的值回落到默认关闭,不主动启用系统材质。 assert_eq!( RuntimeSettings::from_raw(&RawSettings::from_text("blur=frosted\n")).blur, - BlurModeName::Mica + BlurModeName::None ); assert_eq!( RuntimeSettings::from_raw(&RawSettings::from_text("blur=acrylic\n")).blur, @@ -1539,6 +1529,25 @@ mod tests { assert!(BlurModeName::Acrylic.enabled()); } + #[test] + fn opacity_defaults_to_opaque_and_preserves_explicit_values_for_every_material() { + for blur in ["none", "mica", "mica-alt", "aero", "acrylic", "1", "true"] { + let raw = format!("blur={blur}\n"); + let runtime = RuntimeSettings::from_raw(&RawSettings::from_text(&raw)); + assert_eq!(runtime.opacity, 1.0, "{blur}"); + assert_eq!(Some(runtime.blur), BlurModeName::from_settings(blur)); + for opacity in [0.0, 0.65, 1.0] { + let raw = format!("blur={blur}\nopacity={opacity}\n"); + let runtime = RuntimeSettings::from_raw(&RawSettings::from_text(&raw)); + assert_eq!(runtime.opacity, opacity, "{raw}"); + } + } + let invalid = + RuntimeSettings::from_raw(&RawSettings::from_text("blur=unknown\nopacity=invalid\n")); + assert_eq!(invalid.blur, BlurModeName::None); + assert_eq!(invalid.opacity, 1.0); + } + #[test] fn tabs_position_parses_supported_values_and_defaults_to_sidebar() { assert_eq!(TabsPositionName::from_settings("sidebar"), Some(TabsPositionName::Sidebar)); diff --git a/nebula_settings/src/reset.rs b/nebula_settings/src/reset.rs index f48470f..2e8c87c 100644 --- a/nebula_settings/src/reset.rs +++ b/nebula_settings/src/reset.rs @@ -127,7 +127,7 @@ mod tests { #[test] fn reset_removes_all_overrides_and_keeps_user_data() { - let text = "# preferences\r\n THEME = Nord\r\ncopy_on_select=1\nkeybind=ctrl+x:Copy\nFONT_SIZE=30\nexecutor=custom\nssh_hosts=saved-host\nai_provider=custom\nfuture_setting=keep\n"; + let text = "# preferences\r\n THEME = Nord\r\ncopy_on_select=1\nkeybind=ctrl+x:Copy\nFONT_SIZE=30\nexecutor=custom\nblur=acrylic\nopacity=0.65\nssh_hosts=saved-host\nai_provider=custom\nfuture_setting=keep\n"; let result = default_settings_text(text); assert_eq!( result, @@ -137,6 +137,8 @@ mod tests { let defaults = RuntimeSettings::from_raw(&RawSettings::default()); assert_eq!(runtime.theme, defaults.theme); assert_eq!(runtime.copy_on_select, defaults.copy_on_select); + assert_eq!(runtime.blur, crate::BlurModeName::None); + assert_eq!(runtime.opacity, 1.0); assert!(runtime.font_size_px.is_none()); assert!(runtime.shell.is_none()); assert!(crate::keybind_pairs_from_text(&result).is_empty()); diff --git a/nebula_settings/src/themes.rs b/nebula_settings/src/themes.rs index d4eaf06..99f4943 100644 --- a/nebula_settings/src/themes.rs +++ b/nebula_settings/src/themes.rs @@ -13,7 +13,7 @@ pub struct FreshPalette { impl ThemeName { /// The only catalog of selectable built-ins. Retired identifiers remain readable. - pub const BUILTIN: [Self; 13] = [ + pub const BUILTIN: [Self; 15] = [ Self::BreezeLight, Self::BreezeDark, Self::MintLight, @@ -25,12 +25,14 @@ impl ThemeName { Self::LinenLight, Self::CatppuccinMocha, Self::CatppuccinLatte, + Self::CatppuccinFrappe, + Self::CatppuccinMacchiato, Self::GlassLight, Self::GlassDark, ]; - pub const BUILTIN_NAMES: [&'static str; 13] = { - let mut names = [""; 13]; + pub const BUILTIN_NAMES: [&'static str; Self::BUILTIN.len()] = { + let mut names = [""; Self::BUILTIN.len()]; let mut index = 0; while index < Self::BUILTIN.len() { names[index] = Self::BUILTIN[index].prompt_name(); @@ -292,6 +294,40 @@ impl ThemeName { cyan: [0x17, 0x92, 0x99], frame: [0x9c, 0xa0, 0xb0], }, + // Catppuccin/palette (MIT): https://github.com/catppuccin/palette + // Base/Mantle surfaces, Text/Subtext 1 ink, Lavender accent. + Self::CatppuccinFrappe => ReviewedPalette { + shell: [0x29, 0x2c, 0x3c], + background: [0x30, 0x34, 0x46], + foreground: [0xc6, 0xd0, 0xf5], + muted: [0xb5, 0xbf, 0xe2], + accent: [0xba, 0xbb, 0xf1], + selected: [0x41, 0x45, 0x59, 255], + line: [0x51, 0x57, 0x6d, 255], + red: [0xe7, 0x82, 0x84], + green: [0xa6, 0xd1, 0x89], + yellow: [0xe5, 0xc8, 0x90], + blue: [0x8c, 0xaa, 0xee], + purple: [0xca, 0x9e, 0xe6], + cyan: [0x81, 0xc8, 0xbe], + frame: [0x73, 0x79, 0x94], + }, + Self::CatppuccinMacchiato => ReviewedPalette { + shell: [0x1e, 0x20, 0x30], + background: [0x24, 0x27, 0x3a], + foreground: [0xca, 0xd3, 0xf5], + muted: [0xb8, 0xc0, 0xe0], + accent: [0xb7, 0xbd, 0xf8], + selected: [0x36, 0x3a, 0x4f, 255], + line: [0x49, 0x4d, 0x64, 255], + red: [0xed, 0x87, 0x96], + green: [0xa6, 0xda, 0x95], + yellow: [0xee, 0xd4, 0x9f], + blue: [0x8a, 0xad, 0xf4], + purple: [0xc6, 0xa0, 0xf6], + cyan: [0x8b, 0xd5, 0xca], + frame: [0x6e, 0x73, 0x8d], + }, Self::GlassLight => ReviewedPalette { shell: [0xe9, 0xec, 0xef], background: [0xef, 0xf1, 0xf5], @@ -340,6 +376,15 @@ pub(crate) fn fresh_terminal(name: ThemeName) -> TermTheme { 0x5c5f77, 0xd20f39, 0x40a02b, 0xdf8e1d, 0x1e66f5, 0xea76cb, 0x179299, 0xacb0be, 0x6c6f85, 0xd20f39, 0x40a02b, 0xdf8e1d, 0x1e66f5, 0xea76cb, 0x179299, 0xbcc0cc, ], + // Official ANSI 0–15, including the distinct bright colors (2026-09-12). + ThemeName::CatppuccinFrappe => [ + 0x51576d, 0xe78284, 0xa6d189, 0xe5c890, 0x8caaee, 0xf4b8e4, 0x81c8be, 0xa5adce, + 0x626880, 0xe67172, 0x8ec772, 0xd9ba73, 0x7b9ef0, 0xf2a4db, 0x5abfb5, 0xb5bfe2, + ], + ThemeName::CatppuccinMacchiato => [ + 0x494d64, 0xed8796, 0xa6da95, 0xeed49f, 0x8aadf4, 0xf5bde6, 0x8bd5ca, 0xa5adcb, + 0x5b6078, 0xec7486, 0x8ccf7f, 0xe1c682, 0x78a1f6, 0xf2a9dd, 0x63cbc0, 0xb8c0e0, + ], ThemeName::GlassLight => [ 0x303030, 0xa31700, 0x0a7f3d, 0xaf551d, 0x006cd8, 0x583cac, 0x00798a, 0x494949, 0x8c8c8c, 0xa31700, 0x0a7f3d, 0xaf551d, 0x006cd8, 0x583cac, 0x00798a, 0x1c1c1c, @@ -393,6 +438,8 @@ mod tests { ThemeName::BreezeDark, ThemeName::MintLight, ThemeName::MintDark, + ThemeName::CatppuccinFrappe, + ThemeName::CatppuccinMacchiato, ] { assert_eq!(ThemeName::from_prompt_name(name.prompt_name()), Some(name)); let palette = name.fresh_palette().unwrap(); diff --git a/nebula_terminal/src/render/boxdraw.rs b/nebula_terminal/src/render/boxdraw.rs index 182325a..c01aad6 100644 --- a/nebula_terminal/src/render/boxdraw.rs +++ b/nebula_terminal/src/render/boxdraw.rs @@ -7,8 +7,8 @@ //! 且任何渲染后端都免费继承同一套字形。 //! //! 坐标系:单元格局部、逻辑像素、y 向下。内部先换算到设备像素做整数 -//! 吸附(与旧渲染器同规则),出口再除回 scale——这是笔画清晰、跨行列 -//! 连续的关键。与旧实现的两点已知差异: +//! 吸附,直线和圆角共享笔画边界规则,出口再除回 scale——这是笔画清晰、 +//! 跨行列连续的关键。与旧实现的另外两点已知差异: //! - Powerline 三角/箭头在窄单元格下不再回退字体,而是在右缘截平 //! (几何裁剪天然优雅,无需 None 逃逸路径); //! - ░▒▓ 以 alpha 表达浓度(64/128/192 ÷ 255,与旧像素灰度一致)。 @@ -61,8 +61,13 @@ pub fn primitives(c: char, width: f32, height: f32, scale: f32) -> Option (f32, f32) { + ((center - stroke / 2.0).round(), (center + stroke / 2.0).round()) +} + +/// 设备像素坐标下的图元累加器:像素吸附与画布边缘裁剪后输出几何。 struct Geom { w: f32, h: f32, @@ -85,17 +90,15 @@ impl Geom { self.h / 2.0 } - /// 横线在 `y` 处、笔画宽 `stroke` 的上下界(整数吸附,同旧 Canvas)。 + /// 横线在 `y` 处、笔画宽 `stroke` 的上下界(整数吸附后裁剪)。 fn h_line_bounds(&self, y: f32, stroke: f32) -> (f32, f32) { - let top = ((y - stroke / 2.0) as i32).max(0) as f32; - let bottom = ((y + stroke / 2.0) as i32).min(self.h as i32) as f32; - (top, bottom) + let (top, bottom) = stroke_bounds(y, stroke); + (top.max(0.0), bottom.min(self.h)) } fn v_line_bounds(&self, x: f32, stroke: f32) -> (f32, f32) { - let left = ((x - stroke / 2.0) as i32).max(0) as f32; - let right = ((x + stroke / 2.0) as i32).min(self.w as i32) as f32; - (left, right) + let (left, right) = stroke_bounds(x, stroke); + (left.max(0.0), right.min(self.w)) } /// 实心矩形,裁剪到单元格(旧 Canvas 在画布边缘截断的对应物)。 @@ -446,8 +449,8 @@ fn draw(c: char, g: &mut Geom) { // 比相邻直线偏半个像素。 let t = stroke; let half = t / 2.0; - let snap_beg = |base: f32| (base - half).round() + half; - let snap_end = |base: f32| (base + half).round() - half; + let snap_beg = |base: f32| stroke_bounds(base, t).0 + half; + let snap_end = |base: f32| stroke_bounds(base, t).1 - half; let (left, top, right, bottom, start_angle, end_angle) = match c { '\u{256d}' => ( snap_beg(w * 0.5), @@ -834,8 +837,8 @@ mod tests { let max_x = rects.iter().map(|(r, _)| r.x + r.w).fold(f32::MIN, f32::max); assert_eq!((min_x, max_x), (0.0, W)); for (rect, alpha) in &rects { - // y_center = 10,stroke 1:(10 - 0.5) as i32 = 9,高 1。 - assert_eq!((rect.y, rect.h, *alpha), (9.0, 1.0, 1.0)); + // y_center = 10,stroke 1:round(10 - 0.5) = 10,高 1。 + assert_eq!((rect.y, rect.h, *alpha), (10.0, 1.0, 1.0)); } } @@ -925,6 +928,69 @@ mod tests { } } + fn covers_pixel(primitives: &[Primitive], point: [f32; 2]) -> bool { + primitives.iter().any(|primitive| match primitive { + Primitive::Rect { rect, .. } => { + point[0] >= rect.x + && point[0] < rect.x + rect.w + && point[1] >= rect.y + && point[1] < rect.y + rect.h + }, + Primitive::Poly { points } => { + let mut positive = false; + let mut negative = false; + for (a, b) in points.iter().zip(points.iter().cycle().skip(1)) { + let cross = + (b[0] - a[0]) * (point[1] - a[1]) - (b[1] - a[1]) * (point[0] - a[0]); + positive |= cross > 1e-5; + negative |= cross < -1e-5; + } + !(positive && negative) + }, + }) + } + + #[test] + fn rounded_corners_join_straight_lines_at_every_device_pixel() { + for width in [7, 8, 9, 10, 13, 14, 15, 16, 20, 21, 24] { + for height in [14, 15, 20, 21, 32, 33] { + for scale in [1.0, 1.25, 1.5, 2.0] { + let glyph = |c| { + primitives(c, width as f32 / scale, height as f32 / scale, scale).unwrap() + }; + let horizontal = glyph('─'); + let vertical = glyph('│'); + for (c, right, down) in [ + ('╭', true, true), + ('╮', false, true), + ('╯', false, false), + ('╰', true, false), + ] { + let corner = glyph(c); + let x = if right { width as f32 - 0.5 } else { 0.5 } / scale; + let y = if down { height as f32 - 0.5 } else { 0.5 } / scale; + for row in 0..height { + let point = [x, (row as f32 + 0.5) / scale]; + assert_eq!( + covers_pixel(&corner, point), + covers_pixel(&horizontal, point), + "{c} horizontal seam at row {row}, {width}x{height}, scale {scale}", + ); + } + for column in 0..width { + let point = [(column as f32 + 0.5) / scale, y]; + assert_eq!( + covers_pixel(&corner, point), + covers_pixel(&vertical, point), + "{c} vertical seam at column {column}, {width}x{height}, scale {scale}", + ); + } + } + } + } + } + } + /// 圆角 '╭' 输出直臂 + 圆环分段,并像 WT 的 aliased cell clip 一样 /// 把扩展 rounded-rect 裁回当前单元格。 #[test] diff --git a/scripts/platform_cfg_budget.txt b/scripts/platform_cfg_budget.txt index e5a0177..629f34c 100644 --- a/scripts/platform_cfg_budget.txt +++ b/scripts/platform_cfg_budget.txt @@ -1 +1 @@ -488 +485