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#[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#[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 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 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 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 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}