tawasuyu

tawasuyu/pata

Declarative desktop frame (bars/dock/tray/widgets), portable Linux/Wawa, on Llimphi.

⎇ 95bc028eeab35a55ae8bef41f820de73348d5baa 02_ruway/pata/pata-core/src/wire.rs Rust · 320 lines · 9.5 KB History Who touched what raw

feat: pata standalone — marco de escritorio declarativo (barras/dock/tray/widgets) portable Linux/Wawa (front-door, git-dep al monorepo) 95bc028ee · Sergio ·

1//! Representación **postcard-safe** del marco para akasha (wawa).
2//!
3//! El modelo de [`Config`](crate::Config) está afinado para el loader TOML de
4//! Linux: `WidgetSpec.props` usa `#[serde(flatten)]` (para que las props vivan al
5//! nivel del `kind`) y [`Prop`] es `#[serde(untagged)]` (para que TOML infiera el
6//! tipo). Ambos atributos **rompen postcard** —el codec de akasha—, que no es
7//! auto-descriptivo y no soporta `deserialize_any`.
8//!
9//! Este módulo define un espejo *plano y etiquetado* del modelo: [`WireConfig`]
10//! es exactamente la misma información, pero serializable con cualquier formato
11//! binario no auto-descriptivo. El kernel de wawa serializa el `WireConfig` con
12//! postcard, lo guarda en el grafo direccionado por contenido y lo lee de vuelta
13//! —el config del marco viaja por akasha como cualquier otro objeto—. En Linux
14//! el camino TOML sigue usando `Config` directo; este espejo es sólo para el
15//! cruce a wawa.
16//!
17//! Las conversiones son **sin pérdida**: `Config → WireConfig → Config` devuelve
18//! el mismo modelo (lo fija un test).
19
20use alloc::string::String;
21use alloc::vec::Vec;
22
23#[cfg(feature = "serde")]
24use serde::{Deserialize, Serialize};
25
26use crate::config::{
27    Anchor, Config, FloatingCard, General, Prop, SidebarTab, Surface, SurfaceKind, WidgetSpec,
28};
29
30/// Valor de propiedad **etiquetado** (a diferencia de [`Prop`], que es
31/// `untagged`): así postcard puede deserializarlo sin auto-descripción.
32#[derive(Debug, Clone, PartialEq)]
33#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
34pub enum WireProp {
35    Bool(bool),
36    Int(i64),
37    Num(f64),
38    Str(String),
39}
40
41impl From<&Prop> for WireProp {
42    fn from(p: &Prop) -> Self {
43        match p {
44            Prop::Bool(b) => WireProp::Bool(*b),
45            Prop::Int(i) => WireProp::Int(*i),
46            Prop::Num(n) => WireProp::Num(*n),
47            Prop::Str(s) => WireProp::Str(s.clone()),
48        }
49    }
50}
51
52impl From<WireProp> for Prop {
53    fn from(p: WireProp) -> Self {
54        match p {
55            WireProp::Bool(b) => Prop::Bool(b),
56            WireProp::Int(i) => Prop::Int(i),
57            WireProp::Num(n) => Prop::Num(n),
58            WireProp::Str(s) => Prop::Str(s),
59        }
60    }
61}
62
63/// Espejo de [`WidgetSpec`] con las props como **lista ordenada** (no `flatten`).
64#[derive(Debug, Clone, PartialEq)]
65#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
66pub struct WireWidget {
67    pub kind: String,
68    pub props: Vec<(String, WireProp)>,
69}
70
71impl From<&WidgetSpec> for WireWidget {
72    fn from(w: &WidgetSpec) -> Self {
73        Self {
74            kind: w.kind.clone(),
75            props: w.props.iter().map(|(k, v)| (k.clone(), WireProp::from(v))).collect(),
76        }
77    }
78}
79
80impl From<WireWidget> for WidgetSpec {
81    fn from(w: WireWidget) -> Self {
82        let mut spec = WidgetSpec::new(w.kind);
83        for (k, v) in w.props {
84            spec = spec.with(k, Prop::from(v));
85        }
86        spec
87    }
88}
89
90fn a_wire(specs: &[WidgetSpec]) -> Vec<WireWidget> {
91    specs.iter().map(WireWidget::from).collect()
92}
93
94fn de_wire(wires: Vec<WireWidget>) -> Vec<WidgetSpec> {
95    wires.into_iter().map(WidgetSpec::from).collect()
96}
97
98/// Espejo de [`FloatingCard`].
99#[derive(Debug, Clone, PartialEq)]
100#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
101pub struct WireCard {
102    pub x: f32,
103    pub y: f32,
104    pub w: f32,
105    pub h: f32,
106    pub title: Option<String>,
107    pub widgets: Vec<WireWidget>,
108}
109
110impl From<&FloatingCard> for WireCard {
111    fn from(c: &FloatingCard) -> Self {
112        Self {
113            x: c.x,
114            y: c.y,
115            w: c.w,
116            h: c.h,
117            title: c.title.clone(),
118            widgets: a_wire(&c.widgets),
119        }
120    }
121}
122
123impl From<WireCard> for FloatingCard {
124    fn from(c: WireCard) -> Self {
125        FloatingCard {
126            x: c.x,
127            y: c.y,
128            w: c.w,
129            h: c.h,
130            title: c.title,
131            widgets: de_wire(c.widgets),
132        }
133    }
134}
135
136/// Espejo de [`SidebarTab`].
137#[derive(Debug, Clone, PartialEq)]
138#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
139pub struct WireTab {
140    pub icon: String,
141    pub label: String,
142    pub content: WireWidget,
143}
144
145impl From<&SidebarTab> for WireTab {
146    fn from(t: &SidebarTab) -> Self {
147        Self {
148            icon: t.icon.clone(),
149            label: t.label.clone(),
150            content: WireWidget::from(&t.content),
151        }
152    }
153}
154
155impl From<WireTab> for SidebarTab {
156    fn from(t: WireTab) -> Self {
157        SidebarTab {
158            icon: t.icon,
159            label: t.label,
160            content: WidgetSpec::from(t.content),
161        }
162    }
163}
164
165/// Espejo de [`Surface`].
166#[derive(Debug, Clone, PartialEq)]
167#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
168pub struct WireSurface {
169    pub kind: SurfaceKind,
170    pub anchor: Anchor,
171    pub thickness: f32,
172    pub autohide: bool,
173    pub padding: f32,
174    pub gap: f32,
175    pub start: Vec<WireWidget>,
176    pub center: Vec<WireWidget>,
177    pub end: Vec<WireWidget>,
178    pub cards: Vec<WireCard>,
179    pub output: String,
180    pub tabs: Vec<WireTab>,
181    pub panel_width: f32,
182}
183
184impl From<&Surface> for WireSurface {
185    fn from(s: &Surface) -> Self {
186        Self {
187            kind: s.kind,
188            anchor: s.anchor,
189            thickness: s.thickness,
190            autohide: s.autohide,
191            padding: s.padding,
192            gap: s.gap,
193            start: a_wire(&s.start),
194            center: a_wire(&s.center),
195            end: a_wire(&s.end),
196            cards: s.cards.iter().map(WireCard::from).collect(),
197            output: s.output.clone(),
198            tabs: s.tabs.iter().map(WireTab::from).collect(),
199            panel_width: s.panel_width,
200        }
201    }
202}
203
204impl From<WireSurface> for Surface {
205    fn from(s: WireSurface) -> Self {
206        Surface {
207            kind: s.kind,
208            anchor: s.anchor,
209            thickness: s.thickness,
210            autohide: s.autohide,
211            padding: s.padding,
212            gap: s.gap,
213            start: de_wire(s.start),
214            center: de_wire(s.center),
215            end: de_wire(s.end),
216            cards: s.cards.into_iter().map(FloatingCard::from).collect(),
217            output: s.output,
218            tabs: s.tabs.into_iter().map(SidebarTab::from).collect(),
219            panel_width: s.panel_width,
220        }
221    }
222}
223
224/// Espejo postcard-safe de [`Config`]: la misma información, sin `flatten` ni
225/// `untagged`. Es lo que viaja por akasha en wawa.
226#[derive(Debug, Clone, PartialEq)]
227#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
228pub struct WireConfig {
229    pub general: General,
230    pub surfaces: Vec<WireSurface>,
231}
232
233impl From<&Config> for WireConfig {
234    fn from(c: &Config) -> Self {
235        Self {
236            general: c.general.clone(),
237            surfaces: c.surfaces.iter().map(WireSurface::from).collect(),
238        }
239    }
240}
241
242impl From<WireConfig> for Config {
243    fn from(c: WireConfig) -> Self {
244        Config {
245            general: c.general,
246            surfaces: c.surfaces.into_iter().map(Surface::from).collect(),
247        }
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use crate::config::{Prop, SidebarTab};
255    use crate::{Config, Surface, WidgetSpec};
256    use alloc::string::ToString;
257
258    /// Un config con todos los rincones poblados (props de cada tipo, una
259    /// tarjeta flotante) para que el round-trip cubra el caso difícil.
260    fn config_rico() -> Config {
261        let mut top = Surface::bar(Anchor::Top);
262        top.thickness = 32.0;
263        top.start = alloc::vec![WidgetSpec::new("start_button").with("label", Prop::Str("⊞".to_string()))];
264        top.center = alloc::vec![WidgetSpec::new("clock")
265            .with("format", Prop::Str("%H:%M".to_string()))
266            .with("size", Prop::Int(14))
267            .with("ratio", Prop::Num(0.5))
268            .with("flag", Prop::Bool(true))];
269        top.end = alloc::vec![WidgetSpec::new("ram_meter")];
270
271        let mut panel = Surface::default();
272        panel.kind = SurfaceKind::Panel;
273        panel.cards = alloc::vec![FloatingCard {
274            x: 40.0,
275            y: 80.0,
276            w: 220.0,
277            h: 110.0,
278            title: Some("sistema".to_string()),
279            widgets: alloc::vec![WidgetSpec::new("cpu_meter"), WidgetSpec::new("ram_meter")],
280        }];
281
282        let mut sidebar = Surface::sidebar(Anchor::Left);
283        sidebar.panel_width = 300.0;
284        sidebar.tabs = alloc::vec![
285            SidebarTab::new(
286                "monads",
287                "Mónadas",
288                WidgetSpec::new("navigator").with("source", Prop::Str("nouser".to_string())),
289            ),
290            SidebarTab::new("files", "Archivos", WidgetSpec::new("navigator")),
291        ];
292
293        let mut cfg = Config::default();
294        cfg.surfaces.push(top);
295        cfg.surfaces.push(panel);
296        cfg.surfaces.push(sidebar);
297        cfg
298    }
299
300    #[test]
301    fn round_trip_sin_perdida() {
302        let cfg = config_rico();
303        let wire = WireConfig::from(&cfg);
304        let vuelta: Config = wire.into();
305        assert_eq!(cfg, vuelta);
306    }
307
308    #[test]
309    fn round_trip_postcard() {
310        // El caso real de akasha: serializar con postcard (no auto-descriptivo,
311        // sin soporte de flatten/untagged) y volver. Falla con `Config` directo;
312        // funciona con `WireConfig`.
313        let cfg = config_rico();
314        let wire = WireConfig::from(&cfg);
315        let bytes = postcard::to_allocvec(&wire).expect("postcard serializa el wire");
316        let wire2: WireConfig = postcard::from_bytes(&bytes).expect("postcard deserializa el wire");
317        let vuelta: Config = wire2.into();
318        assert_eq!(cfg, vuelta);
319    }
320}