runa_tui/app/
nav.rs

1use crate::file_manager::FileEntry;
2use std::collections::{HashMap, HashSet};
3use std::ffi::OsString;
4use std::path::{Path, PathBuf};
5
6pub struct NavState {
7    current_dir: PathBuf,
8    entries: Vec<FileEntry>,
9    selected: usize,
10    positions: HashMap<PathBuf, usize>,
11    markers: HashSet<PathBuf>,
12    filter: String,
13    request_id: u64,
14}
15
16impl NavState {
17    pub fn new(path: PathBuf) -> Self {
18        Self {
19            current_dir: path,
20            entries: Vec::new(),
21            selected: 0,
22            positions: HashMap::new(),
23            markers: HashSet::new(),
24            filter: String::new(),
25            request_id: 0,
26        }
27    }
28
29    // Getters / Accessors
30
31    pub fn current_dir(&self) -> &Path {
32        &self.current_dir
33    }
34
35    pub fn entries(&self) -> &[FileEntry] {
36        &self.entries
37    }
38
39    pub fn markers(&self) -> &HashSet<PathBuf> {
40        &self.markers
41    }
42
43    pub fn filter(&self) -> &str {
44        &self.filter
45    }
46
47    pub fn selected_idx(&self) -> usize {
48        self.selected
49    }
50
51    pub fn request_id(&self) -> u64 {
52        self.request_id
53    }
54
55    pub fn selected_entry(&self) -> Option<&FileEntry> {
56        self.entries.get(self.selected)
57    }
58
59    // Nav functions
60
61    pub fn prepare_new_request(&mut self) -> u64 {
62        self.request_id += 1;
63        self.request_id
64    }
65
66    pub fn move_up(&mut self) -> bool {
67        let len = self.shown_entries_len();
68        if len == 0 {
69            return false;
70        }
71
72        if self.selected == 0 {
73            self.selected = len - 1;
74        } else {
75            self.selected -= 1;
76        }
77        true
78    }
79
80    pub fn move_down(&mut self) -> bool {
81        let len = self.shown_entries_len();
82        if len == 0 {
83            return false;
84        }
85
86        self.selected = (self.selected + 1) % len;
87        true
88    }
89
90    pub fn save_position(&mut self) {
91        self.positions
92            .insert(self.current_dir.clone(), self.selected);
93    }
94
95    pub fn get_position(&self) -> &HashMap<PathBuf, usize> {
96        &self.positions
97    }
98
99    pub fn set_path(&mut self, path: PathBuf) {
100        self.current_dir = path;
101        self.entries.clear();
102        self.selected = 0;
103        // instantly kills all pending messages from the previous directory.
104        self.request_id += 1;
105    }
106
107    pub fn update_from_worker(
108        &mut self,
109        path: PathBuf,
110        entries: Vec<FileEntry>,
111        focus: Option<OsString>,
112    ) {
113        self.current_dir = path;
114        self.entries = entries;
115
116        if let Some(f) = focus {
117            self.selected = self
118                .entries
119                .iter()
120                .position(|e| e.name() == &f)
121                .unwrap_or(0);
122        } else {
123            self.selected = self.positions.get(&self.current_dir).cloned().unwrap_or(0);
124        }
125
126        self.selected = self.selected.min(self.entries.len().saturating_sub(1));
127    }
128
129    pub fn toggle_marker(&mut self) {
130        if let Some(entry) = self.selected_shown_entry() {
131            let path = self.current_dir().join(entry.name());
132            if !self.markers.remove(&path) {
133                self.markers.insert(path);
134            }
135        }
136    }
137
138    pub fn clear_markers(&mut self) {
139        self.markers.clear();
140    }
141
142    pub fn get_action_targets(&self) -> HashSet<PathBuf> {
143        if self.markers.is_empty() {
144            self.selected_entry()
145                .map(|e| self.current_dir.join(e.name()))
146                .into_iter()
147                .collect()
148        } else {
149            self.markers.iter().cloned().collect()
150        }
151    }
152
153    // Filter functions
154
155    pub fn filtered_entries(&self) -> Vec<&FileEntry> {
156        if self.filter.is_empty() {
157            self.entries.iter().collect()
158        } else {
159            let filter_lower = self.filter.to_lowercase();
160            self.entries
161                .iter()
162                .filter(|e| {
163                    e.name()
164                        .to_string_lossy()
165                        .to_lowercase()
166                        .contains(&filter_lower)
167                })
168                .collect()
169        }
170    }
171
172    pub fn shown_entries(&self) -> Box<dyn Iterator<Item = &FileEntry> + '_> {
173        if self.filter.is_empty() {
174            Box::new(self.entries.iter())
175        } else {
176            let filter_lower = self.filter.to_lowercase();
177            Box::new(self.entries.iter().filter(move |e| {
178                e.name()
179                    .to_string_lossy()
180                    .to_lowercase()
181                    .contains(&filter_lower)
182            }))
183        }
184    }
185
186    pub fn shown_entries_len(&self) -> usize {
187        if self.filter.is_empty() {
188            self.entries.len()
189        } else {
190            let filter_lower = self.filter.to_lowercase();
191            self.entries
192                .iter()
193                .filter(|e| {
194                    e.name()
195                        .to_string_lossy()
196                        .to_lowercase()
197                        .contains(&filter_lower)
198                })
199                .count()
200        }
201    }
202
203    pub fn selected_shown_entry(&self) -> Option<&FileEntry> {
204        self.shown_entries().nth(self.selected)
205    }
206
207    pub fn set_filter(&mut self, filter: String) {
208        self.filter = filter;
209        self.selected = 0;
210    }
211
212    pub fn clear_filters(&mut self) {
213        self.filter.clear();
214    }
215}