1use kimun_core::nfs::VaultPath;
2use kimun_core::{ResultType, SearchResult};
3use ratatui::style::{Modifier, Style};
4use ratatui::text::{Line, Span, Text};
5use ratatui::widgets::ListItem;
6
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 let lines: Vec<Line> = match self {
194 Self::Up { .. } => vec![Line::from(Span::styled(
195 format!("{} [UP] ..", icons.directory_up),
196 Style::default().fg(theme.fg_muted.to_ratatui()),
197 ))],
198 Self::Note {
199 title,
200 filename,
201 journal_date,
202 ..
203 } => {
204 let mut lines = vec![];
205 if let Some(date) = journal_date {
206 lines.push(Line::from(format!("{} {}", icons.journal, title)));
207 lines.push(Line::from(Span::styled(
208 format!(" {}", date),
209 Style::default().fg(theme.color_journal_date.to_ratatui()),
210 )));
211 } else {
212 lines.push(Line::from(format!("{} {}", icons.note, title)));
213 }
214 lines.push(Line::from(Span::styled(
215 format!(" {}", filename),
216 Style::default()
217 .add_modifier(Modifier::ITALIC)
218 .fg(theme.fg_secondary.to_ratatui()),
219 )));
220 lines
221 }
222 Self::Directory { name, .. } => vec![Line::from(Span::styled(
223 format!("{} {}", icons.directory, name),
224 Style::default().fg(theme.color_directory.to_ratatui()),
225 ))],
226 Self::Attachment { filename, .. } => vec![Line::from(Span::styled(
227 format!("{} {}", icons.attachment, filename),
228 Style::default()
229 .add_modifier(Modifier::ITALIC)
230 .fg(theme.fg_secondary.to_ratatui()),
231 ))],
232 Self::CreateNote { filename, .. } => vec![Line::from(Span::styled(
233 format!("+ Create: {}", filename),
234 Style::default().fg(theme.accent.to_ratatui()),
235 ))],
236 };
237 ListItem::new(Text::from(lines))
238 }
239}
240
241impl crate::components::search_list::SearchRow for FileListEntry {
242 fn to_list_item(&self, theme: &Theme, icons: &Icons, _selected: bool) -> ListItem<'static> {
243 FileListEntry::to_list_item(self, theme, icons)
245 }
246
247 fn visual_height(&self) -> u16 {
248 FileListEntry::visual_height(self)
249 }
250
251 fn match_text(&self) -> Option<&str> {
252 match self {
253 Self::Note { filename, .. } | Self::CreateNote { filename, .. } => Some(filename),
254 Self::Directory { name, .. } => Some(name),
257 _ => None,
258 }
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use crate::components::search_list::SearchRow;
266
267 #[test]
268 fn directory_match_text_is_some_name() {
269 let dir = FileListEntry::Directory {
270 path: VaultPath::note_path_from("projects"),
271 name: "projects".to_string(),
272 };
273 assert_eq!(SearchRow::match_text(&dir), Some("projects"));
274 }
275
276 #[test]
277 fn up_match_text_is_none() {
278 let up = FileListEntry::Up {
279 parent: VaultPath::root(),
280 };
281 assert_eq!(SearchRow::match_text(&up), None);
282 }
283
284 #[test]
285 fn sort_field_setting_roundtrip() {
286 use crate::settings::SortFieldSetting;
287 assert_eq!(
288 SortFieldSetting::from(SortField::Name),
289 SortFieldSetting::Name
290 );
291 assert_eq!(
292 SortFieldSetting::from(SortField::Title),
293 SortFieldSetting::Title
294 );
295 assert_eq!(SortField::from(SortFieldSetting::Title), SortField::Title);
296 }
297
298 #[test]
299 fn sort_order_setting_roundtrip() {
300 use crate::settings::SortOrderSetting;
301 assert_eq!(
302 SortOrderSetting::from(SortOrder::Ascending),
303 SortOrderSetting::Ascending
304 );
305 assert_eq!(
306 SortOrderSetting::from(SortOrder::Descending),
307 SortOrderSetting::Descending
308 );
309 }
310}