Skip to main content

kimun_notes/components/
file_list.rs

1use kimun_core::nfs::VaultPath;
2use kimun_core::{ResultType, SearchResult};
3use ratatui::style::{Modifier, Style};
4use ratatui::widgets::ListItem;
5
6use crate::components::rich_row::RichRow;
7use crate::settings::icons::Icons;
8use crate::settings::themes::Theme;
9use crate::settings::{SortFieldSetting, SortOrderSetting};
10
11// ---------------------------------------------------------------------------
12// Sort options
13// ---------------------------------------------------------------------------
14
15#[derive(Clone, Copy, PartialEq, Debug)]
16pub enum SortField {
17    Name,
18    Title,
19}
20
21#[derive(Clone, Copy, PartialEq, Debug)]
22pub enum SortOrder {
23    Ascending,
24    Descending,
25}
26
27impl From<SortFieldSetting> for SortField {
28    fn from(s: SortFieldSetting) -> Self {
29        match s {
30            SortFieldSetting::Name => Self::Name,
31            SortFieldSetting::Title => Self::Title,
32        }
33    }
34}
35
36impl From<SortOrderSetting> for SortOrder {
37    fn from(s: SortOrderSetting) -> Self {
38        match s {
39            SortOrderSetting::Ascending => Self::Ascending,
40            SortOrderSetting::Descending => Self::Descending,
41        }
42    }
43}
44
45impl From<SortField> for SortFieldSetting {
46    fn from(s: SortField) -> Self {
47        match s {
48            SortField::Name => Self::Name,
49            SortField::Title => Self::Title,
50        }
51    }
52}
53
54impl From<SortOrder> for SortOrderSetting {
55    fn from(s: SortOrder) -> Self {
56        match s {
57            SortOrder::Ascending => Self::Ascending,
58            SortOrder::Descending => Self::Descending,
59        }
60    }
61}
62
63impl SortField {
64    pub fn label(self) -> char {
65        match self {
66            Self::Name => 'N',
67            Self::Title => 'T',
68        }
69    }
70
71    pub fn cycle(self) -> Self {
72        match self {
73            Self::Name => Self::Title,
74            Self::Title => Self::Name,
75        }
76    }
77}
78
79impl SortOrder {
80    pub fn label(self) -> char {
81        match self {
82            Self::Ascending => '↑',
83            Self::Descending => '↓',
84        }
85    }
86
87    pub fn toggle(self) -> Self {
88        match self {
89            Self::Ascending => Self::Descending,
90            Self::Descending => Self::Ascending,
91        }
92    }
93}
94
95// ---------------------------------------------------------------------------
96// FileListEntry
97// ---------------------------------------------------------------------------
98
99#[derive(Clone)]
100pub enum FileListEntry {
101    Up {
102        parent: VaultPath,
103    },
104    Note {
105        path: VaultPath,
106        title: String,
107        filename: String,
108        journal_date: Option<String>,
109    },
110    Directory {
111        path: VaultPath,
112        name: String,
113    },
114    Attachment {
115        path: VaultPath,
116        filename: String,
117    },
118    CreateNote {
119        filename: String,
120        path: VaultPath,
121    },
122}
123
124impl FileListEntry {
125    pub fn from_result(result: SearchResult, journal_date: Option<String>) -> Self {
126        let filename = result.path.get_parent_path().1;
127        match result.rtype {
128            ResultType::Note(data) => {
129                let title = if data.title.trim().is_empty() {
130                    "<no title>".to_string()
131                } else {
132                    data.title
133                };
134                Self::Note {
135                    path: result.path,
136                    title,
137                    filename,
138                    journal_date,
139                }
140            }
141            ResultType::Directory => Self::Directory {
142                path: result.path,
143                name: filename,
144            },
145            ResultType::Attachment => Self::Attachment {
146                path: result.path,
147                filename,
148            },
149        }
150    }
151
152    pub fn path(&self) -> &VaultPath {
153        match self {
154            Self::Up { parent } => parent,
155            Self::Note { path, .. } => path,
156            Self::Directory { path, .. } => path,
157            Self::Attachment { path, .. } => path,
158            Self::CreateNote { path, .. } => path,
159        }
160    }
161
162    /// Sort key for the given field.
163    pub(crate) fn sort_key(&self, field: SortField) -> String {
164        match self {
165            Self::Up { .. } => String::new(),
166            Self::Note {
167                title, filename, ..
168            } => match field {
169                SortField::Title => title.to_lowercase(),
170                SortField::Name => filename.to_lowercase(),
171            },
172            Self::Directory { name, .. } => name.to_lowercase(),
173            Self::Attachment { filename, .. } => filename.to_lowercase(),
174            Self::CreateNote { filename, .. } => filename.to_lowercase(),
175        }
176    }
177
178    /// Terminal rows this entry occupies when rendered.
179    pub fn visual_height(&self) -> u16 {
180        match self {
181            Self::Note { journal_date, .. } => {
182                if journal_date.is_some() {
183                    3
184                } else {
185                    2
186                }
187            }
188            _ => 1,
189        }
190    }
191
192    pub fn to_list_item(&self, theme: &Theme, icons: &Icons) -> ListItem<'static> {
193        match self {
194            Self::Up { .. } => RichRow::new(icons.directory_up, "[UP] ..")
195                .glyph_style(Style::default().fg(theme.gray.to_ratatui()))
196                .title_style(Style::default().fg(theme.gray.to_ratatui()))
197                .into_list_item(theme),
198            Self::Note {
199                title,
200                filename,
201                journal_date,
202                ..
203            } => {
204                let glyph = if journal_date.is_some() {
205                    icons.journal
206                } else {
207                    icons.note
208                };
209                let mut row = RichRow::new(glyph, title.clone()).filename(filename.clone());
210                if let Some(date) = journal_date {
211                    row = row.secondary(
212                        date.clone(),
213                        Some(Style::default().fg(theme.color_journal_date.to_ratatui())),
214                    );
215                }
216                row.into_list_item(theme)
217            }
218            Self::Directory { name, .. } => {
219                let dir_style = Style::default().fg(theme.color_directory.to_ratatui());
220                RichRow::new(icons.directory, name.clone())
221                    .glyph_style(dir_style)
222                    .title_style(dir_style)
223                    .into_list_item(theme)
224            }
225            Self::Attachment { filename, .. } => {
226                let style = Style::default()
227                    .add_modifier(Modifier::ITALIC)
228                    .fg(theme.fg_secondary.to_ratatui());
229                RichRow::new(icons.attachment, filename.clone())
230                    .glyph_style(style)
231                    .title_style(style)
232                    .into_list_item(theme)
233            }
234            Self::CreateNote { filename, .. } => {
235                let style = Style::default().fg(theme.accent.to_ratatui());
236                RichRow::new("+", format!("Create: {}", filename))
237                    .glyph_style(style)
238                    .title_style(style)
239                    .into_list_item(theme)
240            }
241        }
242    }
243}
244
245impl crate::components::search_list::SearchRow for FileListEntry {
246    fn to_list_item(&self, theme: &Theme, icons: &Icons, _selected: bool) -> ListItem<'static> {
247        // Delegate to inherent method; engine applies selection highlight via `highlight_style`.
248        FileListEntry::to_list_item(self, theme, icons)
249    }
250
251    fn visual_height(&self) -> u16 {
252        FileListEntry::visual_height(self)
253    }
254
255    fn match_text(&self) -> Option<&str> {
256        match self {
257            Self::Note { filename, .. } | Self::CreateNote { filename, .. } => Some(filename),
258            // Directories participate in the fuzzy filter (matched on their
259            // name). `Up` stays exempt.
260            Self::Directory { name, .. } => Some(name),
261            _ => None,
262        }
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use crate::components::search_list::SearchRow;
270
271    #[test]
272    fn directory_match_text_is_some_name() {
273        let dir = FileListEntry::Directory {
274            path: VaultPath::note_path_from("projects"),
275            name: "projects".to_string(),
276        };
277        assert_eq!(SearchRow::match_text(&dir), Some("projects"));
278    }
279
280    #[test]
281    fn up_match_text_is_none() {
282        let up = FileListEntry::Up {
283            parent: VaultPath::root(),
284        };
285        assert_eq!(SearchRow::match_text(&up), None);
286    }
287
288    #[test]
289    fn sort_field_setting_roundtrip() {
290        use crate::settings::SortFieldSetting;
291        assert_eq!(
292            SortFieldSetting::from(SortField::Name),
293            SortFieldSetting::Name
294        );
295        assert_eq!(
296            SortFieldSetting::from(SortField::Title),
297            SortFieldSetting::Title
298        );
299        assert_eq!(SortField::from(SortFieldSetting::Title), SortField::Title);
300    }
301
302    #[test]
303    fn sort_order_setting_roundtrip() {
304        use crate::settings::SortOrderSetting;
305        assert_eq!(
306            SortOrderSetting::from(SortOrder::Ascending),
307            SortOrderSetting::Ascending
308        );
309        assert_eq!(
310            SortOrderSetting::from(SortOrder::Descending),
311            SortOrderSetting::Descending
312        );
313    }
314}