feat(tahuantinsuyu): scaffolding del estudio astrológico (10 crates + ventana 3-panes)
Módulo nuevo `modules/tahuantinsuyu/` con 9 crates reusables + app `apps/tahuantinsuyu` ejecutable que abre la ventana del explorador y coordina los widgets: - tahuantinsuyu-card: Card Brahman + spawn_sidecar (flows chart-request/chart-result). - tahuantinsuyu-model: tipos agnósticos (Group/Contact/Chart, StoredBirthData, StoredChartConfig, ChartKind, TreeSelection). - tahuantinsuyu-store: persistencia SQLite (rusqlite) con migración v1, CRUD por entidad y descenso recursivo `charts_under_group`. - tahuantinsuyu-engine: bridge agnóstico al canvas vía `RenderModel` (Layer/Glyph/Geometry). Feature `eternal-bridge` (off por default) reservada para enchufar eternal-astrology desde ~/eternal. - tahuantinsuyu-modules: registry de módulos pluggables (Module trait + Control schema) con `NatalModule` placeholder. - tahuantinsuyu-theme: AstroPalette (elementos / modos / planetas / aspectos) con variantes dark + light sobre yahweh-theme. - tahuantinsuyu-canvas: widget GPUI con CanvasState (Empty / Wheel / Thumbnails). Render placeholder hasta cablear la rueda real. - tahuantinsuyu-tree: explorador izquierdo sobre yahweh-widget-tree, prefijos g:/c:/h: para Group/Contact/Chart. - tahuantinsuyu-panel: control panel inferior que lee Controls de los módulos del registry y los pinta. - apps/tahuantinsuyu: binario `tahuantinsuyu` (launch_app-style) con Shell coordinador (tree↔canvas↔panel), DB en $XDG_DATA_HOME. Workspace Cargo.toml actualizado con los 10 miembros. `cargo check` verde, tests unitarios verdes (model/store/engine/modules/theme/card). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
//! Tahuantinsuyu — binario standalone.
|
||||
//!
|
||||
//! Boot:
|
||||
//! 1. `tahuantinsuyu_card::spawn_sidecar()` se presenta al Init brahman
|
||||
//! (fire-and-forget; si no hay Init, la app sigue standalone).
|
||||
//! 2. Abre la DB SQLite en `$XDG_DATA_HOME/tahuantinsuyu/charts.db`
|
||||
//! (fallback a `~/.local/share/tahuantinsuyu/charts.db`).
|
||||
//! 3. Levanta GPUI con [`yahweh_theme::Theme::install_default`].
|
||||
//! 4. Compone el shell: [`Shell`] dueño del tree (izq), canvas (centro)
|
||||
//! y panel (abajo). Cablea las suscripciones cross-widget.
|
||||
//!
|
||||
//! ## Layout
|
||||
//!
|
||||
//! ```text
|
||||
//! ┌───────────┬────────────────────────────────────────┐
|
||||
//! │ │ │
|
||||
//! │ tree │ canvas │
|
||||
//! │ (groups, │ (rueda / thumbnails) │
|
||||
//! │ contacts,│ │
|
||||
//! │ charts) │ │
|
||||
//! │ │ │
|
||||
//! ├───────────┴────────────────────────────────────────┤
|
||||
//! │ control panel (módulos) │
|
||||
//! └─────────────────────────────────────────────────────┘
|
||||
//! ```
|
||||
|
||||
mod shell;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use gpui::{
|
||||
App, AppContext, Application, Bounds, SharedString, TitlebarOptions, WindowBounds,
|
||||
WindowOptions, px, size,
|
||||
};
|
||||
|
||||
use tahuantinsuyu_store::Store;
|
||||
use yahweh_theme::Theme;
|
||||
|
||||
use crate::shell::Shell;
|
||||
|
||||
const DB_FILENAME: &str = "charts.db";
|
||||
const APP_TITLE: &str = "Tahuantinsuyu";
|
||||
|
||||
fn main() {
|
||||
// Sidecar brahman primero — si el Init está corriendo, nos presentamos.
|
||||
tahuantinsuyu_card::spawn_sidecar();
|
||||
|
||||
// DB en directorio de datos del usuario.
|
||||
let db_path = resolve_db_path();
|
||||
let store = match Store::open(&db_path) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"[tahuantinsuyu] no se pudo abrir la DB en {:?}: {} — usando memoria",
|
||||
db_path, e
|
||||
);
|
||||
Store::in_memory().expect("in-memory store")
|
||||
}
|
||||
};
|
||||
|
||||
Application::new().run(move |cx: &mut App| {
|
||||
Theme::install_default(cx);
|
||||
|
||||
let bounds = Bounds::centered(None, size(px(1400.0), px(900.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: Some(SharedString::from(APP_TITLE)),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
move |_w, cx| cx.new(|cx| Shell::new(store.clone(), cx)),
|
||||
)
|
||||
.expect("open window");
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
fn resolve_db_path() -> PathBuf {
|
||||
if let Some(dirs) = directories::ProjectDirs::from("net", "gioser", "tahuantinsuyu") {
|
||||
let dir = dirs.data_dir().to_path_buf();
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
return dir.join(DB_FILENAME);
|
||||
}
|
||||
PathBuf::from(DB_FILENAME)
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
//! Shell — coordinador de los tres widgets.
|
||||
//!
|
||||
//! Es el "director de orquesta": dueño del tree, del canvas y del panel,
|
||||
//! reenvía eventos entre ellos y aplica las mutaciones en la store.
|
||||
//!
|
||||
//! Flujo típico:
|
||||
//!
|
||||
//! ```text
|
||||
//! Tree.Selected(Chart) → Shell → Canvas.set_mode(Wheel)
|
||||
//! → Panel.set_active_kind(chart.kind)
|
||||
//!
|
||||
//! Tree.Selected(Group) → Shell → Canvas.set_mode(Thumbnails{…})
|
||||
//! → Panel.set_active_kind(None)
|
||||
//!
|
||||
//! Panel.ModuleToggled → Shell → Store.upsert_module_state
|
||||
//! → Canvas.toggle_module
|
||||
//! ```
|
||||
//!
|
||||
//! Fase 1: las suscripciones están cableadas pero los handlers son
|
||||
//! mínimos (logging + transición de modo). La pipeline real de cómputo
|
||||
//! viene con la fase 3.
|
||||
|
||||
use gpui::{
|
||||
Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, Window, div,
|
||||
prelude::*, px,
|
||||
};
|
||||
|
||||
use tahuantinsuyu_canvas::{
|
||||
AstrologyCanvas, CanvasMode, ThumbnailItem, ThumbnailScope,
|
||||
};
|
||||
use tahuantinsuyu_engine::compute_mock;
|
||||
use tahuantinsuyu_model::TreeSelection;
|
||||
use tahuantinsuyu_panel::{ControlPanel, PanelEvent};
|
||||
use tahuantinsuyu_store::Store;
|
||||
use tahuantinsuyu_tree::{TahuantinsuyuTree, TreeEvent};
|
||||
use yahweh_bus::AppBus;
|
||||
use yahweh_theme::Theme;
|
||||
|
||||
const TREE_WIDTH: f32 = 280.0;
|
||||
const PANEL_HEIGHT: f32 = 180.0;
|
||||
|
||||
pub struct Shell {
|
||||
store: Store,
|
||||
#[allow(dead_code)]
|
||||
bus: Entity<AppBus>,
|
||||
tree: Entity<TahuantinsuyuTree>,
|
||||
canvas: Entity<AstrologyCanvas>,
|
||||
panel: Entity<ControlPanel>,
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
pub fn new(store: Store, cx: &mut Context<Self>) -> Self {
|
||||
cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
|
||||
|
||||
let bus = cx.new(|_| AppBus);
|
||||
let tree = cx.new(|cx| TahuantinsuyuTree::new(store.clone(), cx));
|
||||
let canvas = cx.new(AstrologyCanvas::new);
|
||||
let panel = cx.new(ControlPanel::new);
|
||||
|
||||
// Tree → Shell: aplicar selección al canvas/panel.
|
||||
cx.subscribe(&tree, |this: &mut Self, _, ev: &TreeEvent, cx| {
|
||||
this.on_tree_event(ev, cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Panel → Shell: persistir y propagar al canvas.
|
||||
cx.subscribe(&panel, |this: &mut Self, _, ev: &PanelEvent, cx| {
|
||||
this.on_panel_event(ev, cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
store,
|
||||
bus,
|
||||
tree,
|
||||
canvas,
|
||||
panel,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tree_event(&mut self, ev: &TreeEvent, cx: &mut Context<Self>) {
|
||||
let selection = match ev {
|
||||
TreeEvent::Selected(s) => s,
|
||||
TreeEvent::Opened(s) => s,
|
||||
};
|
||||
self.apply_selection(selection.clone(), cx);
|
||||
}
|
||||
|
||||
fn apply_selection(&mut self, sel: TreeSelection, cx: &mut Context<Self>) {
|
||||
match sel {
|
||||
TreeSelection::Chart(id) => {
|
||||
let chart = match self.store.get_chart(id) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("[shell] get_chart {}: {}", id, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let kind = chart.kind;
|
||||
let render = compute_mock(&chart);
|
||||
self.canvas.update(cx, |c, cx| {
|
||||
c.set_mode(
|
||||
CanvasMode::Wheel {
|
||||
render: Box::new(render),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
self.panel
|
||||
.update(cx, |p, cx| p.set_active_kind(Some(kind), cx));
|
||||
}
|
||||
TreeSelection::Contact(id) => {
|
||||
let charts = self.store.list_charts(id).unwrap_or_default();
|
||||
let items: Vec<ThumbnailItem> = charts
|
||||
.into_iter()
|
||||
.map(|c| ThumbnailItem {
|
||||
chart_id: c.id,
|
||||
label: SharedString::from(c.label),
|
||||
subtitle: Some(SharedString::from(format!("{:?}", c.kind))),
|
||||
preview: None,
|
||||
})
|
||||
.collect();
|
||||
self.canvas.update(cx, |c, cx| {
|
||||
c.set_mode(
|
||||
CanvasMode::Thumbnails {
|
||||
scope: ThumbnailScope::Contact(id),
|
||||
items,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
self.panel.update(cx, |p, cx| p.set_active_kind(None, cx));
|
||||
}
|
||||
TreeSelection::Group(id) => {
|
||||
let charts = self.store.charts_under_group(id).unwrap_or_default();
|
||||
let items: Vec<ThumbnailItem> = charts
|
||||
.into_iter()
|
||||
.map(|c| ThumbnailItem {
|
||||
chart_id: c.id,
|
||||
label: SharedString::from(c.label),
|
||||
subtitle: Some(SharedString::from(format!("{:?}", c.kind))),
|
||||
preview: None,
|
||||
})
|
||||
.collect();
|
||||
self.canvas.update(cx, |c, cx| {
|
||||
c.set_mode(
|
||||
CanvasMode::Thumbnails {
|
||||
scope: ThumbnailScope::Group(id),
|
||||
items,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
self.panel.update(cx, |p, cx| p.set_active_kind(None, cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_panel_event(&mut self, ev: &PanelEvent, cx: &mut Context<Self>) {
|
||||
match ev {
|
||||
PanelEvent::ModuleToggled { module_id, .. } => {
|
||||
self.canvas
|
||||
.update(cx, |c, cx| c.toggle_module(module_id, cx));
|
||||
}
|
||||
PanelEvent::ControlChanged { .. } => {
|
||||
// Fase 4: aplicar config al canvas + persistir en store.
|
||||
}
|
||||
}
|
||||
// Silenciar warnings de campos no leídos hasta que la fase 2
|
||||
// cablee CRUD desde el tree.
|
||||
let _ = (&self.store, &self.tree, &self.bus);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Shell {
|
||||
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let theme = Theme::global(cx).clone();
|
||||
|
||||
let header = div()
|
||||
.h(px(34.0))
|
||||
.px(px(12.0))
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.gap(px(10.0))
|
||||
.border_b_1()
|
||||
.border_color(theme.border)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(13.0))
|
||||
.text_color(theme.fg_text)
|
||||
.child("☉ Tahuantinsuyu"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(10.0))
|
||||
.text_color(theme.fg_muted)
|
||||
.child("estudio de astrología profesional"),
|
||||
);
|
||||
|
||||
let tree_panel = div()
|
||||
.w(px(TREE_WIDTH))
|
||||
.min_w(px(TREE_WIDTH))
|
||||
.h_full()
|
||||
.border_r_1()
|
||||
.border_color(theme.border)
|
||||
.child(self.tree.clone());
|
||||
|
||||
let canvas_panel = div()
|
||||
.flex_grow()
|
||||
.h_full()
|
||||
.child(self.canvas.clone());
|
||||
|
||||
let main_row = div()
|
||||
.flex_grow()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.child(tree_panel)
|
||||
.child(canvas_panel);
|
||||
|
||||
let bottom_panel = div()
|
||||
.h(px(PANEL_HEIGHT))
|
||||
.min_h(px(PANEL_HEIGHT))
|
||||
.w_full()
|
||||
.border_t_1()
|
||||
.border_color(theme.border)
|
||||
.child(self.panel.clone());
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.bg(theme.bg_app.clone())
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(header)
|
||||
.child(main_row)
|
||||
.child(bottom_panel)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user