feat(gpui): GPUI UI layer moves into nebula_app - one product, one codebase

Physically relocates the validated GPUI surface out of the lab crate into nebula_app/src/gpui_shell/ (git mv keeps history): the terminal stack (view/element/session/keymap/mouse_protocol/colors), the config bridge (nebula.toml + nebula_settings.txt + theme tables), the workspace view and the theme/prelude glue. Per product ruling, nebula_gpui is a scratch lab - it keeps only the component gallery (terminal preview removed) plus annotated copies of theme/prelude, and its Cargo manifest drops the terminal/settings dependencies. The gpui-shell feature is now self-contained: nebula depends directly on gpui/gpui-component/-assets (redirected to our fork by the root patch table), nebula-settings and futures; the nebula-gpui crate dependency is gone. Entry points: 'nebula --gpui' runs the GPUI main-window form on the main thread with winit never starting (P3 acceptance form; gates get re-measured here), and the NEBULA_GPUI_SHELL=1 dedicated-thread spike stays until the P3 takeover completes. Verified: cargo check on both crates, and nebula --gpui opens the Nebula main window with working tabs, ConPTY terminal, CJK text and powerline prompt (screenshot .tmp_parity_gpui-in-nebula.png).
This commit is contained in:
kuddev committed 2026-08-13 13:29:51 +08:00
1 parent 6d3a076b80
commit ce140f526a
24 files changed
+220 -113

No files matched your search

Generated
+5 -7
View File
@@ -4570,10 +4570,14 @@ dependencies = [
"dirs 6.0.0",
"dwrote",
"embed-resource",
"futures",
"getrandom 0.3.3",
"gl_generator",
"global-hotkey",
"glutin",
"gpui",
"gpui-component",
"gpui-component-assets",
"home",
"html-escape",
"image",
@@ -4581,7 +4585,7 @@ dependencies = [
"log",
"mlua",
"nebula-completions",
"nebula-gpui",
"nebula-settings",
"nebula_config",
"nebula_config_derive",
"nebula_terminal",
@@ -4635,15 +4639,9 @@ dependencies = [
name = "nebula-gpui"
version = "0.1.0"
dependencies = [
"dirs 6.0.0",
"futures",
"gpui",
"gpui-component",
"gpui-component-assets",
"nebula-settings",
"nebula_terminal",
"serde",
"toml 0.9.11+spec-1.1.0",
]
[[package]]
+31 -6
View File
@@ -25,9 +25,28 @@ version = "1.0.0"
path = "../nebula_config"
version = "1.0.0"
# 可选:进程内 GPUI UI 层(feature = "gpui-shell")。
[dependencies.nebula-gpui]
path = "../nebula_gpui"
# 可选:GPUI UI 层(feature = "gpui-shell",模块 src/gpui_shell/)。
# gpui 与 gpui-component 成对锁定:0.5.1 编译在 gpui 0.2.2 上;两者由根
# Cargo.toml 的 [patch.crates-io] 重定向到我们的 fork。
[dependencies.gpui]
version = "=0.2.2"
optional = true
[dependencies.gpui-component]
version = "=0.5.1"
optional = true
[dependencies.gpui-component-assets]
version = "=0.5.1"
optional = true
# 运行时用户设置(nebula_settings.txt + 主题终端色表)的共享权威实现。
[dependencies.nebula-settings]
path = "../nebula_settings"
optional = true
[dependencies.futures]
version = "0.3"
optional = true
[dependencies.nebula-completions]
@@ -198,6 +217,12 @@ wayland = [
]
nightly = []
wgpu-foundation = ["dep:pollster", "dep:wgpu"]
# C 路线:在 nebula.exe 进程内的专用线程上拉起 GPUI UI 层。
# 默认关闭,不影响常规构建;开启后由 NEBULA_GPUI_SHELL=1 触发。
gpui-shell = ["dep:nebula-gpui"]
# GPUI UI 层(产品代码在 src/gpui_shell/)。默认关闭,不影响常规构建。
# 拉起:`nebula --gpui`(主窗形态)或 NEBULA_GPUI_SHELL=1(spike 并存形态)。
gpui-shell = [
"dep:gpui",
"dep:gpui-component",
"dep:gpui-component-assets",
"dep:nebula-settings",
"dep:futures",
]
+5
View File
@@ -33,6 +33,11 @@ pub struct Options {
#[clap(long)]
pub embed: Option<String>,
/// Launch the GPUI UI shell as the main window (P3 acceptance form).
#[cfg(feature = "gpui-shell")]
#[clap(long)]
pub gpui: bool,
/// Specify alternative configuration file [default:
/// $XDG_CONFIG_HOME/nebula/nebula.toml].
#[cfg(not(any(target_os = "macos", windows)))]
@@ -1,4 +1,4 @@
//! 读取用户配置:`nebula.toml` + `nebula_settings.txt`,与 `nebula_app`
//! 读取用户配置:`nebula.toml` + `nebula_settings.txt`,与 `nebula_app`
//! 共享同一套文件与语义。
//!
//! 两个来源、一个优先级:`nebula_settings.txt`(设置界面持久化的运行时
@@ -23,7 +23,7 @@ use nebula_settings::{CursorShapeName, RuntimeSettings};
use nebula_terminal::vte::ansi::CursorShape;
use serde::Deserialize;
use crate::terminal::colors::Palette;
use crate::gpui_shell::terminal::colors::Palette;
/// 应用启动时装载一次的全局设置。
pub struct Settings {
@@ -206,7 +206,7 @@ fn load_merged_toml(path: &Path) -> toml::Value {
if import_path.exists() {
merged = merge_values(merged, read_toml(&import_path));
} else {
eprintln!("[nebula-gpui] config import not found: {}", import_path.display());
eprintln!("[nebula:gpui] config import not found: {}", import_path.display());
}
}
merge_values(merged, main)
@@ -216,7 +216,7 @@ fn read_toml(path: &Path) -> toml::Value {
let text = match std::fs::read_to_string(path) {
Ok(text) => text,
Err(err) => {
eprintln!("[nebula-gpui] failed to read config {}: {err}", path.display());
eprintln!("[nebula:gpui] failed to read config {}: {err}", path.display());
return toml::Value::Table(Default::default());
},
};
@@ -224,7 +224,7 @@ fn read_toml(path: &Path) -> toml::Value {
match text.parse::<toml::Table>() {
Ok(table) => toml::Value::Table(table),
Err(err) => {
eprintln!("[nebula-gpui] failed to parse config {}: {err}", path.display());
eprintln!("[nebula:gpui] failed to parse config {}: {err}", path.display());
toml::Value::Table(Default::default())
},
}
@@ -234,7 +234,7 @@ fn read_toml(path: &Path) -> toml::Value {
fn resolve_import_path(import: &str, base_config: &Path) -> PathBuf {
let mut path = PathBuf::from(import);
if let Ok(stripped) = path.strip_prefix("~/") {
if let Some(home) = dirs::home_dir() {
if let Some(home) = home::home_dir() {
path = home.join(stripped);
}
}
+70
View File
@@ -0,0 +1,70 @@
//! GPUI UI 层(产品代码,feature = "gpui-shell")。
//!
//! 这里是 nebula.exe 体内的新 UI:终端视图/渲染元素/工作区与配置桥全部
//! 住在本模块,`nebula_gpui` crate 只是组件验收的实验场,不承载产品代码
//! (ROADMAP 终局形态第 1 条)。
//!
//! 两种拉起形态:
//! - `nebula --gpui`:GPUI 主窗形态——主线程直接进 GPUI 消息循环,winit
//! 旧壳完全不启动。P3 的产品验收形态;三闸门复测都在这里做。
//! - `NEBULA_GPUI_SHELL=1`(需 gpui-shell 构建):spike 形态——GPUI 跑在
//! 专用线程,与 winit 旧壳同进程并存,仅用于双运行时验证,P3 完成后移除。
pub mod config;
pub mod prelude;
pub mod terminal;
pub mod theme;
pub mod workspace;
use gpui::{App, AppContext as _, Bounds, WindowBounds, WindowOptions, px, size};
use gpui_component::{Root, TitleBar};
use gpui_component_assets::Assets;
use workspace::NebulaWorkspace;
/// 在当前线程启动 GPUI 运行时并打开主窗口,阻塞直至 UI 退出。
///
/// GPUI 拥有自己的消息循环:主窗形态从主线程调用(winit 不启动);
/// spike 形态从专用线程调用。一个进程内只允许调用一次。
pub fn run_shell() {
gpui::Application::new().with_assets(Assets).run(|cx| {
init(cx);
cx.activate(true);
open_main_window(cx);
});
}
/// 组件库/主题/快捷键/用户配置的一次性初始化。
fn init(cx: &mut App) {
// 组件库必须只初始化一次,否则全局 action、菜单和主题状态会重复注册。
gpui_component::init(cx);
theme::apply_nebula_theme(cx);
workspace::init(cx);
// 用户 nebula.toml + nebula_settings.txt,启动读一次;失败静默回退默认。
// 诊断走 stderr:GPUI 主窗形态在旧壳 logger 建立之前拉起。
let settings = config::Settings::load();
match &settings.source_path {
Some(path) => eprintln!("[nebula:gpui] config loaded: {}", path.display()),
None => eprintln!("[nebula:gpui] no user config found, using defaults"),
}
cx.set_global(settings);
}
fn open_main_window(cx: &mut App) {
let bounds = Bounds::centered(None, size(px(1080.0), px(720.0)), cx);
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
window_min_size: Some(size(px(760.0), px(540.0))),
// 自绘 TitleBar 要求系统标题栏透明化,窗口控制按钮由组件库接管。
titlebar: Some(TitleBar::title_bar_options()),
app_id: Some("nebula".to_owned()),
..Default::default()
};
cx.open_window(options, |window, cx| {
let workspace = cx.new(|cx| NebulaWorkspace::new(window, cx));
cx.new(|cx| Root::new(workspace, window, cx))
})
.expect("failed to open Nebula GPUI window");
}
+25
View File
@@ -0,0 +1,25 @@
//! 业务视图只从这里导入获准使用的第三方组件。
//!
//! 简单组件直接复用;只有需要承载 Nebula 业务合同的组件才在本 crate 中封装。
// 这些导出是受控的业务入口,未被当前验收页使用的组件仍应保留,避免业务代码
// 重新从上游散落导入而绕过升级边界。
#[allow(unused_imports)]
pub use gpui_component::{
ActiveTheme as _, Icon, IconName, Sizable as _, TitleBar,
button::{Button, ButtonVariants as _},
checkbox::Checkbox,
dialog::{Dialog, DialogButtonProps},
dock::{DockArea, DockAreaState, DockItem, Panel, PanelControl, PanelEvent, PanelState, TabPanel},
h_flex,
input::{Input, InputState},
menu::{ContextMenuExt as _, PopupMenu},
scroll::ScrollableElement as _,
select::{Select, SelectState},
sidebar::{Sidebar, SidebarGroup, SidebarMenu, SidebarMenuItem},
tab::{Tab, TabBar},
v_flex,
};
pub use gpui_component::IndexPath;
pub use gpui_component::WindowExt as _;
File renamed without changes.
@@ -23,7 +23,7 @@ use super::element::TerminalElement;
use super::keymap;
use super::mouse_protocol;
use super::session::{self, GridSize, TerminalSession};
use crate::config::Settings;
use crate::gpui_shell::config::Settings;
use futures::StreamExt as _;
+46
View File
@@ -0,0 +1,46 @@
use gpui::{App, hsla, px};
use gpui_component::{Theme, ThemeMode};
pub fn apply_nebula_theme(cx: &mut App) {
Theme::change(ThemeMode::Dark, None, cx);
// 先改全局 token,而不是逐组件覆写样式,确保后续直接引入的组件自然进入
// Nebula 视觉系统,也让上游升级时需要维护的差异集中在一个位置。
let theme = Theme::global_mut(cx);
let background = hsla(220.0 / 360.0, 0.16, 0.075, 1.0);
let surface = hsla(220.0 / 360.0, 0.13, 0.105, 1.0);
let border = hsla(216.0 / 360.0, 0.12, 0.21, 1.0);
let text = hsla(210.0 / 360.0, 0.12, 0.91, 1.0);
let muted_text = hsla(214.0 / 360.0, 0.09, 0.62, 1.0);
let accent = hsla(155.0 / 360.0, 0.62, 0.47, 1.0);
let accent_hover = hsla(155.0 / 360.0, 0.58, 0.52, 1.0);
let accent_active = hsla(155.0 / 360.0, 0.66, 0.40, 1.0);
theme.background = background;
theme.foreground = text;
theme.group_box = surface;
theme.group_box_foreground = text;
theme.popover = surface;
theme.popover_foreground = text;
theme.border = border;
theme.input = border;
theme.primary = accent;
theme.primary_hover = accent_hover;
theme.primary_active = accent_active;
theme.primary_foreground = background;
theme.secondary = surface;
theme.secondary_hover = border;
theme.secondary_active = background;
theme.secondary_foreground = text;
theme.muted = surface;
theme.muted_foreground = muted_text;
theme.accent = accent;
theme.accent_foreground = background;
theme.caret = accent;
theme.ring = accent;
theme.selection = hsla(155.0 / 360.0, 0.52, 0.34, 0.72);
theme.font_size = px(14.0);
theme.mono_font_size = px(13.0);
theme.radius = px(5.0);
theme.radius_lg = px(7.0);
}
@@ -11,8 +11,8 @@ use gpui::{
Styled as _, Subscription, Window, div, px,
};
use crate::terminal::view::{TerminalView, TerminalViewEvent};
use crate::ui::prelude::*;
use crate::gpui_shell::prelude::*;
use crate::gpui_shell::terminal::view::{TerminalView, TerminalViewEvent};
gpui::actions!(nebula_workspace, [NewTerminal, CloseActiveTerminal, ToggleSidebar]);
@@ -199,18 +199,6 @@ impl NebulaWorkspace {
),
)
.child(v_flex().flex_1().gap_1().children(items))
.child(
h_flex().px_1().child(
Button::new("open-gallery")
.icon(IconName::Settings)
.ghost()
.small()
.tooltip("组件验收页")
.on_click(|_, _, cx| {
crate::app::open_gallery_window(cx);
}),
),
)
}
}
+12 -2
View File
@@ -46,6 +46,8 @@ mod event;
mod file_uri;
#[cfg(windows)]
mod font_install;
#[cfg(feature = "gpui-shell")]
mod gpui_shell;
mod input;
mod logging;
#[cfg(target_os = "macos")]
@@ -129,14 +131,22 @@ fn main() -> Result<(), Box<dyn Error>> {
// Load command line options.
let options = Options::new();
// P3 主窗形态:GPUI 作为 nebula.exe 的 UI 层,从主线程直接进 GPUI
// 消息循环,winit 旧壳完全不启动。三闸门在此形态复测。
#[cfg(feature = "gpui-shell")]
if options.gpui {
gpui_shell::run_shell();
return Ok(());
}
// C 路线 spike:GPUI UI 层跑在专用线程,拥有自己的消息循环与窗口;
// 与主线程的 winit 循环互不接管。仅在显式设置环境变量时启动,
// 用于验证双 UI 运行时共存(焦点/IME/DPI)。
// 用于验证双 UI 运行时共存(焦点/IME/DPI);P3 主窗接管完成后移除。
#[cfg(feature = "gpui-shell")]
if std::env::var_os("NEBULA_GPUI_SHELL").is_some() {
std::thread::Builder::new()
.name("gpui-shell".into())
.spawn(nebula_gpui::run_shell)
.spawn(gpui_shell::run_shell)
.expect("spawn gpui-shell thread");
}
+3 -13
View File
@@ -2,13 +2,13 @@
name = "nebula-gpui"
version = "0.1.0"
license = "GPL-3.0-or-later"
description = "Nebula's GPUI application foundation and component acceptance surface"
description = "Nebula's GPUI component acceptance lab (scratch; product UI lives in nebula_app)"
publish = false
edition = "2024"
rust-version = "1.85.0"
# lib + bin 双形态:bin 是开发/验证入口;lib 供 nebula.exe(C 路线)
# 在专用线程上以 run_shell() 拉起进程内 GPUI UI 层。
# lib + bin 双形态:bin 是组件验收入口;产品 GPUI UI 层已迁入
# nebula_app/src/gpui_shell/(feature = "gpui-shell"),本 crate 不再承载。
[lib]
name = "nebula_gpui"
path = "src/lib.rs"
@@ -25,16 +25,6 @@ path = "src/main.rs"
gpui = "=0.2.2"
gpui-component = "=0.5.1"
gpui-component-assets = "=0.5.1"
# 终端核心是纯 Rust(vte/grid/ConPTY),没有 GUI 原生依赖,因此可以跨 workspace
# 边界直接 path 引用而不会重新引入 fontconfig links 冲突。
nebula_terminal = { path = "../nebula_terminal" }
# 运行时用户设置(nebula_settings.txt + 主题终端色表)的共享权威实现。
nebula-settings = { path = "../nebula_settings" }
futures = "0.3"
# 读取用户 nebula.toml 的字体/配色子集;版本与主仓保持一致。
serde = { version = "1", features = ["derive"] }
toml = "0.9"
dirs = "6.0.0"
[lints.clippy]
dbg_macro = "deny"
+1 -21
View File
@@ -1,28 +1,8 @@
use gpui::{App, AppContext as _, Bounds, WindowBounds, WindowOptions, px, size};
use gpui_component::{Root, TitleBar};
use gpui_component::Root;
use crate::views::component_gallery::ComponentGallery;
use crate::views::workspace::NebulaWorkspace;
pub fn open_main_window(cx: &mut App) {
let bounds = Bounds::centered(None, size(px(1080.0), px(720.0)), cx);
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
window_min_size: Some(size(px(760.0), px(540.0))),
// 自绘 TitleBar 要求系统标题栏透明化,窗口控制按钮由组件库接管。
titlebar: Some(TitleBar::title_bar_options()),
app_id: Some("nebula-gpui".to_owned()),
..Default::default()
};
cx.open_window(options, |window, cx| {
let workspace = cx.new(|cx| NebulaWorkspace::new(window, cx));
cx.new(|cx| Root::new(workspace, window, cx))
})
.expect("failed to open Nebula GPUI window");
}
/// 组件验收页降级为辅助窗口,从工作区标题栏进入,仍用于回归组件接入面。
pub fn open_gallery_window(cx: &mut App) {
let bounds = Bounds::centered(None, size(px(1080.0), px(720.0)), cx);
let options = WindowOptions {
+7 -10
View File
@@ -1,23 +1,20 @@
//! GPUI UI 层(C 路线):既可作为独立 bin 运行(开发/验证形态),
//! 也可被 nebula.exe 作为库在专用线程上拉起(产品形态)。
//! 铁律:领域逻辑不进本 crate;这里只有视图与组件接线。
//! GPUI 组件验收实验场(scratch)。
//!
//! 产品代码不在这里:GPUI UI 层住在 `nebula_app/src/gpui_shell/`
//! (feature = "gpui-shell"),本 crate 只保留组件验收页,用于快速验证
//! gpui-component 组件、fork 补丁与上游升级回归,验证通过的方案进产品。
mod app;
mod config;
mod terminal;
mod ui;
mod views;
use gpui_component_assets::Assets;
/// 在当前线程启动 GPUI 运行时并打开主窗口,阻塞直至 UI 退出。
///
/// GPUI 拥有自己的消息循环,宿主必须从一个专用线程调用(不能是
/// winit 主循环所在线程)。一个进程内只允许调用一次。
/// 在当前线程启动 GPUI 运行时并打开组件验收窗口,阻塞直至 UI 退出。
pub fn run_shell() {
gpui::Application::new().with_assets(Assets).run(|cx| {
ui::bootstrap::init(cx);
cx.activate(true);
app::open_main_window(cx);
app::open_gallery_window(cx);
});
}
-9
View File
@@ -4,13 +4,4 @@ pub fn init(cx: &mut App) {
// 组件库必须只初始化一次,否则全局 action、菜单和主题状态会重复注册。
gpui_component::init(cx);
super::theme::apply_nebula_theme(cx);
crate::views::workspace::init(cx);
// 用户 nebula.toml(字体/终端配色),启动读一次;失败静默回退默认值。
let settings = crate::config::Settings::load();
match &settings.source_path {
Some(path) => eprintln!("[nebula-gpui] config loaded: {}", path.display()),
None => eprintln!("[nebula-gpui] no user config found, using defaults"),
}
cx.set_global(settings);
}
+2 -3
View File
@@ -1,6 +1,5 @@
//! 业务视图只从这里导入获准使用的第三方组件。
//!
//! 简单组件直接复用;只有需要承载 Nebula 业务合同的组件才在本 crate 中封装。
//! 实验场副本:产品权威在 `nebula_app/src/gpui_shell/prelude.rs`。
//! 业务视图只从这里导入获准使用的第三方组件;改导出面时两处同步。
// 这些导出是受控的业务入口,未被当前验收页使用的组件仍应保留,避免业务代码
// 重新从上游散落导入而绕过升级边界。
+3
View File
@@ -1,3 +1,6 @@
//! 实验场副本:产品权威在 `nebula_app/src/gpui_shell/theme.rs`。
//! gallery 用产品同款视觉 token 验收组件;改 token 时两处同步。
use gpui::{App, hsla, px};
use gpui_component::{Theme, ThemeMode};
+1 -20
View File
@@ -4,7 +4,6 @@ use gpui::{
Window, div, px,
};
use crate::terminal::view::TerminalView;
use crate::ui::prelude::*;
gpui::actions!(component_gallery, [Increment]);
@@ -14,7 +13,6 @@ pub struct ComponentGallery {
editor: Entity<InputState>,
select: Entity<SelectState<Vec<&'static str>>>,
dock_area: Entity<DockArea>,
terminal: Entity<TerminalView>,
enabled: bool,
clicks: usize,
active_tab: usize,
@@ -95,9 +93,7 @@ impl ComponentGallery {
dock.set_center(DockItem::tab(panel, &weak_dock_area, window, cx), window, cx);
});
let terminal = cx.new(TerminalView::new);
Self { input, editor, select, dock_area, terminal, enabled: true, clicks: 0, active_tab: 0 }
Self { input, editor, select, dock_area, enabled: true, clicks: 0, active_tab: 0 }
}
}
@@ -238,21 +234,6 @@ impl Render for ComponentGallery {
.child(self.dock_area.clone()),
),
)
.child(
div().text_sm().child(
"终端垂直切片(ConPTY + nebula_terminal + 自定义 Element)",
),
)
.child(
div()
.w_full()
.h(px(360.0))
.border_1()
.border_color(cx.theme().border)
.rounded_md()
.overflow_hidden()
.child(self.terminal.clone()),
)
.child(div().text_sm().child("Code Editor / Tree-sitter 基础"))
.child(Input::new(&self.editor).h(px(220.0)))
.child(
-1
View File
@@ -1,2 +1 @@
pub mod component_gallery;
pub mod workspace;