chore: monorepo inicial con arje + minga + yahweh absorbidos

Workspace en 4 ejes (core/modules/apps/shared):

- core/: 24 crates de arje (Init systemd-compatible: ente-card, ente-zero,
  ente-kernel, ente-bus, ente-cas, ente-soma, ente-wasm, ente-snapshot,
  ente-brain, ente-echo, ente-policy-provider, + 12 crates *-compat)
- modules/semantic_dht/: 5 crates de minga (minga-core con AST/CAS/MST,
  minga-p2p con libp2p Kad, minga-store, minga-vfs, minga-cli)
- modules/ui_engine/: 11 crates de yahweh (libs/{core,theme,bus,providers},
  widgets/{tree,splitter,tabs,tiled,container_core,text_input})
- apps/: 5 crates de yahweh (file_explorer, database_explorer, text_viewer,
  image_viewer, yahweh-shell)
- shared_wit/protocol.wit: handshake/lifecycle inicial

Cargo.toml unificado: thiserror bumped a 2 (transparente para arje), tokio
"full", paths intra-workspace de yahweh redirigidos a su nueva ubicación.

cargo check --workspace: 0 errores, 17 warnings (dead code preexistente).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-08 04:45:44 +00:00
commit 53dbdf0f1d
176 changed files with 34845 additions and 0 deletions
@@ -0,0 +1,9 @@
[package]
name = "yahweh-bus"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
description = "AppBus + AppEvent — comunicación cross-widget app-level."
[dependencies]
gpui = { workspace = true }
@@ -0,0 +1,44 @@
//! `yahweh_bus` — `AppBus` + `AppEvent` para comunicación cross-widget.
//!
//! Es un `Entity<AppBus>` que emite [`AppEvent`]. Cualquier widget se
//! subscribe con `cx.subscribe(&bus, |this, _, ev, cx| { ... })`. La
//! Shell crea exactamente un AppBus al boot y lo distribuye:
//!
//! - **Productores** (FileExplorer, DatabaseExplorer): el LayoutHost los
//! subscribe individualmente y reenvía sus eventos tipados al bus,
//! normalizando al formato `{provider, id, …}` agnóstico.
//! - **Consumidores** (TextViewer, ImageViewer, …): reciben el handle del
//! bus en su constructor y se subscriben directo.
//!
//! Por qué un bus y no `cx.subscribe` directo entre productor y consumidor:
//! los viewers no saben qué explorers existen (ni viceversa). El bus
//! desacopla — puede haber 0, 1 o N explorers de distintos providers, y
//! varios viewers en paralelo viendo el mismo evento.
use gpui::EventEmitter;
/// Eventos cross-widget. Diseñados para ser agnósticos del dominio:
/// `provider` es el id (string) del DataProvider que sabe interpretar el
/// `id`. `provider_path` es el contexto opcional (ej. el .sqlite del
/// DatabaseExplorer) que el viewer necesita para construir su provider.
#[derive(Clone, Debug)]
pub enum AppEvent {
/// Una entidad fue seleccionada (single click). Suele triggerear un
/// preview en el viewer activo.
EntitySelected {
provider: String,
provider_path: Option<String>,
id: String,
},
/// Una entidad fue ejecutada (doble click u "Open" del menú).
EntityOpened {
provider: String,
provider_path: Option<String>,
id: String,
},
}
#[derive(Default)]
pub struct AppBus;
impl EventEmitter<AppEvent> for AppBus {}
@@ -0,0 +1,12 @@
[package]
name = "yahweh-core"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
description = "Tipos data compartidos: providers, layout JSON, taxonomía de módulos."
[dependencies]
async-trait = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
@@ -0,0 +1,266 @@
//! `yahweh_core` — tipos compartidos por toda la app, sin dependencias de UI.
//!
//! Contiene tres bloques:
//! 1. **Providers** (`DataProvider`, `EntityNode`, `DisplayType`) — fuente de
//! datos jerárquicos para los exploradores. Portado intacto de `gioser_core`.
//! 2. **Layout** (`LayerConfig`, `LayerParam`, `ModuloTipo`, `LayoutDirection`,
//! `LayoutMode`) — el JSON describe el árbol de widgets. Portado de
//! `gioser_core` quitando los helpers acoplados a Makepad (`LiveId`).
//! 3. **Identidad** (`NodeId`) — id estable de un nodo del layout, derivado
//! del `id` JSON o del path estructural.
//!
//! NO contiene tipos de comunicación entre widgets. Esos viven en la `shell`
//! y se construyen sobre el sistema de eventos de GPUI (`EventEmitter`,
//! `cx.subscribe`).
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::pin::Pin;
use tokio::io::{AsyncRead, AsyncWrite};
// =====================================================================
// Providers
// =====================================================================
#[derive(Clone, Debug)]
pub enum DisplayType {
Folder,
File,
Stream,
}
#[derive(Clone, Debug)]
pub struct EntityNode {
pub id: String,
pub name: String,
pub display_type: DisplayType,
pub mime_type: Option<String>,
}
#[async_trait]
pub trait DataProvider: Send + Sync {
fn provider_id(&self) -> String;
async fn list_children(&self, parent_id: Option<&str>) -> Result<Vec<EntityNode>, String>;
async fn get_read_stream(
&self,
entity_id: &str,
) -> Result<Pin<Box<dyn AsyncRead + Send>>, String>;
async fn get_write_stream(
&self,
entity_id: &str,
) -> Result<Pin<Box<dyn AsyncWrite + Send>>, String>;
/// Default convenience: vacía un read stream a `Vec<u8>`. Los providers
/// pueden override si tienen un fast-path.
async fn get_data(&self, entity_id: &str) -> Result<Vec<u8>, String> {
use tokio::io::AsyncReadExt;
let mut stream = self.get_read_stream(entity_id).await?;
let mut buffer = Vec::new();
stream
.read_to_end(&mut buffer)
.await
.map_err(|e| e.to_string())?;
Ok(buffer)
}
}
// =====================================================================
// Identidad estable de nodos del layout
// =====================================================================
/// Identificador estable de un nodo del árbol de layout. Construido con
/// `NodeId::from_layer(&LayerConfig, path)` durante el DFS del LayoutHost:
/// si el `LayerConfig` trae `id` propio, se usa ese; si no, se sintetiza a
/// partir del path estructural (`root/main/0`).
///
/// Internamente es una `String` para no atarse al sistema de hashing de
/// ningún framework. La igualdad lexicográfica garantiza estabilidad: el
/// mismo `id` o el mismo path producen el mismo `NodeId`.
#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeId(pub String);
impl NodeId {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn from_layer(cfg: &LayerConfig, path: &str) -> Self {
match &cfg.id {
Some(id) if !id.is_empty() => Self(id.clone()),
_ => Self(path.to_string()),
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for NodeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
// =====================================================================
// Taxonomía y layout JSON
// =====================================================================
/// Tipo de módulo que la Shell sabe instanciar. Cualquier `kind` del JSON se
/// resuelve a uno de estos via `ModuloTipo::from_kind`.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ModuloTipo {
Texto,
Arbol,
Imagen,
/// Marco contenedor — define un sub-layout y aloja hijos.
Contenedor,
/// Tile manager autónomo (Tiled / Floating / Stacked + shortcuts).
TileManager,
}
impl ModuloTipo {
pub fn from_kind(kind: &str) -> Self {
match kind {
"TextViewer" | "SectionEditor" | "Texto" => Self::Texto,
"FileExplorer" | "DatabaseExplorer" | "Arbol" => Self::Arbol,
"ImageViewer" | "Imagen" => Self::Imagen,
"TileManager" | "Tiled" => Self::TileManager,
_ => Self::Contenedor,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum LayoutDirection {
Horizontal,
Vertical,
Overlay,
}
impl Default for LayoutDirection {
fn default() -> Self {
Self::Vertical
}
}
impl LayoutDirection {
pub fn from_str(s: &str) -> Self {
match s {
"horizontal" | "Horizontal" | "row" => Self::Horizontal,
"overlay" | "Overlay" | "stack" => Self::Overlay,
_ => Self::Vertical,
}
}
}
/// Política global del root: cómo se presentan los hijos directos del
/// `LayerConfig` raíz entre sí (Tiled / Stacked / Floating). Distinta de
/// `LayoutDirection` que es por contenedor.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum LayoutMode {
Tiled,
Stacked,
Floating,
}
impl Default for LayoutMode {
fn default() -> Self {
Self::Tiled
}
}
impl LayoutMode {
pub fn from_str(s: &str) -> Self {
match s {
"stacked" | "Stacked" => Self::Stacked,
"floating" | "Floating" => Self::Floating,
_ => Self::Tiled,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LayerParam {
pub key: String,
pub value: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LayerConfig {
/// Identificador estable. Si falta, el LayoutHost sintetiza desde el path.
pub id: Option<String>,
/// Nombre de clase del módulo (e.g. "FileExplorer", "Split", "Tabs").
pub kind: String,
/// Peso flex relativo entre hermanos. `None` ⇒ 1.0.
pub flex: Option<f64>,
/// Solo válido para contenedores con orientación. `None` ⇒ Vertical.
pub direction: Option<String>,
pub params: Vec<LayerParam>,
pub children: Vec<LayerConfig>,
}
impl Default for LayerConfig {
fn default() -> Self {
Self {
id: Some("root".to_string()),
kind: "Split".to_string(),
flex: Some(1.0),
direction: Some("horizontal".to_string()),
params: vec![],
children: vec![
LayerConfig {
id: Some("nav".to_string()),
kind: "FileExplorer".to_string(),
flex: Some(0.3),
direction: None,
params: vec![],
children: vec![],
},
LayerConfig {
id: Some("main".to_string()),
kind: "TextViewer".to_string(),
flex: Some(0.7),
direction: None,
params: vec![],
children: vec![],
},
],
}
}
}
impl LayerConfig {
pub fn load_or_default(path: &str) -> Self {
std::fs::read_to_string(path)
.ok()
.and_then(|json| serde_json::from_str(&json).ok())
.unwrap_or_default()
}
pub fn serialize_json(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_default()
}
pub fn get_param(&self, key: &str) -> Option<&String> {
self.params.iter().find(|p| p.key == key).map(|p| &p.value)
}
pub fn flex_weight(&self) -> f64 {
self.flex.unwrap_or(1.0).max(0.0)
}
pub fn modulo_tipo(&self) -> ModuloTipo {
ModuloTipo::from_kind(&self.kind)
}
pub fn layout_direction(&self) -> LayoutDirection {
self.direction
.as_deref()
.map(LayoutDirection::from_str)
.unwrap_or(LayoutDirection::Vertical)
}
}
@@ -0,0 +1,12 @@
[package]
name = "yahweh-provider-fs"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
description = "DataProvider de filesystem local."
[dependencies]
yahweh-core = { workspace = true }
async-trait = { workspace = true }
tokio = { workspace = true }
notify = { workspace = true }
@@ -0,0 +1,67 @@
//! Provider de filesystem local. Crate puro: cero dependencia de UI.
//! Implementa `yahweh_core::DataProvider` listando hijos de un path con
//! `std::fs::read_dir` y leyendo archivos a `Vec<u8>` via `tokio::io`.
use async_trait::async_trait;
use std::fs;
use std::io::Cursor;
use std::path::Path;
use std::pin::Pin;
use tokio::io::{AsyncRead, AsyncWrite};
use yahweh_core::{DataProvider, DisplayType, EntityNode};
pub const PROVIDER_ID: &str = "local_fs";
pub struct FileDataProvider;
#[async_trait]
impl DataProvider for FileDataProvider {
fn provider_id(&self) -> String {
PROVIDER_ID.to_string()
}
async fn list_children(&self, parent_id: Option<&str>) -> Result<Vec<EntityNode>, String> {
let path = parent_id.unwrap_or(".");
let mut children = Vec::new();
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
let name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned();
let display_type = if path.is_dir() {
DisplayType::Folder
} else {
DisplayType::File
};
children.push(EntityNode {
id: path.to_string_lossy().into_owned(),
name,
display_type,
mime_type: None,
});
}
}
Ok(children)
}
async fn get_read_stream(
&self,
entity_id: &str,
) -> Result<Pin<Box<dyn AsyncRead + Send>>, String> {
let content = fs::read(Path::new(entity_id)).map_err(|e| e.to_string())?;
Ok(Box::pin(Cursor::new(content)))
}
async fn get_write_stream(
&self,
_entity_id: &str,
) -> Result<Pin<Box<dyn AsyncWrite + Send>>, String> {
Err("Escritura en streaming no implementada para FS".to_string())
}
}
@@ -0,0 +1,12 @@
[package]
name = "yahweh-provider-sqlite"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
description = "DataProvider de SQLite (jerarquía vía parent_id)."
[dependencies]
yahweh-core = { workspace = true }
async-trait = { workspace = true }
tokio = { workspace = true }
rusqlite = { workspace = true }
@@ -0,0 +1,118 @@
//! Provider de SQLite. Crate puro: cero dependencia de UI.
//! Tabla `items(id, parent_id, name, display_type, content)` con jerarquía
//! por `parent_id NULL` = raíz.
use async_trait::async_trait;
use rusqlite::Connection;
use std::io::Cursor;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use tokio::io::{AsyncRead, AsyncWrite};
use yahweh_core::{DataProvider, DisplayType, EntityNode};
pub const PROVIDER_ID: &str = "sqlite_db";
pub struct SqliteDataProvider {
db: Arc<Mutex<Connection>>,
}
impl SqliteDataProvider {
pub fn new(path: &str) -> Result<Self, String> {
let conn = Connection::open(path).map_err(|e| e.to_string())?;
conn.execute(
"CREATE TABLE IF NOT EXISTS items (
id TEXT PRIMARY KEY,
parent_id TEXT,
name TEXT NOT NULL,
display_type TEXT NOT NULL,
content BLOB
)",
[],
)
.map_err(|e| e.to_string())?;
// Seed mínimo si la tabla está vacía — para que el DatabaseExplorer
// tenga algo que mostrar en una primera ejecución sin pre-config.
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM items", [], |row| row.get(0))
.unwrap_or(0);
if count == 0 {
let _ = conn.execute(
"INSERT INTO items (id, parent_id, name, display_type, content) VALUES \
('readme', NULL, 'README.md', 'File', ?), \
('notes', NULL, 'notes', 'Folder', NULL), \
('todo', 'notes', 'TODO.md', 'File', ?)",
rusqlite::params![
b"# Yahweh\n\nDemo readme stored in SQLite.\n",
b"- TreeView gen\xC3\xA9rico\n- containers swappables\n- layout JSON\n",
],
);
}
Ok(Self {
db: Arc::new(Mutex::new(conn)),
})
}
}
#[async_trait]
impl DataProvider for SqliteDataProvider {
fn provider_id(&self) -> String {
PROVIDER_ID.to_string()
}
async fn list_children(&self, parent_id: Option<&str>) -> Result<Vec<EntityNode>, String> {
let db = self.db.lock().unwrap();
let mut stmt = db
.prepare("SELECT id, name, display_type FROM items WHERE parent_id IS ?")
.map_err(|e| e.to_string())?;
let rows = stmt
.query_map([parent_id], |row| {
let display_type_str: String = row.get(2)?;
let display_type = match display_type_str.as_str() {
"Folder" => DisplayType::Folder,
"Stream" => DisplayType::Stream,
_ => DisplayType::File,
};
Ok(EntityNode {
id: row.get(0)?,
name: row.get(1)?,
display_type,
mime_type: None,
})
})
.map_err(|e| e.to_string())?;
let mut children = Vec::new();
for row in rows {
children.push(row.map_err(|e| e.to_string())?);
}
Ok(children)
}
async fn get_read_stream(
&self,
entity_id: &str,
) -> Result<Pin<Box<dyn AsyncRead + Send>>, String> {
let db = self.db.lock().unwrap();
let content: Vec<u8> = db
.query_row(
"SELECT content FROM items WHERE id = ?",
[entity_id],
|row| row.get(0),
)
.map_err(|e| e.to_string())?;
Ok(Box::pin(Cursor::new(content)))
}
async fn get_write_stream(
&self,
_entity_id: &str,
) -> Result<Pin<Box<dyn AsyncWrite + Send>>, String> {
Err("Escritura en streaming no implementada para SQLite (todavía)".to_string())
}
}
@@ -0,0 +1,9 @@
[package]
name = "yahweh-theme"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
description = "Sistema de temas — paleta + gradientes, Theme como Global GPUI."
[dependencies]
gpui = { workspace = true }
@@ -0,0 +1,334 @@
//! `yahweh_theme` — paleta de colores y backgrounds compartidos.
//!
//! El `Theme` se instala como `Global` de GPUI. Los widgets lo leen vía
//! `cx.global::<Theme>()` durante su `render`, y se subscriben con
//! `cx.observe_global::<Theme>(…)` para auto-redibujarse cuando cambia.
//!
//! Filosofía: el theme es **dato puro** (sin lógica de UI). No conoce a
//! ningún widget concreto. Cada widget pide slots semánticos (panel_bg,
//! row_hover, accent…) sin acoplarse a colores hex específicos.
use gpui::{Background, Global, Hsla, hsla, linear_color_stop, linear_gradient};
/// Paleta semántica del theme. Cada slot tiene un nombre funcional, no
/// cromático — así los widgets piden "fondo de panel" sin acoplarse a
/// "azul oscuro".
///
/// Convención de slots:
/// - `bg_*` se devuelve como `Background` (soporta gradientes); los widgets
/// lo pasan a `.bg(...)` directamente.
/// - `fg_*`, `accent`, `border` son `Hsla` (colores planos para texto y
/// ornamentos).
/// - `bg_row_*` son `Hsla` porque las filas de una lista virtualizada se
/// beneficiarían poco de un gradiente individual.
#[derive(Clone, Debug)]
pub struct Theme {
pub name: &'static str,
pub is_dark: bool,
// Fondos.
pub bg_app: Background,
pub bg_panel: Background,
pub bg_panel_alt: Background,
pub bg_row_hover: Hsla,
pub bg_row_active: Hsla,
// Foregrounds.
pub fg_text: Hsla,
pub fg_muted: Hsla,
pub fg_disabled: Hsla,
// Acentos y ornamentos.
pub accent: Hsla,
pub accent_strong: Hsla,
pub border: Hsla,
pub border_strong: Hsla,
/// Marker colors para indicar "este file está abierto en container N".
/// Paleta circular — el N-ésimo container usa `marker_palette[n % len]`.
pub marker_palette: Vec<Hsla>,
}
impl Global for Theme {}
impl Theme {
pub fn global(cx: &gpui::App) -> &Self {
cx.global::<Self>()
}
/// Default — primer preset de `all()`. La Shell lo carga al boot si no
/// hay otro persistido.
pub fn install_default(cx: &mut gpui::App) {
cx.set_global(Self::nebula());
}
/// Reemplaza el theme global. GPUI notifica a todos los `observe_global`
/// suscriptores en el siguiente frame.
pub fn set(cx: &mut gpui::App, theme: Self) {
cx.set_global(theme);
}
/// Lista todos los presets en orden estable. Usado por el switcher para
/// ciclar y por el menú de "Tema" cuando lo agreguemos.
pub fn all() -> Vec<Self> {
vec![
Self::nebula(),
Self::aurora(),
Self::sunset(),
Self::flat_dark(),
Self::solarized_light(),
Self::high_contrast(),
]
}
/// Devuelve el preset cuyo `name` matchea (case-insensitive). `None` si
/// el nombre no existe — útil para validar input de usuario al cargar
/// preferencias persistidas.
pub fn by_name(name: &str) -> Option<Self> {
Self::all()
.into_iter()
.find(|t| t.name.eq_ignore_ascii_case(name))
}
/// Próximo preset en la rotación de `all()`. Si `current` no está, se
/// vuelve al primero. La rotación es circular (último → primero).
pub fn next_after(current: &str) -> Self {
let all = Self::all();
let idx = all.iter().position(|t| t.name == current);
match idx {
Some(i) => all[(i + 1) % all.len()].clone(),
None => all[0].clone(),
}
}
// =====================================================================
// Presets
// =====================================================================
/// **Nebula** — default. Gradiente vertical violáceo profundo → teal
/// medianoche. Pensado para sentirse moderno y descansado de noche.
pub fn nebula() -> Self {
let bg_app = linear_gradient(
165.0,
linear_color_stop(hsla(265.0 / 360.0, 0.38, 0.07, 1.0), 0.0),
linear_color_stop(hsla(195.0 / 360.0, 0.42, 0.09, 1.0), 1.0),
);
let bg_panel = linear_gradient(
165.0,
linear_color_stop(hsla(245.0 / 360.0, 0.28, 0.10, 1.0), 0.0),
linear_color_stop(hsla(210.0 / 360.0, 0.30, 0.12, 1.0), 1.0),
);
let bg_panel_alt = linear_gradient(
165.0,
linear_color_stop(hsla(255.0 / 360.0, 0.25, 0.13, 1.0), 0.0),
linear_color_stop(hsla(220.0 / 360.0, 0.27, 0.14, 1.0), 1.0),
);
Self {
name: "Nebula",
is_dark: true,
bg_app,
bg_panel,
bg_panel_alt,
bg_row_hover: hsla(220.0 / 360.0, 0.30, 0.20, 0.45),
bg_row_active: hsla(280.0 / 360.0, 0.55, 0.28, 0.65),
fg_text: hsla(210.0 / 360.0, 0.35, 0.88, 1.0),
fg_muted: hsla(215.0 / 360.0, 0.22, 0.58, 1.0),
fg_disabled: hsla(215.0 / 360.0, 0.10, 0.40, 1.0),
accent: hsla(280.0 / 360.0, 0.65, 0.65, 1.0),
accent_strong: hsla(285.0 / 360.0, 0.78, 0.74, 1.0),
border: hsla(225.0 / 360.0, 0.20, 0.22, 1.0),
border_strong: hsla(280.0 / 360.0, 0.40, 0.45, 1.0),
marker_palette: vec![
hsla(280.0 / 360.0, 0.65, 0.55, 0.45),
hsla(195.0 / 360.0, 0.65, 0.50, 0.45),
hsla(35.0 / 360.0, 0.75, 0.55, 0.45),
hsla(135.0 / 360.0, 0.55, 0.50, 0.45),
hsla(0.0, 0.60, 0.55, 0.45),
],
}
}
/// **Aurora** — verdes-cian-azul, evoca aurora boreal. Más frío que
/// Nebula, contraste alto.
pub fn aurora() -> Self {
let bg_app = linear_gradient(
190.0,
linear_color_stop(hsla(170.0 / 360.0, 0.45, 0.06, 1.0), 0.0),
linear_color_stop(hsla(220.0 / 360.0, 0.50, 0.09, 1.0), 1.0),
);
let bg_panel = linear_gradient(
190.0,
linear_color_stop(hsla(165.0 / 360.0, 0.32, 0.10, 1.0), 0.0),
linear_color_stop(hsla(215.0 / 360.0, 0.36, 0.12, 1.0), 1.0),
);
let bg_panel_alt = linear_gradient(
190.0,
linear_color_stop(hsla(170.0 / 360.0, 0.30, 0.13, 1.0), 0.0),
linear_color_stop(hsla(220.0 / 360.0, 0.32, 0.15, 1.0), 1.0),
);
Self {
name: "Aurora",
is_dark: true,
bg_app,
bg_panel,
bg_panel_alt,
bg_row_hover: hsla(180.0 / 360.0, 0.40, 0.22, 0.50),
bg_row_active: hsla(160.0 / 360.0, 0.55, 0.30, 0.65),
fg_text: hsla(180.0 / 360.0, 0.20, 0.92, 1.0),
fg_muted: hsla(185.0 / 360.0, 0.18, 0.62, 1.0),
fg_disabled: hsla(185.0 / 360.0, 0.10, 0.40, 1.0),
accent: hsla(150.0 / 360.0, 0.70, 0.55, 1.0),
accent_strong: hsla(160.0 / 360.0, 0.85, 0.65, 1.0),
border: hsla(195.0 / 360.0, 0.25, 0.20, 1.0),
border_strong: hsla(160.0 / 360.0, 0.55, 0.45, 1.0),
marker_palette: vec![
hsla(150.0 / 360.0, 0.75, 0.50, 0.45),
hsla(195.0 / 360.0, 0.70, 0.50, 0.45),
hsla(225.0 / 360.0, 0.70, 0.55, 0.45),
hsla(85.0 / 360.0, 0.65, 0.50, 0.45),
hsla(330.0 / 360.0, 0.65, 0.55, 0.45),
],
}
}
/// **Sunset** — naranjas-rosas-violetas profundos. Cálido, alto contraste
/// con texto claro.
pub fn sunset() -> Self {
let bg_app = linear_gradient(
170.0,
linear_color_stop(hsla(20.0 / 360.0, 0.50, 0.08, 1.0), 0.0),
linear_color_stop(hsla(310.0 / 360.0, 0.45, 0.10, 1.0), 1.0),
);
let bg_panel = linear_gradient(
170.0,
linear_color_stop(hsla(15.0 / 360.0, 0.32, 0.12, 1.0), 0.0),
linear_color_stop(hsla(315.0 / 360.0, 0.30, 0.13, 1.0), 1.0),
);
let bg_panel_alt = linear_gradient(
170.0,
linear_color_stop(hsla(20.0 / 360.0, 0.30, 0.15, 1.0), 0.0),
linear_color_stop(hsla(320.0 / 360.0, 0.28, 0.16, 1.0), 1.0),
);
Self {
name: "Sunset",
is_dark: true,
bg_app,
bg_panel,
bg_panel_alt,
bg_row_hover: hsla(25.0 / 360.0, 0.40, 0.25, 0.45),
bg_row_active: hsla(5.0 / 360.0, 0.55, 0.32, 0.65),
fg_text: hsla(30.0 / 360.0, 0.30, 0.92, 1.0),
fg_muted: hsla(25.0 / 360.0, 0.20, 0.62, 1.0),
fg_disabled: hsla(25.0 / 360.0, 0.10, 0.42, 1.0),
accent: hsla(15.0 / 360.0, 0.78, 0.62, 1.0),
accent_strong: hsla(355.0 / 360.0, 0.85, 0.68, 1.0),
border: hsla(15.0 / 360.0, 0.25, 0.25, 1.0),
border_strong: hsla(355.0 / 360.0, 0.55, 0.45, 1.0),
marker_palette: vec![
hsla(15.0 / 360.0, 0.80, 0.55, 0.45),
hsla(310.0 / 360.0, 0.65, 0.55, 0.45),
hsla(45.0 / 360.0, 0.80, 0.55, 0.45),
hsla(285.0 / 360.0, 0.65, 0.60, 0.45),
hsla(355.0 / 360.0, 0.70, 0.55, 0.45),
],
}
}
/// **Flat Dark** — sin gradientes, paleta cool gris-azulado. Para quien
/// prefiere monocromía. Útil para contrastar visualmente con los temas
/// de gradiente.
pub fn flat_dark() -> Self {
let bg_app: Background = hsla(220.0 / 360.0, 0.15, 0.09, 1.0).into();
let bg_panel: Background = hsla(220.0 / 360.0, 0.15, 0.12, 1.0).into();
let bg_panel_alt: Background = hsla(220.0 / 360.0, 0.15, 0.14, 1.0).into();
Self {
name: "Flat Dark",
is_dark: true,
bg_app,
bg_panel,
bg_panel_alt,
bg_row_hover: hsla(220.0 / 360.0, 0.20, 0.20, 1.0),
bg_row_active: hsla(220.0 / 360.0, 0.40, 0.30, 1.0),
fg_text: hsla(210.0 / 360.0, 0.20, 0.85, 1.0),
fg_muted: hsla(215.0 / 360.0, 0.15, 0.55, 1.0),
fg_disabled: hsla(215.0 / 360.0, 0.10, 0.40, 1.0),
accent: hsla(210.0 / 360.0, 0.70, 0.55, 1.0),
accent_strong: hsla(210.0 / 360.0, 0.85, 0.65, 1.0),
border: hsla(220.0 / 360.0, 0.15, 0.20, 1.0),
border_strong: hsla(220.0 / 360.0, 0.30, 0.35, 1.0),
marker_palette: vec![
hsla(210.0 / 360.0, 0.65, 0.55, 0.40),
hsla(160.0 / 360.0, 0.55, 0.50, 0.40),
hsla(30.0 / 360.0, 0.75, 0.55, 0.40),
hsla(0.0, 0.55, 0.55, 0.40),
],
}
}
/// **Solarized Light** — preset claro inspirado en la paleta clásica de
/// Schoonover. Sin gradientes (en light un gradiente sutil pasa
/// desapercibido y solo introduce ruido).
pub fn solarized_light() -> Self {
let bg_app: Background = hsla(44.0 / 360.0, 0.87, 0.94, 1.0).into();
let bg_panel: Background = hsla(46.0 / 360.0, 0.42, 0.88, 1.0).into();
let bg_panel_alt: Background = hsla(46.0 / 360.0, 0.42, 0.92, 1.0).into();
Self {
name: "Solarized Light",
is_dark: false,
bg_app,
bg_panel,
bg_panel_alt,
bg_row_hover: hsla(46.0 / 360.0, 0.45, 0.80, 0.65),
bg_row_active: hsla(45.0 / 360.0, 0.55, 0.72, 0.85),
fg_text: hsla(196.0 / 360.0, 0.13, 0.30, 1.0),
fg_muted: hsla(196.0 / 360.0, 0.13, 0.45, 1.0),
fg_disabled: hsla(196.0 / 360.0, 0.10, 0.62, 1.0),
accent: hsla(205.0 / 360.0, 0.69, 0.42, 1.0),
accent_strong: hsla(205.0 / 360.0, 0.82, 0.38, 1.0),
border: hsla(46.0 / 360.0, 0.30, 0.78, 1.0),
border_strong: hsla(205.0 / 360.0, 0.40, 0.55, 1.0),
marker_palette: vec![
hsla(205.0 / 360.0, 0.69, 0.42, 0.30),
hsla(175.0 / 360.0, 0.74, 0.32, 0.30),
hsla(45.0 / 360.0, 1.00, 0.36, 0.30),
hsla(331.0 / 360.0, 0.74, 0.42, 0.30),
hsla(18.0 / 360.0, 0.89, 0.40, 0.30),
],
}
}
/// **High Contrast** — accesibilidad. Negro puro con texto blanco y
/// ornamentos amarillo/verde fuertes. Suficientemente diferente para
/// notar inmediatamente al usar el switcher.
pub fn high_contrast() -> Self {
let bg_app: Background = hsla(0.0, 0.0, 0.0, 1.0).into();
let bg_panel: Background = hsla(0.0, 0.0, 0.05, 1.0).into();
let bg_panel_alt: Background = hsla(0.0, 0.0, 0.10, 1.0).into();
Self {
name: "High Contrast",
is_dark: true,
bg_app,
bg_panel,
bg_panel_alt,
bg_row_hover: hsla(60.0 / 360.0, 1.00, 0.50, 0.35),
bg_row_active: hsla(120.0 / 360.0, 1.00, 0.40, 0.55),
fg_text: hsla(0.0, 0.0, 1.0, 1.0),
fg_muted: hsla(0.0, 0.0, 0.75, 1.0),
fg_disabled: hsla(0.0, 0.0, 0.50, 1.0),
accent: hsla(60.0 / 360.0, 1.00, 0.60, 1.0),
accent_strong: hsla(60.0 / 360.0, 1.00, 0.75, 1.0),
border: hsla(0.0, 0.0, 0.30, 1.0),
border_strong: hsla(60.0 / 360.0, 1.00, 0.60, 1.0),
marker_palette: vec![
hsla(60.0 / 360.0, 1.00, 0.55, 0.50),
hsla(120.0 / 360.0, 1.00, 0.50, 0.50),
hsla(180.0 / 360.0, 1.00, 0.55, 0.50),
hsla(0.0, 1.00, 0.60, 0.50),
hsla(300.0 / 360.0, 1.00, 0.65, 0.50),
],
}
}
}