From 37e40073ef5b8a2f3600c1ced5bb67688755580c Mon Sep 17 00:00:00 2001 From: Sergio Date: Sun, 10 May 2026 15:19:17 +0000 Subject: [PATCH] =?UTF-8?q?feat(yahweh-launcher):=20F3=20=E2=80=94=20extra?= =?UTF-8?q?cci=C3=B3n=20del=20shell=20standard=20de=20explorers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iter 19. Patrón con 4 consumers idénticos: cada main() repetía el mismo ~20 líneas de boot (Application::new + Theme::install_default + cx.open_window + WindowOptions + cx.activate). Sólo varían título, tamaño y root factory. crates/modules/ui_engine/libs/launcher/: - pub fn launch_app(title, size, root_factory) → 1-line boot. - pub fn launch_app_with(config, root_factory) → variante con config armado afuera (env-var driven, etc). - pub struct AppLaunchConfig::new(title, size). - 2 tests cubren normalización del config. Migración 4 consumers (nakui/nouser/minga/brahman-broker explorer): - main() pasa de ~20 líneas a 1: launch_app(...). - Imports gpui podados (no más App/Application/Bounds/WindowOpts/etc). - Cada uno agrega dep yahweh-launcher. Naming: yahweh-shell ya existe (bootstrap heavyweight con file/db/text viewers en crates/apps/). Helper liviano queda como yahweh-launcher. Ahorro ~75 líneas de boot hardcoded. Cambios de window/theme boot ahora en un solo lugar. 2/2 tests launcher; 4 consumer suites intactas, todo verde. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 38 ++++++ Cargo.lock | 12 ++ Cargo.toml | 1 + .../apps/brahman-broker-explorer/Cargo.toml | 1 + .../apps/brahman-broker-explorer/src/main.rs | 24 +--- crates/apps/minga-explorer/Cargo.toml | 1 + crates/apps/minga-explorer/src/main.rs | 24 +--- crates/apps/nakui-explorer/Cargo.toml | 1 + crates/apps/nakui-explorer/src/main.rs | 27 +--- crates/apps/nouser-explorer/Cargo.toml | 1 + crates/apps/nouser-explorer/src/main.rs | 26 +--- .../ui_engine/libs/launcher/Cargo.toml | 13 ++ .../ui_engine/libs/launcher/src/lib.rs | 123 ++++++++++++++++++ 13 files changed, 207 insertions(+), 85 deletions(-) create mode 100644 crates/modules/ui_engine/libs/launcher/Cargo.toml create mode 100644 crates/modules/ui_engine/libs/launcher/src/lib.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 30c9354..eb29652 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,44 @@ ratio/diff ver `git show `. ## 2026-05-10 +### feat(yahweh-launcher): F3 — extracción del shell standard de explorers +Iter 19. Patrón con 4 consumers idénticos (nakui-explorer, +nouser-explorer, minga-explorer, brahman-broker-explorer) declaraban +~20 líneas de boot: +`Application::new + Theme::install_default + cx.open_window ++ WindowOptions{ window_bounds, titlebar } + cx.activate(true)`. +Todo idéntico salvo título, tamaño, y root entity factory. + +Crate nuevo `crates/modules/ui_engine/libs/launcher/` +(`yahweh-launcher`): +- **Deps**: `gpui`, `yahweh-theme`. Sin más. +- **`pub fn launch_app(title, size, root_factory)`**: 1-line boot. +- **`pub fn launch_app_with(config, root_factory)`**: variante con + `AppLaunchConfig` armado afuera, para casos donde título/tamaño se + computan condicionalmente (env var, etc). +- **`AppLaunchConfig::new(title, size)`**: builder normalizador. +- 2 tests `#[test]` cubren la normalización de config (no se testea + `launch_app` per se porque bloquea el thread main hasta que la + ventana se cierra). + +Migración 4 consumers: +- `main()` pasa de 20 líneas a 1: `launch_app("Title", (W, H), Explorer::new)`. +- Imports de gpui se podan: ya no necesitan `App, Application, + Bounds, WindowBounds, WindowOptions` ni `SharedString` ni `prelude::*`. +- Cada consumer agrega dep `yahweh-launcher` (path local). + +Naming: el slot natural era `yahweh-shell`, pero ya existe un crate +`yahweh-shell` en `crates/apps/` (bootstrap heavyweight con file +explorer + DB explorer + viewers). El helper es liviano y específico +al patrón de launch, así que `yahweh-launcher` evita confusión. + +Total ahorro: ~75 líneas hardcoded de boilerplate UI a 4 líneas en +total. Cambios de boot (window opts, theme, etc) ahora viven en un +solo lugar. + +Tests stack: 2 nuevos en launcher; suites de los 4 consumers +intactas. Todo verde. + ### chore(.gitignore): excluir .claude/ (state local de Claude Code) Iter 18. Side cleanup tras debugging: `.claude/` aparecía en `git status` cada sesión (contenía `scheduled_tasks.lock` y diff --git a/Cargo.lock b/Cargo.lock index 3a464e4..23e415a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1242,6 +1242,7 @@ dependencies = [ "brahman-handshake", "brahman-sidecar", "gpui", + "yahweh-launcher", "yahweh-theme", "yahweh-widget-app-header", "yahweh-widget-banner", @@ -6215,6 +6216,7 @@ version = "0.1.0" dependencies = [ "gpui", "minga-store", + "yahweh-launcher", "yahweh-theme", "yahweh-widget-app-header", "yahweh-widget-banner", @@ -6484,6 +6486,7 @@ dependencies = [ "serde_json", "tempfile", "uuid", + "yahweh-launcher", "yahweh-meta-runtime", "yahweh-theme", "yahweh-widget-app-header", @@ -6882,6 +6885,7 @@ dependencies = [ "brahman-sidecar", "gpui", "nouser-card", + "yahweh-launcher", "yahweh-theme", "yahweh-widget-app-header", "yahweh-widget-banner", @@ -12944,6 +12948,14 @@ dependencies = [ "yahweh-theme", ] +[[package]] +name = "yahweh-launcher" +version = "0.1.0" +dependencies = [ + "gpui", + "yahweh-theme", +] + [[package]] name = "yahweh-meta-runtime" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 76e62f5..ee99d5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ members = [ # ============================================================ "crates/modules/ui_engine/libs/core", "crates/modules/ui_engine/libs/theme", + "crates/modules/ui_engine/libs/launcher", "crates/modules/ui_engine/libs/bus", "crates/modules/ui_engine/libs/meta-schema", "crates/modules/ui_engine/libs/meta-runtime", diff --git a/crates/apps/brahman-broker-explorer/Cargo.toml b/crates/apps/brahman-broker-explorer/Cargo.toml index 7c5eda7..1f6baa3 100644 --- a/crates/apps/brahman-broker-explorer/Cargo.toml +++ b/crates/apps/brahman-broker-explorer/Cargo.toml @@ -9,6 +9,7 @@ description = "Probe GUI del broker brahman: conecta cada N segundos vía await_ brahman-handshake = { path = "../../core/brahman-handshake" } brahman-sidecar = { path = "../../shared/brahman-sidecar" } yahweh-theme = { path = "../../modules/ui_engine/libs/theme" } +yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" } yahweh-widget-banner = { path = "../../modules/ui_engine/widgets/banner" } yahweh-widget-stat-card = { path = "../../modules/ui_engine/widgets/stat-card" } yahweh-widget-app-header = { path = "../../modules/ui_engine/widgets/app-header" } diff --git a/crates/apps/brahman-broker-explorer/src/main.rs b/crates/apps/brahman-broker-explorer/src/main.rs index 0db0433..17e6763 100644 --- a/crates/apps/brahman-broker-explorer/src/main.rs +++ b/crates/apps/brahman-broker-explorer/src/main.rs @@ -29,35 +29,19 @@ use std::time::{Duration, Instant}; use brahman_handshake::transport; use brahman_sidecar::{await_provider_blocking, build_consumer_card, ConsumerError}; use gpui::{ - div, prelude::*, px, App, Application, Bounds, Context, IntoElement, Render, SharedString, - Window, WindowBounds, WindowOptions, + div, prelude::*, px, Context, IntoElement, Render, SharedString, Window, }; +use yahweh_launcher::launch_app; use yahweh_theme::Theme; +use yahweh_widget_app_header::app_header; use yahweh_widget_banner::{banner_themed, Banner}; use yahweh_widget_stat_card::stat_card; -use yahweh_widget_app_header::app_header; const POLL_INTERVAL: Duration = Duration::from_secs(5); const PROBE_TIMEOUT: Duration = Duration::from_secs(1); fn main() { - Application::new().run(|cx: &mut App| { - Theme::install_default(cx); - let bounds = Bounds::centered(None, gpui::size(px(720.), px(480.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - titlebar: Some(gpui::TitlebarOptions { - title: Some(SharedString::from("Brahman Broker — Probe")), - ..Default::default() - }), - ..Default::default() - }, - |_w, cx| cx.new(Explorer::new), - ) - .expect("open window"); - cx.activate(true); - }); + launch_app("Brahman Broker — Probe", (720., 480.), Explorer::new); } /// Snapshot de un probe. diff --git a/crates/apps/minga-explorer/Cargo.toml b/crates/apps/minga-explorer/Cargo.toml index 68304c5..f4d55d2 100644 --- a/crates/apps/minga-explorer/Cargo.toml +++ b/crates/apps/minga-explorer/Cargo.toml @@ -8,6 +8,7 @@ description = "Dashboard GPUI del repo Minga: counts de nodos AST, atestaciones, [dependencies] minga-store = { path = "../../modules/semantic_dht/minga-store" } yahweh-theme = { path = "../../modules/ui_engine/libs/theme" } +yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" } yahweh-widget-banner = { path = "../../modules/ui_engine/widgets/banner" } yahweh-widget-stat-card = { path = "../../modules/ui_engine/widgets/stat-card" } yahweh-widget-app-header = { path = "../../modules/ui_engine/widgets/app-header" } diff --git a/crates/apps/minga-explorer/src/main.rs b/crates/apps/minga-explorer/src/main.rs index 335e53d..1200ed0 100644 --- a/crates/apps/minga-explorer/src/main.rs +++ b/crates/apps/minga-explorer/src/main.rs @@ -27,36 +27,20 @@ use std::path::PathBuf; use std::time::Duration; use gpui::{ - div, prelude::*, px, App, Application, Bounds, Context, IntoElement, Render, SharedString, - Window, WindowBounds, WindowOptions, + div, prelude::*, px, Context, IntoElement, Render, SharedString, Window, }; use minga_store::PersistentRepo; +use yahweh_launcher::launch_app; use yahweh_theme::Theme; +use yahweh_widget_app_header::app_header; use yahweh_widget_banner::{banner_themed, Banner}; use yahweh_widget_stat_card::stat_card; -use yahweh_widget_app_header::app_header; const REFRESH_INTERVAL: Duration = Duration::from_secs(2); const REPO_DIRNAME: &str = "repo"; fn main() { - Application::new().run(|cx: &mut App| { - Theme::install_default(cx); - let bounds = Bounds::centered(None, gpui::size(px(800.), px(560.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - titlebar: Some(gpui::TitlebarOptions { - title: Some(SharedString::from("Minga — Repo")), - ..Default::default() - }), - ..Default::default() - }, - |_w, cx| cx.new(Explorer::new), - ) - .expect("open window"); - cx.activate(true); - }); + launch_app("Minga — Repo", (800., 560.), Explorer::new); } /// Cuántos items recientes mostrar por sección. Los stores no diff --git a/crates/apps/nakui-explorer/Cargo.toml b/crates/apps/nakui-explorer/Cargo.toml index 141df07..bb69773 100644 --- a/crates/apps/nakui-explorer/Cargo.toml +++ b/crates/apps/nakui-explorer/Cargo.toml @@ -11,6 +11,7 @@ yahweh-meta-runtime = { path = "../../modules/ui_engine/libs/meta-runtime" } yahweh-widget-banner = { path = "../../modules/ui_engine/widgets/banner" } yahweh-widget-card = { path = "../../modules/ui_engine/widgets/card" } yahweh-theme = { path = "../../modules/ui_engine/libs/theme" } +yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" } yahweh-widget-app-header = { path = "../../modules/ui_engine/widgets/app-header" } gpui = { workspace = true } serde_json = { workspace = true } diff --git a/crates/apps/nakui-explorer/src/main.rs b/crates/apps/nakui-explorer/src/main.rs index 54d54d4..632c95f 100644 --- a/crates/apps/nakui-explorer/src/main.rs +++ b/crates/apps/nakui-explorer/src/main.rs @@ -26,39 +26,20 @@ use std::path::PathBuf; use std::time::Duration; use gpui::{ - div, prelude::*, px, rgb, App, Application, Bounds, Context, IntoElement, Render, - SharedString, Window, WindowBounds, WindowOptions, + div, prelude::*, px, rgb, Context, IntoElement, Render, SharedString, Window, }; use nakui_core::event_log::{EventLog, LogEntry}; +use yahweh_launcher::launch_app; use yahweh_meta_runtime::{preview_value, short_hash, short_uuid}; use yahweh_theme::Theme; -use yahweh_widget_banner::{banner_themed, Banner}; use yahweh_widget_app_header::app_header; +use yahweh_widget_banner::{banner_themed, Banner}; use yahweh_widget_card::card_themed; const REFRESH_INTERVAL: Duration = Duration::from_secs(2); fn main() { - Application::new().run(|cx: &mut App| { - // Theme global instalado al boot — los widgets themed lo - // requieren, y simplifica el chrome del app a una paleta - // consistente. - Theme::install_default(cx); - let bounds = Bounds::centered(None, gpui::size(px(900.), px(640.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - titlebar: Some(gpui::TitlebarOptions { - title: Some(SharedString::from("Nakui — Event Log")), - ..Default::default() - }), - ..Default::default() - }, - |_w, cx| cx.new(Explorer::new), - ) - .expect("open window"); - cx.activate(true); - }); + launch_app("Nakui — Event Log", (900., 640.), Explorer::new); } /// Estado de la vista. `entries` se reescribe en cada tick (el log diff --git a/crates/apps/nouser-explorer/Cargo.toml b/crates/apps/nouser-explorer/Cargo.toml index f79dae0..e29f4f5 100644 --- a/crates/apps/nouser-explorer/Cargo.toml +++ b/crates/apps/nouser-explorer/Cargo.toml @@ -10,6 +10,7 @@ brahman-card = { path = "../../core/brahman-card" } brahman-sidecar = { path = "../../shared/brahman-sidecar" } nouser-card = { path = "../../modules/nouser/card" } yahweh-theme = { path = "../../modules/ui_engine/libs/theme" } +yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" } yahweh-widget-banner = { path = "../../modules/ui_engine/widgets/banner" } yahweh-widget-card = { path = "../../modules/ui_engine/widgets/card" } yahweh-widget-app-header = { path = "../../modules/ui_engine/widgets/app-header" } diff --git a/crates/apps/nouser-explorer/src/main.rs b/crates/apps/nouser-explorer/src/main.rs index 39418aa..2205556 100644 --- a/crates/apps/nouser-explorer/src/main.rs +++ b/crates/apps/nouser-explorer/src/main.rs @@ -23,41 +23,23 @@ use std::time::Duration; use brahman_sidecar::{await_provider_blocking, build_consumer_card, ConsumerError}; use gpui::{ - div, prelude::*, px, rgb, App, Application, Bounds, Context, IntoElement, Render, SharedString, - Window, WindowBounds, WindowOptions, + div, prelude::*, px, rgb, Context, IntoElement, Render, SharedString, Window, }; use nouser_card::query::client as query_client; use nouser_card::query::{transport, ListMonadsResponse, FLOW_MONAD_LIST, FLOW_TYPE_NAME}; use nouser_card::Lens; +use yahweh_launcher::launch_app; use yahweh_theme::Theme; +use yahweh_widget_app_header::app_header; use yahweh_widget_banner::{banner_themed, Banner}; use yahweh_widget_card::card_themed; -use yahweh_widget_app_header::app_header; const REFRESH_INTERVAL: Duration = Duration::from_secs(2); const DISCOVERY_TIMEOUT: Duration = Duration::from_secs(3); const QUERY_TIMEOUT: Duration = Duration::from_secs(2); fn main() { - Application::new().run(|cx: &mut App| { - // Theme global instalado al boot — los widgets themed lo - // requieren y unifica el chrome del app. - Theme::install_default(cx); - let bounds = Bounds::centered(None, gpui::size(px(900.), px(640.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - titlebar: Some(gpui::TitlebarOptions { - title: Some(SharedString::from("Nouser — Mónadas")), - ..Default::default() - }), - ..Default::default() - }, - |_w, cx| cx.new(Explorer::new), - ) - .expect("open window"); - cx.activate(true); - }); + launch_app("Nouser — Mónadas", (900., 640.), Explorer::new); } /// Vista raíz: cachea el socket descubierto, el último snapshot y el diff --git a/crates/modules/ui_engine/libs/launcher/Cargo.toml b/crates/modules/ui_engine/libs/launcher/Cargo.toml new file mode 100644 index 0000000..6557234 --- /dev/null +++ b/crates/modules/ui_engine/libs/launcher/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "yahweh-launcher" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +description = "Launcher GPUI reusable: Application::new + Theme::install_default + cx.open_window + cx.activate. Las explorer apps lo invocan en una sola línea (`launch_app(title, size, root_factory)`)." + +[dependencies] +gpui = { workspace = true } +yahweh-theme = { path = "../theme" } + +[dev-dependencies] +gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/modules/ui_engine/libs/launcher/src/lib.rs b/crates/modules/ui_engine/libs/launcher/src/lib.rs new file mode 100644 index 0000000..fb2b284 --- /dev/null +++ b/crates/modules/ui_engine/libs/launcher/src/lib.rs @@ -0,0 +1,123 @@ +//! Yahweh shell — reduce el boot de un app GPUI temed a una línea. +//! +//! Las 4 (próximamente más) apps explorer del repo declaran el mismo +//! patrón: `Application::new + Theme::install_default + cx.open_window +//! + cx.activate(true)`. Sólo varían el título, el tamaño inicial y la +//! fábrica del root entity. +//! +//! Antes (~20 líneas): +//! +//! ```ignore +//! Application::new().run(|cx: &mut App| { +//! Theme::install_default(cx); +//! let bounds = Bounds::centered(None, gpui::size(px(900.), px(640.)), cx); +//! cx.open_window( +//! WindowOptions { +//! window_bounds: Some(WindowBounds::Windowed(bounds)), +//! titlebar: Some(gpui::TitlebarOptions { +//! title: Some(SharedString::from("Nakui — Event Log")), +//! ..Default::default() +//! }), +//! ..Default::default() +//! }, +//! |_w, cx| cx.new(Explorer::new), +//! ).expect("open window"); +//! cx.activate(true); +//! }); +//! ``` +//! +//! Ahora (1 línea): +//! +//! ```ignore +//! launch_app("Nakui — Event Log", (900., 640.), Explorer::new); +//! ``` + +use gpui::{ + App, AppContext, Application, Bounds, Context, Render, SharedString, TitlebarOptions, + WindowBounds, WindowOptions, px, +}; +use yahweh_theme::Theme; + +/// Configuración del primer (y normalmente único) ventana del app. +/// +/// `size` es `(ancho, alto)` en píxeles lógicos. La ventana queda +/// centrada en el monitor primario. +pub struct AppLaunchConfig { + pub title: SharedString, + pub size: (f32, f32), +} + +impl AppLaunchConfig { + pub fn new(title: impl Into, size: (f32, f32)) -> Self { + Self { + title: title.into(), + size, + } + } +} + +/// Levanta un app GPUI con tema instalado y root entity construido. +/// +/// El root debe implementar `Render`. La fábrica `root_factory` recibe +/// el `Context` del nuevo entity para que pueda usar `cx.spawn`, +/// suscribirse a eventos, etc — lo mismo que en el patrón directo. +/// +/// Bloquea el thread main hasta que se cierre la ventana +/// (`Application::run` no retorna). +pub fn launch_app(title: impl Into, size: (f32, f32), root_factory: F) +where + T: Render + 'static, + F: FnOnce(&mut Context) -> T + Send + 'static, +{ + launch_app_with(AppLaunchConfig::new(title, size), root_factory); +} + +/// Variante que acepta un `AppLaunchConfig` armado afuera. Útil cuando +/// el config se calcula condicionalmente (env var para tamaño, etc). +pub fn launch_app_with(config: AppLaunchConfig, root_factory: F) +where + T: Render + 'static, + F: FnOnce(&mut Context) -> T + Send + 'static, +{ + Application::new().run(move |cx: &mut App| { + Theme::install_default(cx); + let bounds = Bounds::centered(None, gpui::size(px(config.size.0), px(config.size.1)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + titlebar: Some(TitlebarOptions { + title: Some(config.title.clone()), + ..Default::default() + }), + ..Default::default() + }, + |_w, cx| cx.new(root_factory), + ) + .expect("open window"); + cx.activate(true); + }); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn config_new_normalizes_inputs() { + let c = AppLaunchConfig::new("My App", (800.0, 600.0)); + assert_eq!(c.title.as_ref(), "My App"); + assert_eq!(c.size, (800.0, 600.0)); + } + + #[test] + fn config_accepts_owned_string_title() { + let owned = String::from("Owned Title"); + let c = AppLaunchConfig::new(owned, (400.0, 300.0)); + assert_eq!(c.title.as_ref(), "Owned Title"); + } + + // No hay test de `launch_app` aquí: bloquea el thread main hasta + // que la ventana se cierre, y en sandbox no hay DISPLAY. La + // cobertura real es que cada explorer app lo invoque y arranque + // (smoke test manual o con DISPLAY). +}