Skip to main content

kimun_notes/components/
rich_row.rs

1//! The shared **rich list row** format every drawer list uses (spec §4):
2//!
3//! ```text
4//! ▤ Auth Flow Meeting              04-08
5//!   attendees: maria, david              ← optional secondary line
6//!   2026-04-08.md                        ← dim italic filename line
7//! ```
8//!
9//! A `RichRow` is a declarative description; `into_list_item` renders it with
10//! the theme's roles. Selection background is applied by the `SearchList`
11//! engine's highlight style — rows only choose foregrounds.
12//!
13//! `meta` currently renders inline after the title; right-aligning it needs
14//! the row width, which the `SearchRow` seam does not carry yet — that pass
15//! lands with the telescope alignment work (Phase 08).
16
17use ratatui::style::{Modifier, Style};
18use ratatui::text::{Line, Span, Text};
19use ratatui::widgets::ListItem;
20
21use crate::settings::themes::Theme;
22
23#[derive(Default)]
24pub struct RichRow {
25    glyph: String,
26    glyph_style: Option<Style>,
27    title: String,
28    title_style: Option<Style>,
29    /// Dim metadata after the title (count, date, …).
30    meta: Option<String>,
31    /// Optional secondary line with its own style.
32    secondary: Option<(String, Option<Style>)>,
33    /// Dim italic filename line.
34    filename: Option<String>,
35}
36
37impl RichRow {
38    pub fn new(glyph: impl Into<String>, title: impl Into<String>) -> Self {
39        Self {
40            glyph: glyph.into(),
41            title: title.into(),
42            ..Self::default()
43        }
44    }
45
46    pub fn glyph_style(mut self, style: Style) -> Self {
47        self.glyph_style = Some(style);
48        self
49    }
50
51    pub fn title_style(mut self, style: Style) -> Self {
52        self.title_style = Some(style);
53        self
54    }
55
56    pub fn meta(mut self, meta: impl Into<String>) -> Self {
57        self.meta = Some(meta.into());
58        self
59    }
60
61    pub fn secondary(mut self, text: impl Into<String>, style: Option<Style>) -> Self {
62        self.secondary = Some((text.into(), style));
63        self
64    }
65
66    pub fn filename(mut self, filename: impl Into<String>) -> Self {
67        self.filename = Some(filename.into());
68        self
69    }
70
71    /// Terminal rows this row occupies when rendered.
72    pub fn height(&self) -> u16 {
73        1 + u16::from(self.secondary.is_some()) + u16::from(self.filename.is_some())
74    }
75
76    pub fn into_list_item(self, theme: &Theme) -> ListItem<'static> {
77        let fg = Style::default().fg(theme.fg.to_ratatui());
78        let gray = Style::default().fg(theme.gray.to_ratatui());
79        let secondary_default = Style::default()
80            .fg(theme.fg_secondary.to_ratatui())
81            .add_modifier(Modifier::ITALIC);
82
83        let mut main = vec![
84            Span::styled(format!("{} ", self.glyph), self.glyph_style.unwrap_or(fg)),
85            Span::styled(self.title, self.title_style.unwrap_or(fg)),
86        ];
87        if let Some(meta) = self.meta {
88            main.push(Span::styled(format!("  {meta}"), gray));
89        }
90
91        let mut lines = vec![Line::from(main)];
92        if let Some((text, style)) = self.secondary {
93            lines.push(Line::from(Span::styled(
94                format!("  {text}"),
95                style.unwrap_or(secondary_default),
96            )));
97        }
98        if let Some(filename) = self.filename {
99            lines.push(Line::from(Span::styled(
100                format!("  {filename}"),
101                secondary_default,
102            )));
103        }
104        ListItem::new(Text::from(lines))
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn height_counts_optional_lines() {
114        let theme = Theme::default();
115        let row = RichRow::new("X", "title");
116        assert_eq!(row.height(), 1);
117        let row = RichRow::new("X", "title").filename("a.md");
118        assert_eq!(row.height(), 2);
119        let row = RichRow::new("X", "title")
120            .secondary("sub", None)
121            .filename("a.md");
122        assert_eq!(row.height(), 3);
123        // Renders without panicking.
124        let _ = RichRow::new("X", "t").meta("42").into_list_item(&theme);
125    }
126}