Skip to main content

try_rs/
tui.rs

1use anyhow::Result;
2use chrono::Local;
3use crossterm::event::{self, Event, KeyCode};
4use fuzzy_matcher::FuzzyMatcher;
5use fuzzy_matcher::skim::SkimMatcherV2;
6use ratatui::{prelude::*, widgets::*};
7
8use std::{
9    collections::HashSet,
10    fs,
11    io::{self},
12    path::{Path, PathBuf},
13    sync::{
14        Arc,
15        atomic::{AtomicU64, Ordering},
16    },
17    thread,
18    time::SystemTime,
19};
20
21pub use crate::themes::Theme;
22use crate::{
23    config::{get_file_config_toml_name, save_config},
24    utils::{self, SelectionResult},
25};
26
27#[derive(Clone, Copy, PartialEq)]
28pub enum AppMode {
29    Normal,
30    DeleteConfirm,
31    ThemeSelect,
32    ConfigSavePrompt,
33    ConfigSaveLocationSelect,
34    About,
35}
36
37#[derive(Clone)]
38pub struct TryEntry {
39    pub name: String,
40    pub display_name: String,
41    pub display_offset: usize,
42    pub match_indices: Vec<usize>,
43    pub modified: SystemTime,
44    pub created: SystemTime,
45    pub score: i64,
46    pub is_git: bool,
47    pub is_worktree: bool,
48    pub is_worktree_locked: bool,
49    pub is_gitmodules: bool,
50    pub is_mise: bool,
51    pub is_cargo: bool,
52    pub is_maven: bool,
53    pub is_flutter: bool,
54    pub is_go: bool,
55    pub is_python: bool,
56}
57
58pub struct App {
59    pub query: String,
60    pub all_entries: Vec<TryEntry>,
61    pub filtered_entries: Vec<TryEntry>,
62    pub selected_index: usize,
63    pub should_quit: bool,
64    pub final_selection: SelectionResult,
65    pub mode: AppMode,
66    pub status_message: Option<String>,
67    pub base_path: PathBuf,
68    pub theme: Theme,
69    pub editor_cmd: Option<String>,
70    pub wants_editor: bool,
71    pub apply_date_prefix: Option<bool>,
72    pub transparent_background: bool,
73    pub show_new_option: bool,
74
75    pub available_themes: Vec<Theme>,
76    pub theme_list_state: ListState,
77    pub original_theme: Option<Theme>,
78    pub original_transparent_background: Option<bool>,
79
80    pub config_path: Option<PathBuf>,
81    pub config_location_state: ListState,
82
83    pub cached_free_space_mb: Option<u64>,
84    pub folder_size_mb: Arc<AtomicU64>,
85
86    current_entries: HashSet<String>,
87    matcher: SkimMatcherV2,
88}
89
90impl App {
91    fn is_current_entry(
92        entry_path: &Path,
93        entry_name: &str,
94        is_symlink: bool,
95        cwd_unresolved: &Path,
96        cwd_real: &Path,
97        base_real: &Path,
98    ) -> bool {
99        if cwd_unresolved.starts_with(entry_path) {
100            return true;
101        }
102
103        if is_symlink {
104            if let Ok(target) = entry_path.canonicalize()
105                && cwd_real.starts_with(&target)
106            {
107                return true;
108            }
109        } else {
110            let resolved_entry = base_real.join(entry_name);
111            if cwd_real.starts_with(&resolved_entry) {
112                return true;
113            }
114        }
115
116        false
117    }
118
119    pub fn new(
120        path: PathBuf,
121        theme: Theme,
122        editor_cmd: Option<String>,
123        config_path: Option<PathBuf>,
124        apply_date_prefix: Option<bool>,
125        transparent_background: bool,
126        query: Option<String>,
127    ) -> Self {
128        let mut entries = Vec::new();
129        let mut current_entries = HashSet::new();
130        let cwd_unresolved = std::env::var_os("PWD")
131            .map(PathBuf::from)
132            .filter(|p| !p.as_os_str().is_empty())
133            .or_else(|| std::env::current_dir().ok())
134            .unwrap_or_else(|| PathBuf::from("."));
135        let cwd_real = std::env::current_dir()
136            .ok()
137            .and_then(|cwd| cwd.canonicalize().ok())
138            .unwrap_or_else(|| cwd_unresolved.clone());
139        let base_real = path.canonicalize().unwrap_or_else(|_| path.clone());
140
141        if let Ok(read_dir) = fs::read_dir(&path) {
142            for entry in read_dir.flatten() {
143                if let Ok(metadata) = entry.metadata()
144                    && metadata.is_dir()
145                {
146                    let entry_path = entry.path();
147                    let name = entry.file_name().to_string_lossy().to_string();
148                    let git_path = entry_path.join(".git");
149                    let is_git = git_path.exists();
150                    let is_worktree = git_path.is_file();
151                    let is_worktree_locked = utils::is_git_worktree_locked(&entry_path);
152                    let is_gitmodules = entry_path.join(".gitmodules").exists();
153                    let is_mise = entry_path.join("mise.toml").exists();
154                    let is_cargo = entry_path.join("Cargo.toml").exists();
155                    let is_maven = entry_path.join("pom.xml").exists();
156                    let is_symlink = entry
157                        .file_type()
158                        .map(|kind| kind.is_symlink())
159                        .unwrap_or(false);
160                    let is_current = Self::is_current_entry(
161                        &entry_path,
162                        &name,
163                        is_symlink,
164                        &cwd_unresolved,
165                        &cwd_real,
166                        &base_real,
167                    );
168                    if is_current {
169                        current_entries.insert(name.clone());
170                    }
171
172                    let created;
173                    let display_name;
174                    if let Some((date_prefix, remainder)) = utils::extract_prefix_date(&name) {
175                        created = date_prefix;
176                        display_name = remainder;
177                    } else {
178                        created = metadata.created().unwrap_or(SystemTime::UNIX_EPOCH);
179                        display_name = name.clone();
180                    }
181                    let display_offset = name
182                        .chars()
183                        .count()
184                        .saturating_sub(display_name.chars().count());
185                    let is_flutter = entry_path.join("pubspec.yaml").exists();
186                    let is_go = entry_path.join("go.mod").exists();
187                    let is_python = entry_path.join("pyproject.toml").exists()
188                        || entry_path.join("requirements.txt").exists();
189                    entries.push(TryEntry {
190                        name,
191                        display_name,
192                        display_offset,
193                        match_indices: Vec::new(),
194                        modified: metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH),
195                        created,
196                        score: 0,
197                        is_git,
198                        is_worktree,
199                        is_worktree_locked,
200                        is_gitmodules,
201                        is_mise,
202                        is_cargo,
203                        is_maven,
204                        is_flutter,
205                        is_go,
206                        is_python,
207                    });
208                }
209            }
210        }
211        entries.sort_by(|a, b| b.modified.cmp(&a.modified));
212
213        let themes = Theme::all();
214
215        let mut theme_state = ListState::default();
216        theme_state.select(Some(0));
217
218        let mut app = Self {
219            query: query.unwrap_or_else(|| String::new()),
220            all_entries: entries.clone(),
221            filtered_entries: entries,
222            selected_index: 0,
223            should_quit: false,
224            final_selection: SelectionResult::None,
225            mode: AppMode::Normal,
226            status_message: None,
227            base_path: path.clone(),
228            theme,
229            editor_cmd,
230            wants_editor: false,
231            apply_date_prefix,
232            transparent_background,
233            show_new_option: false,
234            available_themes: themes,
235            theme_list_state: theme_state,
236            original_theme: None,
237            original_transparent_background: None,
238            config_path,
239            config_location_state: ListState::default(),
240            cached_free_space_mb: utils::get_free_disk_space_mb(&path),
241            folder_size_mb: Arc::new(AtomicU64::new(0)),
242            current_entries,
243            matcher: SkimMatcherV2::default(),
244        };
245
246        // Spawn background thread to calculate folder size
247        let folder_size_arc = Arc::clone(&app.folder_size_mb);
248        let path_clone = path.clone();
249        thread::spawn(move || {
250            let size = utils::get_folder_size_mb(&path_clone);
251            folder_size_arc.store(size, Ordering::Relaxed);
252        });
253
254        app.update_search();
255        app
256    }
257
258    pub fn has_exact_match(&self) -> bool {
259        self.all_entries.iter().any(|e| e.name == self.query)
260    }
261
262    pub fn update_search(&mut self) {
263        if self.query.is_empty() {
264            self.filtered_entries = self.all_entries.clone();
265        } else {
266            self.filtered_entries = self
267                .all_entries
268                .iter()
269                .filter_map(|entry| {
270                    self.matcher
271                        .fuzzy_indices(&entry.name, &self.query)
272                        .map(|(score, indices)| {
273                            let mut e = entry.clone();
274                            e.score = score;
275                            if entry.display_offset == 0 {
276                                e.match_indices = indices;
277                            } else {
278                                e.match_indices = indices
279                                    .into_iter()
280                                    .filter_map(|idx| idx.checked_sub(entry.display_offset))
281                                    .collect();
282                            }
283                            e
284                        })
285                })
286                .collect();
287
288            self.filtered_entries.sort_by(|a, b| b.score.cmp(&a.score));
289        }
290        self.show_new_option = !self.query.is_empty() && !self.has_exact_match();
291        self.selected_index = 0;
292    }
293
294    pub fn delete_selected(&mut self) {
295        if let Some(entry_name) = self
296            .filtered_entries
297            .get(self.selected_index)
298            .map(|e| e.name.clone())
299        {
300            let path_to_remove = self.base_path.join(&entry_name);
301
302            // Only use git worktree remove if it's actually a worktree (not main working tree)
303            if utils::is_git_worktree(&path_to_remove) {
304                match utils::remove_git_worktree(&path_to_remove) {
305                    Ok(output) => {
306                        if output.status.success() {
307                            self.all_entries.retain(|e| e.name != entry_name);
308                            self.update_search();
309                            self.status_message =
310                                Some(format!("Worktree removed: {path_to_remove:?}"));
311                        } else {
312                            self.status_message = Some(format!(
313                                "Error deleting: {}",
314                                String::from_utf8_lossy(&output.stderr)
315                                    .lines()
316                                    .take(1)
317                                    .collect::<String>()
318                            ));
319                        }
320                    }
321                    Err(e) => {
322                        self.status_message = Some(format!("Error removing worktree: {}", e));
323                    }
324                };
325            } else {
326                // Regular directory or main git repo - just delete it
327                match fs::remove_dir_all(&path_to_remove) {
328                    Ok(_) => {
329                        self.all_entries.retain(|e| e.name != entry_name);
330                        self.update_search();
331                        self.status_message =
332                            Some(format!("Deleted: {}", path_to_remove.display()));
333                    }
334                    Err(e) => {
335                        self.status_message = Some(format!("Error deleting: {}", e));
336                    }
337                }
338            };
339        }
340        self.mode = AppMode::Normal;
341    }
342}
343
344fn draw_popup(f: &mut Frame, title: &str, message: &str, theme: &Theme) {
345    let area = f.area();
346
347    let popup_layout = Layout::default()
348        .direction(Direction::Vertical)
349        .constraints([
350            Constraint::Percentage(40),
351            Constraint::Length(8),
352            Constraint::Percentage(40),
353        ])
354        .split(area);
355
356    let popup_area = Layout::default()
357        .direction(Direction::Horizontal)
358        .constraints([
359            Constraint::Percentage(35),
360            Constraint::Percentage(30),
361            Constraint::Percentage(35),
362        ])
363        .split(popup_layout[1])[1];
364
365    f.render_widget(Clear, popup_area);
366
367    let block = Block::default()
368        .title(title)
369        .borders(Borders::ALL)
370        .style(Style::default().bg(theme.popup_bg));
371
372    // Vertically center the text inside the popup
373    let inner_height = popup_area.height.saturating_sub(2) as usize; // subtract borders
374    let text_lines = message.lines().count();
375    let top_padding = inner_height.saturating_sub(text_lines) / 2;
376    let padded_message = format!("{}{}", "\n".repeat(top_padding), message);
377
378    let paragraph = Paragraph::new(padded_message)
379        .block(block)
380        .style(
381            Style::default()
382                .fg(theme.popup_text)
383                .add_modifier(Modifier::BOLD),
384        )
385        .alignment(Alignment::Center);
386
387    f.render_widget(paragraph, popup_area);
388}
389
390fn draw_theme_select(f: &mut Frame, app: &mut App) {
391    let area = f.area();
392    let popup_layout = Layout::default()
393        .direction(Direction::Vertical)
394        .constraints([
395            Constraint::Percentage(25),
396            Constraint::Percentage(50),
397            Constraint::Percentage(25),
398        ])
399        .split(area);
400
401    let popup_area = Layout::default()
402        .direction(Direction::Horizontal)
403        .constraints([
404            Constraint::Percentage(25),
405            Constraint::Percentage(50),
406            Constraint::Percentage(25),
407        ])
408        .split(popup_layout[1])[1];
409
410    f.render_widget(Clear, popup_area);
411
412    // Split popup into theme list and transparency option
413    let inner_layout = Layout::default()
414        .direction(Direction::Vertical)
415        .constraints([Constraint::Min(3), Constraint::Length(3)])
416        .split(popup_area);
417
418    let block = Block::default()
419        .title(" Select Theme ")
420        .borders(Borders::ALL)
421        .style(Style::default().bg(app.theme.popup_bg));
422
423    let items: Vec<ListItem> = app
424        .available_themes
425        .iter()
426        .map(|t| {
427            ListItem::new(t.name.clone()).style(Style::default().fg(app.theme.list_highlight_fg))
428        })
429        .collect();
430
431    let list = List::new(items)
432        .block(block)
433        .highlight_style(
434            Style::default()
435                .bg(app.theme.list_highlight_bg)
436                .fg(app.theme.list_selected_fg)
437                .add_modifier(Modifier::BOLD),
438        )
439        .highlight_symbol(">> ");
440
441    f.render_stateful_widget(list, inner_layout[0], &mut app.theme_list_state);
442
443    // Draw transparency checkbox
444    let checkbox = if app.transparent_background {
445        "[x]"
446    } else {
447        "[ ]"
448    };
449    let transparency_text = format!(" {} Transparent Background (Space to toggle)", checkbox);
450    let transparency_block = Block::default()
451        .borders(Borders::ALL)
452        .style(Style::default().bg(app.theme.popup_bg));
453    let transparency_paragraph = Paragraph::new(transparency_text)
454        .style(Style::default().fg(app.theme.list_highlight_fg))
455        .block(transparency_block);
456    f.render_widget(transparency_paragraph, inner_layout[1]);
457}
458
459fn draw_config_location_select(f: &mut Frame, app: &mut App) {
460    let area = f.area();
461    let popup_layout = Layout::default()
462        .direction(Direction::Vertical)
463        .constraints([
464            Constraint::Percentage(40),
465            Constraint::Length(8),
466            Constraint::Percentage(40),
467        ])
468        .split(area);
469
470    let popup_area = Layout::default()
471        .direction(Direction::Horizontal)
472        .constraints([
473            Constraint::Percentage(20),
474            Constraint::Percentage(60),
475            Constraint::Percentage(20),
476        ])
477        .split(popup_layout[1])[1];
478
479    f.render_widget(Clear, popup_area);
480
481    let block = Block::default()
482        .title(" Select Config Location ")
483        .borders(Borders::ALL)
484        .style(Style::default().bg(app.theme.popup_bg));
485
486    let config_name = get_file_config_toml_name();
487    let items = vec![
488        ListItem::new(format!("System Config (~/.config/try-rs/{})", config_name))
489            .style(Style::default().fg(app.theme.list_highlight_fg)),
490        ListItem::new(format!("Home Directory (~/{})", config_name))
491            .style(Style::default().fg(app.theme.list_highlight_fg)),
492    ];
493
494    let list = List::new(items)
495        .block(block)
496        .highlight_style(
497            Style::default()
498                .bg(app.theme.list_highlight_bg)
499                .fg(app.theme.list_selected_fg)
500                .add_modifier(Modifier::BOLD),
501        )
502        .highlight_symbol(">> ");
503
504    f.render_stateful_widget(list, popup_area, &mut app.config_location_state);
505}
506
507fn draw_about_popup(f: &mut Frame, theme: &Theme) {
508    let area = f.area();
509    let popup_layout = Layout::default()
510        .direction(Direction::Vertical)
511        .constraints([
512            Constraint::Percentage(25),
513            Constraint::Length(12),
514            Constraint::Percentage(25),
515        ])
516        .split(area);
517
518    let popup_area = Layout::default()
519        .direction(Direction::Horizontal)
520        .constraints([
521            Constraint::Percentage(30),
522            Constraint::Percentage(40),
523            Constraint::Percentage(30),
524        ])
525        .split(popup_layout[1])[1];
526
527    f.render_widget(Clear, popup_area);
528
529    let block = Block::default()
530        .title(" About ")
531        .borders(Borders::ALL)
532        .style(Style::default().bg(theme.popup_bg));
533
534    let text = vec![
535        Line::from(vec![
536            Span::styled(
537                "🦀 try",
538                Style::default()
539                    .fg(theme.title_try)
540                    .add_modifier(Modifier::BOLD),
541            ),
542            Span::styled("-", Style::default().fg(Color::DarkGray)),
543            Span::styled(
544                "rs",
545                Style::default()
546                    .fg(theme.title_rs)
547                    .add_modifier(Modifier::BOLD),
548            ),
549            Span::styled(
550                format!(" v{}", env!("CARGO_PKG_VERSION")),
551                Style::default().fg(Color::DarkGray),
552            ),
553        ]),
554        Line::from(""),
555        Line::from(Span::styled(
556            "try-rs.org",
557            Style::default().fg(theme.search_title),
558        )),
559        Line::from(""),
560        Line::from(Span::styled(
561            "github.com/tassiovirginio/try-rs",
562            Style::default().fg(theme.search_title),
563        )),
564        Line::from(""),
565        Line::from(vec![
566            Span::styled("󰈙 License: ", Style::default().fg(theme.helpers_colors)),
567            Span::styled(
568                "MIT",
569                Style::default()
570                    .fg(theme.status_message)
571                    .add_modifier(Modifier::BOLD),
572            ),
573        ]),
574        Line::from(""),
575        Line::from(Span::styled(
576            "Press Esc to close",
577            Style::default().fg(theme.helpers_colors),
578        )),
579    ];
580
581    let paragraph = Paragraph::new(text)
582        .block(block)
583        .alignment(Alignment::Center);
584
585    f.render_widget(paragraph, popup_area);
586}
587
588fn build_highlighted_name_spans(
589    text: &str,
590    match_indices: &[usize],
591    highlight_style: Style,
592) -> Vec<Span<'static>> {
593    if text.is_empty() {
594        return Vec::new();
595    }
596
597    if match_indices.is_empty() {
598        return vec![Span::raw(text.to_string())];
599    }
600
601    let chars = text.chars().collect::<Vec<_>>();
602    let mut spans = Vec::new();
603    let mut cursor = 0usize;
604    let mut idx = 0usize;
605
606    while idx < match_indices.len() {
607        let start = match_indices[idx];
608        if start >= chars.len() {
609            break;
610        }
611
612        if cursor < start {
613            spans.push(Span::raw(chars[cursor..start].iter().collect::<String>()));
614        }
615
616        let mut end = start + 1;
617        idx += 1;
618        while idx < match_indices.len() && match_indices[idx] == end {
619            end += 1;
620            idx += 1;
621        }
622
623        let end = end.min(chars.len());
624
625        spans.push(Span::styled(
626            chars[start..end].iter().collect::<String>(),
627            highlight_style,
628        ));
629        cursor = end;
630    }
631
632    if cursor < chars.len() {
633        spans.push(Span::raw(chars[cursor..].iter().collect::<String>()));
634    }
635
636    spans
637}
638
639pub fn run_app(
640    terminal: &mut Terminal<CrosstermBackend<io::Stderr>>,
641    mut app: App,
642) -> Result<(SelectionResult, bool)> {
643    while !app.should_quit {
644        terminal.draw(|f| {
645            // Render background if not transparent
646            if !app.transparent_background {
647                if let Some(bg_color) = app.theme.background {
648                    let background = Block::default().style(Style::default().bg(bg_color));
649                    f.render_widget(background, f.area());
650                }
651            }
652
653            let chunks = Layout::default()
654                .direction(Direction::Vertical)
655                .constraints([
656                    Constraint::Length(3),
657                    Constraint::Min(1),
658                    Constraint::Length(1),
659                ])
660                .split(f.area());
661
662            let content_chunks = Layout::default()
663                .direction(Direction::Horizontal)
664                .constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
665                .split(chunks[1]);
666
667            let search_chunks = Layout::default()
668                .direction(Direction::Horizontal)
669                .constraints([Constraint::Min(20), Constraint::Length(45)])
670                .split(chunks[0]);
671
672            let search_text = Paragraph::new(app.query.clone())
673                .style(Style::default().fg(app.theme.search_title))
674                .block(
675                    Block::default()
676                        .borders(Borders::ALL)
677                        .title(Span::styled(
678                            " Search/New ",
679                            Style::default().fg(app.theme.search_title),
680                        ))
681                        .border_style(Style::default().fg(app.theme.search_border)),
682                );
683            f.render_widget(search_text, search_chunks[0]);
684
685            let free_space = app
686                .cached_free_space_mb
687                .map(|s| {
688                    if s >= 1000 {
689                        format!("{:.1} GB", s as f64 / 1024.0)
690                    } else {
691                        format!("{} MB", s)
692                    }
693                })
694                .unwrap_or_else(|| "N/A".to_string());
695
696            let folder_size = app.folder_size_mb.load(Ordering::Relaxed);
697            let folder_size_str = if folder_size == 0 {
698                "---".to_string()
699            } else if folder_size >= 1000 {
700                format!("{:.1} GB", folder_size as f64 / 1024.0)
701            } else {
702                format!("{} MB", folder_size)
703            };
704
705            let memory_info = Paragraph::new(Line::from(vec![
706                Span::styled("󰋊 ", Style::default().fg(app.theme.title_rs)),
707                Span::styled("Used: ", Style::default().fg(app.theme.helpers_colors)),
708                Span::styled(
709                    folder_size_str,
710                    Style::default().fg(app.theme.status_message),
711                ),
712                Span::styled(" | ", Style::default().fg(app.theme.helpers_colors)),
713                Span::styled("Free: ", Style::default().fg(app.theme.helpers_colors)),
714                Span::styled(free_space, Style::default().fg(app.theme.status_message)),
715            ]))
716            .block(
717                Block::default()
718                    .borders(Borders::ALL)
719                    .title(Span::styled(
720                        " Disk ",
721                        Style::default().fg(app.theme.disk_title),
722                    ))
723                    .border_style(Style::default().fg(app.theme.disk_border)),
724            )
725            .alignment(Alignment::Center);
726            f.render_widget(memory_info, search_chunks[1]);
727
728            let matched_char_style = Style::default()
729                .fg(app.theme.list_match_fg)
730                .add_modifier(Modifier::BOLD | Modifier::UNDERLINED);
731
732            let mut items: Vec<ListItem> = app
733                .filtered_entries
734                .iter()
735                .map(|entry| {
736                    let now = SystemTime::now();
737                    let elapsed = now
738                        .duration_since(entry.modified)
739                        .unwrap_or(std::time::Duration::ZERO);
740                    let secs = elapsed.as_secs();
741                    let days = secs / 86400;
742                    let hours = (secs % 86400) / 3600;
743                    let minutes = (secs % 3600) / 60;
744                    let date_str = format!("({:02}d {:02}h {:02}m)", days, hours, minutes);
745
746                    let width = content_chunks[0].width.saturating_sub(5) as usize;
747
748                    let date_text = date_str.to_string();
749                    let date_width = date_text.chars().count();
750
751                    // Build icon list: (flag, icon_str, color)
752                    let icons: &[(bool, &str, Color)] = &[
753                        (entry.is_cargo, " ", app.theme.icon_rust),
754                        (entry.is_maven, " ", app.theme.icon_maven),
755                        (entry.is_flutter, " ", app.theme.icon_flutter),
756                        (entry.is_go, " ", app.theme.icon_go),
757                        (entry.is_python, " ", app.theme.icon_python),
758                        (entry.is_mise, "󰬔 ", app.theme.icon_mise),
759                        (entry.is_worktree, "󰙅 ", app.theme.icon_worktree),
760                        (entry.is_worktree_locked, " ", app.theme.icon_worktree_lock),
761                        (entry.is_gitmodules, " ", app.theme.icon_gitmodules),
762                        (entry.is_git, " ", app.theme.icon_git),
763                    ];
764                    let icons_width: usize = icons.iter().filter(|(f, _, _)| *f).count() * 2;
765                    let icon_width = 2; // folder icon
766
767                    let created_dt: chrono::DateTime<Local> = entry.created.into();
768                    let created_text = created_dt.format("%Y-%m-%d").to_string();
769                    let created_width = created_text.chars().count();
770
771                    let reserved = date_width + icons_width + icon_width + created_width + 2;
772                    let available_for_name = width.saturating_sub(reserved);
773                    let name_len = entry.display_name.chars().count();
774
775                    let (display_name, display_match_indices, is_truncated, padding) = if name_len
776                        > available_for_name
777                    {
778                        let safe_len = available_for_name.saturating_sub(3);
779                        let truncated: String = entry.display_name.chars().take(safe_len).collect();
780                        (
781                            truncated,
782                            entry
783                                .match_indices
784                                .iter()
785                                .copied()
786                                .filter(|idx| *idx < safe_len)
787                                .collect::<Vec<_>>(),
788                            true,
789                            1,
790                        )
791                    } else {
792                        (
793                            entry.display_name.clone(),
794                            entry.match_indices.clone(),
795                            false,
796                            width.saturating_sub(
797                                icon_width
798                                    + created_width
799                                    + 1
800                                    + name_len
801                                    + date_width
802                                    + icons_width,
803                            ),
804                        )
805                    };
806
807                    let is_current = app.current_entries.contains(&entry.name);
808                    let marker = if is_current { "* " } else { "  " };
809                    let marker_style = if is_current {
810                        Style::default()
811                            .fg(app.theme.list_match_fg)
812                            .add_modifier(Modifier::BOLD)
813                    } else {
814                        Style::default()
815                    };
816
817                    let mut spans = vec![
818                        Span::styled(marker, marker_style),
819                        Span::styled("󰝰 ", Style::default().fg(app.theme.icon_folder)),
820                        Span::styled(created_text, Style::default().fg(app.theme.list_date)),
821                        Span::raw(" "),
822                    ];
823                    spans.extend(build_highlighted_name_spans(
824                        &display_name,
825                        &display_match_indices,
826                        matched_char_style,
827                    ));
828                    if is_truncated {
829                        spans.push(Span::raw("..."));
830                    }
831                    spans.push(Span::raw(" ".repeat(padding)));
832                    for &(flag, icon, color) in icons {
833                        if flag {
834                            spans.push(Span::styled(icon, Style::default().fg(color)));
835                        }
836                    }
837                    spans.push(Span::styled(
838                        date_text,
839                        Style::default().fg(app.theme.list_date),
840                    ));
841
842                    ListItem::new(Line::from(spans))
843                        .style(Style::default().fg(app.theme.list_highlight_fg))
844                })
845                .collect();
846
847            // Append "new" option when no exact match
848            if app.show_new_option {
849                let new_item = ListItem::new(Line::from(vec![
850                    Span::styled("  ", Style::default().fg(app.theme.search_title)),
851                    Span::styled(
852                        format!("Create new: {}", app.query),
853                        Style::default()
854                            .fg(app.theme.search_title)
855                            .add_modifier(Modifier::ITALIC),
856                    ),
857                ]));
858                items.push(new_item);
859            }
860
861            let list = List::new(items)
862                .block(
863                    Block::default()
864                        .borders(Borders::ALL)
865                        .title(Span::styled(
866                            " Folders ",
867                            Style::default().fg(app.theme.folder_title),
868                        ))
869                        .border_style(Style::default().fg(app.theme.folder_border)),
870                )
871                .highlight_style(
872                    Style::default()
873                        .bg(app.theme.list_highlight_bg)
874                        .add_modifier(Modifier::BOLD),
875                )
876                .highlight_symbol("→ ");
877
878            let mut state = ListState::default();
879            state.select(Some(app.selected_index));
880            f.render_stateful_widget(list, content_chunks[0], &mut state);
881
882            // Split right area between Preview and Icon Legend
883            let right_chunks = Layout::default()
884                .direction(Direction::Vertical)
885                .constraints([Constraint::Min(1), Constraint::Length(4)])
886                .split(content_chunks[1]);
887
888            // Check if "new" option is currently selected
889            let is_new_selected =
890                app.show_new_option && app.selected_index == app.filtered_entries.len();
891
892            if is_new_selected {
893                // Show "new folder" preview
894                let preview_lines = vec![Line::from(Span::styled(
895                    " (new folder) ",
896                    Style::default()
897                        .fg(app.theme.search_title)
898                        .add_modifier(Modifier::ITALIC),
899                ))];
900                let preview = Paragraph::new(preview_lines).block(
901                    Block::default()
902                        .borders(Borders::ALL)
903                        .title(Span::styled(
904                            " Preview ",
905                            Style::default().fg(app.theme.preview_title),
906                        ))
907                        .border_style(Style::default().fg(app.theme.preview_border)),
908                );
909                f.render_widget(preview, right_chunks[0]);
910            } else if let Some(selected) = app.filtered_entries.get(app.selected_index) {
911                let preview_path = app.base_path.join(&selected.name);
912                let mut preview_lines = Vec::new();
913
914                if let Ok(entries) = fs::read_dir(&preview_path) {
915                    for e in entries
916                        .take(right_chunks[0].height.saturating_sub(2) as usize)
917                        .flatten()
918                    {
919                        let file_name = e.file_name().to_string_lossy().to_string();
920                        let is_dir = e.file_type().map(|t| t.is_dir()).unwrap_or(false);
921                        let (icon, color) = if is_dir {
922                            ("󰝰 ", app.theme.icon_folder)
923                        } else {
924                            ("󰈙 ", app.theme.icon_file)
925                        };
926                        preview_lines.push(Line::from(vec![
927                            Span::styled(icon, Style::default().fg(color)),
928                            Span::raw(file_name),
929                        ]));
930                    }
931                }
932
933                if preview_lines.is_empty() {
934                    preview_lines.push(Line::from(Span::styled(
935                        " (empty) ",
936                        Style::default().fg(Color::DarkGray),
937                    )));
938                }
939
940                let preview = Paragraph::new(preview_lines).block(
941                    Block::default()
942                        .borders(Borders::ALL)
943                        .title(Span::styled(
944                            " Preview ",
945                            Style::default().fg(app.theme.preview_title),
946                        ))
947                        .border_style(Style::default().fg(app.theme.preview_border)),
948                );
949                f.render_widget(preview, right_chunks[0]);
950            } else {
951                let preview = Block::default()
952                    .borders(Borders::ALL)
953                    .title(Span::styled(
954                        " Preview ",
955                        Style::default().fg(app.theme.preview_title),
956                    ))
957                    .border_style(Style::default().fg(app.theme.preview_border));
958                f.render_widget(preview, right_chunks[0]);
959            }
960
961            // Icon legend
962            let legend_lines = vec![Line::from(vec![
963                Span::styled(" ", Style::default().fg(app.theme.icon_rust)),
964                Span::styled("Rust ", Style::default().fg(app.theme.helpers_colors)),
965                Span::styled(" ", Style::default().fg(app.theme.icon_maven)),
966                Span::styled("Maven ", Style::default().fg(app.theme.helpers_colors)),
967                Span::styled(" ", Style::default().fg(app.theme.icon_flutter)),
968                Span::styled("Flutter ", Style::default().fg(app.theme.helpers_colors)),
969                Span::styled(" ", Style::default().fg(app.theme.icon_go)),
970                Span::styled("Go ", Style::default().fg(app.theme.helpers_colors)),
971                Span::styled(" ", Style::default().fg(app.theme.icon_python)),
972                Span::styled("Python ", Style::default().fg(app.theme.helpers_colors)),
973                Span::styled("󰬔 ", Style::default().fg(app.theme.icon_mise)),
974                Span::styled("Mise ", Style::default().fg(app.theme.helpers_colors)),
975                Span::styled(" ", Style::default().fg(app.theme.icon_worktree_lock)),
976                Span::styled("Locked ", Style::default().fg(app.theme.helpers_colors)),
977                Span::styled("󰙅 ", Style::default().fg(app.theme.icon_worktree)),
978                Span::styled(
979                    "Git-Worktree ",
980                    Style::default().fg(app.theme.helpers_colors),
981                ),
982                Span::styled(" ", Style::default().fg(app.theme.icon_gitmodules)),
983                Span::styled("Git-Submod ", Style::default().fg(app.theme.helpers_colors)),
984                Span::styled(" ", Style::default().fg(app.theme.icon_git)),
985                Span::styled("Git ", Style::default().fg(app.theme.helpers_colors)),
986            ])];
987
988            let legend = Paragraph::new(legend_lines)
989                .block(
990                    Block::default()
991                        .borders(Borders::ALL)
992                        .title(Span::styled(
993                            " Legends ",
994                            Style::default().fg(app.theme.legends_title),
995                        ))
996                        .border_style(Style::default().fg(app.theme.legends_border)),
997                )
998                .alignment(Alignment::Left)
999                .wrap(Wrap { trim: true });
1000            f.render_widget(legend, right_chunks[1]);
1001
1002            let help_text = if let Some(msg) = &app.status_message {
1003                Line::from(vec![Span::styled(
1004                    msg,
1005                    Style::default()
1006                        .fg(app.theme.status_message)
1007                        .add_modifier(Modifier::BOLD),
1008                )])
1009            } else {
1010                Line::from(vec![
1011                    Span::styled("↑↓", Style::default().add_modifier(Modifier::BOLD)),
1012                    Span::raw(" Nav | "),
1013                    Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
1014                    Span::raw(" Select | "),
1015                    Span::styled("Ctrl-D", Style::default().add_modifier(Modifier::BOLD)),
1016                    Span::raw(" Del | "),
1017                    Span::styled("Ctrl-E", Style::default().add_modifier(Modifier::BOLD)),
1018                    Span::raw(" Edit | "),
1019                    Span::styled("Ctrl-T", Style::default().add_modifier(Modifier::BOLD)),
1020                    Span::raw(" Theme | "),
1021                    Span::styled("Ctrl-A", Style::default().add_modifier(Modifier::BOLD)),
1022                    Span::raw(" About | "),
1023                    Span::styled("Esc/Ctrl+C", Style::default().add_modifier(Modifier::BOLD)),
1024                    Span::raw(" Quit"),
1025                ])
1026            };
1027
1028            let help_message = Paragraph::new(help_text)
1029                .style(Style::default().fg(app.theme.helpers_colors))
1030                .alignment(Alignment::Center);
1031
1032            f.render_widget(help_message, chunks[2]);
1033
1034            if app.mode == AppMode::DeleteConfirm
1035                && let Some(selected) = app.filtered_entries.get(app.selected_index)
1036            {
1037                let msg = format!("Delete '{}'?\n(y/n)", selected.name);
1038                draw_popup(f, " WARNING ", &msg, &app.theme);
1039            }
1040
1041            if app.mode == AppMode::ThemeSelect {
1042                draw_theme_select(f, &mut app);
1043            }
1044
1045            if app.mode == AppMode::ConfigSavePrompt {
1046                draw_popup(
1047                    f,
1048                    " Create Config? ",
1049                    "Config file not found.\nCreate one now to save theme? (y/n)",
1050                    &app.theme,
1051                );
1052            }
1053
1054            if app.mode == AppMode::ConfigSaveLocationSelect {
1055                draw_config_location_select(f, &mut app);
1056            }
1057
1058            if app.mode == AppMode::About {
1059                draw_about_popup(f, &app.theme);
1060            }
1061        })?;
1062
1063        // Poll with 1-second timeout so the screen refreshes periodically
1064        if !event::poll(std::time::Duration::from_secs(1))? {
1065            continue;
1066        }
1067        if let Event::Key(key) = event::read()? {
1068            if !key.is_press() {
1069                continue;
1070            }
1071            // Clear status message on any key press so it disappears after one redraw
1072            app.status_message = None;
1073            match app.mode {
1074                AppMode::Normal => match key.code {
1075                    KeyCode::Char(c) => {
1076                        if c == 'c' && key.modifiers.contains(event::KeyModifiers::CONTROL) {
1077                            app.should_quit = true;
1078                        } else if c == 'd' && key.modifiers.contains(event::KeyModifiers::CONTROL) {
1079                            let is_new_selected = app.show_new_option
1080                                && app.selected_index == app.filtered_entries.len();
1081                            if !app.filtered_entries.is_empty() && !is_new_selected {
1082                                app.mode = AppMode::DeleteConfirm;
1083                            }
1084                        } else if c == 'e' && key.modifiers.contains(event::KeyModifiers::CONTROL) {
1085                            if app.editor_cmd.is_some() {
1086                                let is_new_selected = app.show_new_option
1087                                    && app.selected_index == app.filtered_entries.len();
1088                                if is_new_selected {
1089                                    app.final_selection = SelectionResult::New(app.query.clone());
1090                                    app.wants_editor = true;
1091                                    app.should_quit = true;
1092                                } else if !app.filtered_entries.is_empty() {
1093                                    app.final_selection = SelectionResult::Folder(
1094                                        app.filtered_entries[app.selected_index].name.clone(),
1095                                    );
1096                                    app.wants_editor = true;
1097                                    app.should_quit = true;
1098                                } else if !app.query.is_empty() {
1099                                    app.final_selection = SelectionResult::New(app.query.clone());
1100                                    app.wants_editor = true;
1101                                    app.should_quit = true;
1102                                }
1103                            } else {
1104                                app.status_message =
1105                                    Some("No editor configured in config.toml".to_string());
1106                            }
1107                        } else if c == 't' && key.modifiers.contains(event::KeyModifiers::CONTROL) {
1108                            // Save current theme and transparency before opening selector
1109                            app.original_theme = Some(app.theme.clone());
1110                            app.original_transparent_background = Some(app.transparent_background);
1111                            // Find and select current theme in the list
1112                            let current_idx = app
1113                                .available_themes
1114                                .iter()
1115                                .position(|t| t.name == app.theme.name)
1116                                .unwrap_or(0);
1117                            app.theme_list_state.select(Some(current_idx));
1118                            app.mode = AppMode::ThemeSelect;
1119                        } else if c == 'a' && key.modifiers.contains(event::KeyModifiers::CONTROL) {
1120                            app.mode = AppMode::About;
1121                        } else if matches!(c, 'k' | 'p')
1122                            && key.modifiers.contains(event::KeyModifiers::CONTROL)
1123                        {
1124                            if app.selected_index > 0 {
1125                                app.selected_index -= 1;
1126                            }
1127                        } else if matches!(c, 'j' | 'n')
1128                            && key.modifiers.contains(event::KeyModifiers::CONTROL)
1129                        {
1130                            let max_index = if app.show_new_option {
1131                                app.filtered_entries.len()
1132                            } else {
1133                                app.filtered_entries.len().saturating_sub(1)
1134                            };
1135                            if app.selected_index < max_index {
1136                                app.selected_index += 1;
1137                            }
1138                        } else if c == 'u' && key.modifiers.contains(event::KeyModifiers::CONTROL) {
1139                            app.query.clear();
1140                            app.update_search();
1141                        } else if key.modifiers.is_empty()
1142                            || key.modifiers == event::KeyModifiers::SHIFT
1143                        {
1144                            app.query.push(c);
1145                            app.update_search();
1146                        }
1147                    }
1148                    KeyCode::Backspace => {
1149                        app.query.pop();
1150                        app.update_search();
1151                    }
1152                    KeyCode::Up => {
1153                        if app.selected_index > 0 {
1154                            app.selected_index -= 1;
1155                        }
1156                    }
1157                    KeyCode::Down => {
1158                        let max_index = if app.show_new_option {
1159                            app.filtered_entries.len()
1160                        } else {
1161                            app.filtered_entries.len().saturating_sub(1)
1162                        };
1163                        if app.selected_index < max_index {
1164                            app.selected_index += 1;
1165                        }
1166                    }
1167                    KeyCode::Enter => {
1168                        let is_new_selected =
1169                            app.show_new_option && app.selected_index == app.filtered_entries.len();
1170                        if is_new_selected {
1171                            app.final_selection = SelectionResult::New(app.query.clone());
1172                        } else if !app.filtered_entries.is_empty() {
1173                            app.final_selection = SelectionResult::Folder(
1174                                app.filtered_entries[app.selected_index].name.clone(),
1175                            );
1176                        } else if !app.query.is_empty() {
1177                            app.final_selection = SelectionResult::New(app.query.clone());
1178                        }
1179                        app.should_quit = true;
1180                    }
1181                    KeyCode::Esc => app.should_quit = true,
1182                    _ => {}
1183                },
1184
1185                AppMode::DeleteConfirm => match key.code {
1186                    KeyCode::Char('y') | KeyCode::Char('Y') => {
1187                        app.delete_selected();
1188                    }
1189                    KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
1190                        app.mode = AppMode::Normal;
1191                    }
1192                    KeyCode::Char('c') if key.modifiers.contains(event::KeyModifiers::CONTROL) => {
1193                        app.should_quit = true;
1194                    }
1195                    _ => {}
1196                },
1197
1198                AppMode::ThemeSelect => match key.code {
1199                    KeyCode::Char(' ') => {
1200                        // Toggle transparent background
1201                        app.transparent_background = !app.transparent_background;
1202                    }
1203                    KeyCode::Esc => {
1204                        // Restore original theme and transparency
1205                        if let Some(original) = app.original_theme.take() {
1206                            app.theme = original;
1207                        }
1208                        if let Some(original_transparent) =
1209                            app.original_transparent_background.take()
1210                        {
1211                            app.transparent_background = original_transparent;
1212                        }
1213                        app.mode = AppMode::Normal;
1214                    }
1215                    KeyCode::Char('c') if key.modifiers.contains(event::KeyModifiers::CONTROL) => {
1216                        // Restore original theme and transparency
1217                        if let Some(original) = app.original_theme.take() {
1218                            app.theme = original;
1219                        }
1220                        if let Some(original_transparent) =
1221                            app.original_transparent_background.take()
1222                        {
1223                            app.transparent_background = original_transparent;
1224                        }
1225                        app.mode = AppMode::Normal;
1226                    }
1227                    KeyCode::Up | KeyCode::Char('k' | 'p') => {
1228                        let i = match app.theme_list_state.selected() {
1229                            Some(i) => {
1230                                if i > 0 {
1231                                    i - 1
1232                                } else {
1233                                    i
1234                                }
1235                            }
1236                            None => 0,
1237                        };
1238                        app.theme_list_state.select(Some(i));
1239                        // Apply theme preview
1240                        if let Some(theme) = app.available_themes.get(i) {
1241                            app.theme = theme.clone();
1242                        }
1243                    }
1244                    KeyCode::Down | KeyCode::Char('j' | 'n') => {
1245                        let i = match app.theme_list_state.selected() {
1246                            Some(i) => {
1247                                if i < app.available_themes.len() - 1 {
1248                                    i + 1
1249                                } else {
1250                                    i
1251                                }
1252                            }
1253                            None => 0,
1254                        };
1255                        app.theme_list_state.select(Some(i));
1256                        // Apply theme preview
1257                        if let Some(theme) = app.available_themes.get(i) {
1258                            app.theme = theme.clone();
1259                        }
1260                    }
1261                    KeyCode::Enter => {
1262                        // Clear original theme and transparency (we're confirming the new values)
1263                        app.original_theme = None;
1264                        app.original_transparent_background = None;
1265                        if let Some(i) = app.theme_list_state.selected() {
1266                            if let Some(theme) = app.available_themes.get(i) {
1267                                app.theme = theme.clone();
1268
1269                                if let Some(ref path) = app.config_path {
1270                                    if let Err(e) = save_config(
1271                                        path,
1272                                        &app.theme,
1273                                        &app.base_path,
1274                                        &app.editor_cmd,
1275                                        app.apply_date_prefix,
1276                                        Some(app.transparent_background),
1277                                    ) {
1278                                        app.status_message = Some(format!("Error saving: {}", e));
1279                                    } else {
1280                                        app.status_message = Some("Theme saved.".to_string());
1281                                    }
1282                                    app.mode = AppMode::Normal;
1283                                } else {
1284                                    app.mode = AppMode::ConfigSavePrompt;
1285                                }
1286                            } else {
1287                                app.mode = AppMode::Normal;
1288                            }
1289                        } else {
1290                            app.mode = AppMode::Normal;
1291                        }
1292                    }
1293                    _ => {}
1294                },
1295                AppMode::ConfigSavePrompt => match key.code {
1296                    KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
1297                        app.mode = AppMode::ConfigSaveLocationSelect;
1298                        app.config_location_state.select(Some(0));
1299                    }
1300                    KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
1301                        app.mode = AppMode::Normal;
1302                    }
1303                    _ => {}
1304                },
1305
1306                AppMode::ConfigSaveLocationSelect => match key.code {
1307                    KeyCode::Esc | KeyCode::Char('c')
1308                        if key.modifiers.contains(event::KeyModifiers::CONTROL) =>
1309                    {
1310                        app.mode = AppMode::Normal;
1311                    }
1312                    KeyCode::Up | KeyCode::Char('k' | 'p') => {
1313                        let i = match app.config_location_state.selected() {
1314                            Some(i) => {
1315                                if i > 0 {
1316                                    i - 1
1317                                } else {
1318                                    i
1319                                }
1320                            }
1321                            None => 0,
1322                        };
1323                        app.config_location_state.select(Some(i));
1324                    }
1325                    KeyCode::Down | KeyCode::Char('j' | 'n') => {
1326                        let i = match app.config_location_state.selected() {
1327                            Some(i) => {
1328                                if i < 1 {
1329                                    i + 1
1330                                } else {
1331                                    i
1332                                }
1333                            }
1334                            None => 0,
1335                        };
1336                        app.config_location_state.select(Some(i));
1337                    }
1338                    KeyCode::Enter => {
1339                        if let Some(i) = app.config_location_state.selected() {
1340                            let config_name = get_file_config_toml_name();
1341                            let path = if i == 0 {
1342                                dirs::config_dir()
1343                                    .unwrap_or_else(|| {
1344                                        dirs::home_dir().expect("Folder not found").join(".config")
1345                                    })
1346                                    .join("try-rs")
1347                                    .join(&config_name)
1348                            } else {
1349                                dirs::home_dir()
1350                                    .expect("Folder not found")
1351                                    .join(&config_name)
1352                            };
1353
1354                            if let Err(e) = save_config(
1355                                &path,
1356                                &app.theme,
1357                                &app.base_path,
1358                                &app.editor_cmd,
1359                                app.apply_date_prefix,
1360                                Some(app.transparent_background),
1361                            ) {
1362                                app.status_message = Some(format!("Error saving config: {}", e));
1363                            } else {
1364                                app.config_path = Some(path);
1365                                app.status_message = Some("Theme saved!".to_string());
1366                            }
1367                        }
1368                        app.mode = AppMode::Normal;
1369                    }
1370                    _ => {}
1371                },
1372                AppMode::About => match key.code {
1373                    KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') | KeyCode::Char(' ') => {
1374                        app.mode = AppMode::Normal;
1375                    }
1376                    KeyCode::Char('c') if key.modifiers.contains(event::KeyModifiers::CONTROL) => {
1377                        app.mode = AppMode::Normal;
1378                    }
1379                    _ => {}
1380                },
1381            }
1382        }
1383    }
1384
1385    Ok((app.final_selection, app.wants_editor))
1386}
1387
1388#[cfg(test)]
1389mod tests {
1390    use super::App;
1391    use std::{fs, path::PathBuf};
1392    use tempdir::TempDir;
1393
1394    #[test]
1395    fn current_entry_detects_nested_path() {
1396        let temp = TempDir::new("current-entry-nested").unwrap();
1397        let base_path = temp.path().to_path_buf();
1398        let entry_name = "2025-11-20-gamma";
1399        let entry_path = base_path.join(entry_name);
1400        let nested_path = entry_path.join("nested/deeper");
1401
1402        fs::create_dir_all(&nested_path).unwrap();
1403
1404        assert!(App::is_current_entry(
1405            &entry_path,
1406            entry_name,
1407            false,
1408            &nested_path,
1409            &nested_path,
1410            &base_path,
1411        ));
1412    }
1413
1414    #[test]
1415    fn current_entry_detects_nested_path_with_stale_pwd() {
1416        let temp = TempDir::new("current-entry-script").unwrap();
1417        let base_path = temp.path().to_path_buf();
1418        let entry_name = "2025-11-20-gamma";
1419        let entry_path = base_path.join(entry_name);
1420        let nested_path = entry_path.join("nested/deeper");
1421
1422        fs::create_dir_all(&nested_path).unwrap();
1423
1424        let stale_pwd = PathBuf::from("/tmp/not-the-real-cwd");
1425
1426        assert!(App::is_current_entry(
1427            &entry_path,
1428            entry_name,
1429            false,
1430            &stale_pwd,
1431            &nested_path,
1432            &base_path,
1433        ));
1434    }
1435}