1use serde::de::Error as DeError;
5use serde::ser::Error as SerError;
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use serde_json::Value as Json;
8
9use crate::extras::button::Button;
10use crate::extras::code::Code;
11use crate::extras::diff::Diff;
12use crate::extras::empty::Empty;
13use crate::extras::image::Image;
14use crate::extras::input::Input;
15use crate::extras::key_hints::KeyHints;
16use crate::extras::log::Log;
17use crate::extras::markdown::Markdown;
18use crate::extras::select::Select;
19use crate::extras::spinner::Spinner;
20use crate::extras::toggle::Toggle;
21use crate::extras::tree::Tree;
22use crate::layout::Layout;
23use crate::style::Style;
24use crate::widgets::barchart::BarChart;
25use crate::widgets::block::{Block, BlockProps};
26use crate::widgets::chart::Chart;
27use crate::widgets::gauge::{Gauge, LineGauge};
28use crate::widgets::list::List;
29use crate::widgets::paragraph::Paragraph;
30use crate::widgets::scrollbar::Scrollbar;
31use crate::widgets::sparkline::Sparkline;
32use crate::widgets::table::Table;
33use crate::widgets::tabs::Tabs;
34
35pub type Key = String;
37
38pub type ActionId = String;
40
41#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
44pub struct Common {
45 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub key: Option<Key>,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub style: Option<Style>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub block: Option<BlockProps>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub fallback: Option<Box<Node>>,
57}
58
59impl Common {
60 #[must_use]
62 pub fn new() -> Self {
63 Self {
64 key: None,
65 style: None,
66 block: None,
67 fallback: None,
68 }
69 }
70
71 #[must_use]
73 pub fn key(mut self, key: impl Into<Key>) -> Self {
74 self.key = Some(key.into());
75 self
76 }
77
78 #[must_use]
80 pub fn style(mut self, style: Style) -> Self {
81 self.style = Some(style);
82 self
83 }
84
85 #[must_use]
87 pub fn block(mut self, block: BlockProps) -> Self {
88 self.block = Some(block);
89 self
90 }
91
92 #[must_use]
94 pub fn fallback(mut self, node: impl Into<Node>) -> Self {
95 self.fallback = Some(Box::new(node.into()));
96 self
97 }
98}
99
100#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
104pub struct Unknown {
105 pub t: String,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub key: Option<Key>,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub style: Option<Style>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub block: Option<BlockProps>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub fallback: Option<Box<Node>>,
119}
120
121#[derive(Clone, Debug, PartialEq)]
126pub enum Node {
127 Layout(Layout),
129 Block(Block),
131 Paragraph(Paragraph),
133 List(List),
135 Table(Table),
137 Tabs(Tabs),
139 Gauge(Gauge),
141 LineGauge(LineGauge),
143 Sparkline(Sparkline),
145 BarChart(BarChart),
147 Chart(Chart),
149 Scrollbar(Scrollbar),
151 Markdown(Markdown),
153 Code(Code),
155 Diff(Diff),
157 Image(Image),
159 Log(Log),
161 Tree(Tree),
163 KeyHints(KeyHints),
165 Input(Input),
167 Toggle(Toggle),
169 Select(Select),
171 Button(Button),
173 Spinner(Spinner),
175 Empty(Empty),
177 Unknown(Unknown),
179}
180
181macro_rules! for_each_kind {
182 ($macro:ident) => {
183 $macro! {
184 (Layout, "Layout", Layout),
185 (Block, "Block", Block),
186 (Paragraph, "Paragraph", Paragraph),
187 (List, "List", List),
188 (Table, "Table", Table),
189 (Tabs, "Tabs", Tabs),
190 (Gauge, "Gauge", Gauge),
191 (LineGauge, "LineGauge", LineGauge),
192 (Sparkline, "Sparkline", Sparkline),
193 (BarChart, "BarChart", BarChart),
194 (Chart, "Chart", Chart),
195 (Scrollbar, "Scrollbar", Scrollbar),
196 (Markdown, "Markdown", Markdown),
197 (Code, "Code", Code),
198 (Diff, "Diff", Diff),
199 (Image, "Image", Image),
200 (Log, "Log", Log),
201 (Tree, "Tree", Tree),
202 (KeyHints, "KeyHints", KeyHints),
203 (Input, "Input", Input),
204 (Toggle, "Toggle", Toggle),
205 (Select, "Select", Select),
206 (Button, "Button", Button),
207 (Spinner, "Spinner", Spinner),
208 (Empty, "Empty", Empty)
209 }
210 };
211}
212
213macro_rules! impl_node_kind_table {
214 ($(($variant:ident, $tag:literal, $ty:ty)),* $(,)?) => {
215 pub const NODE_KINDS: [&str; 25] = [$($tag),*];
217
218 impl Node {
219 #[must_use]
221 pub fn tag(&self) -> Option<&'static str> {
222 match self {
223 $(Self::$variant(_) => Some($tag),)*
224 Self::Unknown(_) => None,
225 }
226 }
227
228 #[must_use]
230 pub fn kind(&self) -> &str {
231 match self {
232 $(Self::$variant(_) => $tag,)*
233 Self::Unknown(u) => &u.t,
234 }
235 }
236
237 #[must_use]
239 pub fn common(&self) -> CommonRef<'_> {
240 match self {
241 $(Self::$variant(inner) => CommonRef {
242 key: inner.common.key.as_deref(),
243 style: inner.common.style.as_ref(),
244 block: inner.common.block.as_ref(),
245 fallback: inner.common.fallback.as_deref(),
246 },)*
247 Self::Unknown(u) => CommonRef {
248 key: u.key.as_deref(),
249 style: u.style.as_ref(),
250 block: u.block.as_ref(),
251 fallback: u.fallback.as_deref(),
252 },
253 }
254 }
255 }
256
257 #[derive(Clone, Copy, Debug)]
259 pub struct CommonRef<'a> {
260 pub key: Option<&'a str>,
262 pub style: Option<&'a Style>,
264 pub block: Option<&'a BlockProps>,
266 pub fallback: Option<&'a Node>,
268 }
269
270 impl Serialize for Node {
271 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
272 fn tagged<S: Serializer>(
273 serializer: S,
274 tag: &str,
275 value: &impl Serialize,
276 ) -> Result<S::Ok, S::Error> {
277 use serde::ser::SerializeMap;
278 let inner = serde_json::to_value(value)
279 .map_err(|e| S::Error::custom(e.to_string()))?;
280 let Json::Object(map) = inner else {
281 return Err(S::Error::custom("node props must serialize to an object"));
282 };
283 let mut out = serializer.serialize_map(Some(map.len() + 1))?;
284 out.serialize_entry("t", tag)?;
285 for (k, v) in map {
286 out.serialize_entry(&k, &v)?;
287 }
288 out.end()
289 }
290 match self {
291 $(Self::$variant(inner) => tagged(serializer, $tag, inner),)*
292 Self::Unknown(u) => u.serialize(serializer),
293 }
294 }
295 }
296
297 impl<'de> Deserialize<'de> for Node {
298 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
299 let value = Json::deserialize(deserializer)?;
300 let tag = match value.get("t") {
301 Some(Json::String(tag)) => tag.clone(),
302 _ if !value.is_object() => {
303 return Err(D::Error::custom("a node must be a JSON object"));
304 }
305 _ => {
306 return Err(D::Error::custom("a node needs a string \"t\" tag"));
307 }
308 };
309 match tag.as_str() {
310 $($tag => serde_json::from_value::<$ty>(value)
311 .map(Self::$variant)
312 .map_err(|e| D::Error::custom(format!("{tag}: {}", e))),)*
313 _ => serde_json::from_value::<Unknown>(value)
314 .map(Self::Unknown)
315 .map_err(|e| D::Error::custom(format!("{tag}: {}", e))),
316 }
317 }
318 }
319 };
320}
321
322for_each_kind!(impl_node_kind_table);
323
324pub trait IntoNode: Into<Node> {
326 fn into_node(self) -> Node {
328 self.into()
329 }
330}
331
332impl IntoNode for Node {}
333
334pub trait WithCommon: Sized {
336 fn common_mut(&mut self) -> &mut Common;
338
339 #[must_use]
341 fn key(mut self, key: impl Into<Key>) -> Self {
342 self.common_mut().key = Some(key.into());
343 self
344 }
345
346 #[must_use]
348 fn style(mut self, style: Style) -> Self {
349 self.common_mut().style = Some(style);
350 self
351 }
352
353 #[must_use]
355 fn block(mut self, block: impl Into<BlockProps>) -> Self {
356 self.common_mut().block = Some(block.into());
357 self
358 }
359
360 #[must_use]
362 fn fallback(mut self, node: impl Into<Node>) -> Self {
363 self.common_mut().fallback = Some(Box::new(node.into()));
364 self
365 }
366}
367
368macro_rules! impl_with_common {
369 ($(($variant:ident, $tag:literal, $ty:ty)),* $(,)?) => {
370 $(impl WithCommon for $ty {
371 fn common_mut(&mut self) -> &mut Common {
372 &mut self.common
373 }
374 })*
375 };
376}
377
378for_each_kind!(impl_with_common);
379
380#[cfg(test)]
381mod tests {
382 #![allow(
383 clippy::unwrap_used,
384 clippy::expect_used,
385 clippy::panic,
386 reason = "tests assert by unwrapping"
387 )]
388 use super::*;
389
390 #[test]
391 fn unknown_kind_is_kept_with_fallback() {
392 let raw = r#"{"t":"Holgram","style":{"fg":"accent"},"fallback":{"t":"Spinner"}}"#;
393 let node: Node = serde_json::from_str(raw).expect("unknown node parses");
394 assert_eq!(node.kind(), "Holgram");
395 let fallback = node.common().fallback.expect("fallback kept");
396 assert_eq!(fallback.tag(), Some("Spinner"));
397 let round: Node = serde_json::from_str(&serde_json::to_string(&node).expect("serializes"))
398 .expect("re-parses");
399 assert_eq!(node, round);
400 }
401
402 #[test]
403 fn missing_tag_is_an_error() {
404 assert!(
405 serde_json::from_str::<Node>(r#"{"text":"hi"}"#).is_err(),
406 "missing t must not parse"
407 );
408 }
409
410 #[test]
411 fn all_tags_round_trip() {
412 let samples: Vec<Node> = vec![
413 Layout::vertical([crate::layout::Constraint::Fill(1)]).into(),
414 Block::bordered().into(),
415 Paragraph::new("x").into(),
416 List::new(["a"]).into(),
417 Table::new(
418 [crate::widgets::table::Row::new(["a"])],
419 [crate::layout::Constraint::Length(4)],
420 )
421 .into(),
422 Tabs::new([crate::widgets::tabs::TabTitle::new("a", "A")]).into(),
423 Gauge::new().into(),
424 LineGauge::new(0.5).into(),
425 Sparkline::new([Some(1), None]).into(),
426 BarChart::new([crate::widgets::barchart::BarGroup::new([
427 crate::widgets::barchart::Bar::new(1),
428 ])])
429 .into(),
430 Chart::new([crate::widgets::chart::Dataset::new([[0.0, 1.0]])]).into(),
431 Scrollbar::new(crate::widgets::scrollbar::ScrollbarState::new(10)).into(),
432 Markdown::new("# x").into(),
433 Code::new("let x = 1;").into(),
434 Diff::patch("@@ -1 +1 @@\n-a\n+b").into(),
435 Image::new(crate::extras::image::ImageSource::path("a.png"), "an image").into(),
436 Log::new(["line"]).into(),
437 Tree::new([crate::extras::tree::TreeNode::new("a", "A")]).into(),
438 KeyHints::new([("enter", "open")]).into(),
439 Input::new("name").into(),
440 Toggle::new("on", "Enabled", true).into(),
441 Select::new("mode", [("a", "A")]).into(),
442 Button::new("Go", "go").into(),
443 Spinner::new("working").into(),
444 Empty::new().title("Nothing").into(),
445 ];
446 assert_eq!(samples.len(), NODE_KINDS.len());
447 for node in samples {
448 let json = serde_json::to_value(&node).unwrap_or(Json::Null);
449 let back: Node = serde_json::from_value(json).unwrap_or(Node::Unknown(Unknown {
450 t: "boom".into(),
451 ..Unknown::default()
452 }));
453 assert_eq!(node, back, "round trip for {}", node.kind());
454 }
455 }
456}