Skip to content

Commit cb4a776

Browse files
committed
Simplify navigation state
1 parent 63fee43 commit cb4a776

14 files changed

Lines changed: 167 additions & 201 deletions

File tree

src/application/state.rs

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,9 @@ pub enum PanelMode {
1616
/// Quick view mode variants.
1717
#[derive(Debug, Clone, PartialEq)]
1818
pub enum QuickViewMode {
19-
Text {
20-
lines: Vec<String>,
21-
start: usize,
22-
length: usize,
23-
},
24-
Image(Vec<u8>, Vec<u8>),
25-
Directory {
26-
lines: Vec<String>,
27-
},
19+
Text { lines: Vec<String>, start: usize },
20+
Image(Vec<u8>),
21+
Directory { lines: Vec<String> },
2822
NotSupported,
2923
}
3024

@@ -86,7 +80,12 @@ impl PanelState {
8680

8781
/// Gets the currently selected entry's absolute path.
8882
pub fn get_selected_path(&self) -> Option<PathBuf> {
89-
self.entries.get(self.cursor).map(|e| e.path.clone())
83+
self.selected_entry().map(|entry| entry.path.clone())
84+
}
85+
86+
/// Gets the currently selected entry.
87+
pub fn selected_entry(&self) -> Option<&FileEntry> {
88+
self.entries.get(self.cursor)
9089
}
9190

9291
/// Toggles multi-selection for the given index.
@@ -236,17 +235,10 @@ mod tests {
236235
let mode = PanelMode::QuickView(QuickViewMode::Text {
237236
lines: vec!["line1".to_string(), "line2".to_string()],
238237
start: 0,
239-
length: 2,
240238
});
241-
if let PanelMode::QuickView(QuickViewMode::Text {
242-
lines,
243-
start,
244-
length,
245-
}) = mode
246-
{
239+
if let PanelMode::QuickView(QuickViewMode::Text { lines, start }) = mode {
247240
assert_eq!(lines.len(), 2);
248241
assert_eq!(start, 0);
249-
assert_eq!(length, 2);
250242
} else {
251243
panic!("Expected QuickView Text mode");
252244
}
@@ -266,9 +258,8 @@ mod tests {
266258

267259
#[test]
268260
fn test_panel_mode_quick_view_image() {
269-
let mode = PanelMode::QuickView(QuickViewMode::Image(vec![1, 2, 3], vec![4, 5, 6]));
270-
if let PanelMode::QuickView(QuickViewMode::Image(pixels, raw)) = mode {
271-
assert_eq!(pixels, vec![1, 2, 3]);
261+
let mode = PanelMode::QuickView(QuickViewMode::Image(vec![4, 5, 6]));
262+
if let PanelMode::QuickView(QuickViewMode::Image(raw)) = mode {
272263
assert_eq!(raw, vec![4, 5, 6]);
273264
} else {
274265
panic!("Expected QuickView Image mode");

src/application/use_cases/file_ops.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,9 @@ pub fn copy_to_clipboard(
2626
panel: &mut PanelState,
2727
absolute_path: bool,
2828
) -> Result<(), String> {
29-
if panel.entries.is_empty() || panel.cursor >= panel.entries.len() {
29+
let Some(entry) = panel.selected_entry() else {
3030
return Err("No file selected".to_string());
31-
}
32-
33-
let entry = &panel.entries[panel.cursor];
31+
};
3432
let text = if absolute_path {
3533
entry.path.to_string_lossy().to_string()
3634
} else {

src/application/use_cases/navigate.rs

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,16 @@ pub fn refresh_entries(fs: &dyn FileSystemPort, panel: &mut PanelState) {
2727

2828
// List directory contents
2929
if let Ok(entries) = fs.list_dir(&panel.current_path) {
30-
let filtered: Vec<FileEntry> = entries
31-
.into_iter()
32-
.filter(|e| {
33-
panel.filter_string.is_empty()
34-
|| e.name
35-
.to_lowercase()
36-
.contains(&panel.filter_string.to_lowercase())
37-
})
38-
.collect();
39-
panel.entries.extend(filtered);
30+
if panel.filter_string.is_empty() {
31+
panel.entries.extend(entries);
32+
} else {
33+
let needle = panel.filter_string.to_lowercase();
34+
panel.entries.extend(
35+
entries
36+
.into_iter()
37+
.filter(|entry| entry.name.to_lowercase().contains(&needle)),
38+
);
39+
}
4040
}
4141

4242
panel.cursor = 0;
@@ -78,11 +78,9 @@ pub fn navigate_end(panel: &mut PanelState, visible_rows: u16) {
7878
/// Opens the selected entry (enters directory or triggers file action).
7979
/// Returns true if the selected item is a directory and was entered.
8080
pub fn enter_selected(fs: &dyn FileSystemPort, panel: &mut PanelState) -> bool {
81-
if panel.entries.is_empty() || panel.cursor >= panel.entries.len() {
81+
let Some(entry) = panel.selected_entry() else {
8282
return false;
83-
}
84-
85-
let entry = &panel.entries[panel.cursor];
83+
};
8684

8785
if entry.name == ".." {
8886
go_up_one_level(fs, panel);

src/application/use_cases/quick_view.rs

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,15 @@ const MIN_WRAP_WIDTH: u16 = 20;
1212
const DIR_PREVIEW_LIMIT: usize = 20;
1313

1414
pub fn open(panel: &mut PanelState, wrap_width: u16) {
15-
if panel.entries.is_empty() || panel.cursor >= panel.entries.len() {
16-
return;
15+
if let Some(path) = panel.get_selected_path() {
16+
show_file(panel, path, wrap_width);
1717
}
18-
let path = panel.entries[panel.cursor].path.clone();
19-
show_file(panel, path, wrap_width);
2018
}
2119

2220
pub fn scroll(panel: &mut PanelState, direction: isize, rows: u16, header: u16, footer: u16) {
23-
if let PanelMode::QuickView(QuickViewMode::Text { start, length, .. }) = &mut panel.mode {
21+
if let PanelMode::QuickView(QuickViewMode::Text { lines, start }) = &mut panel.mode {
2422
let visible = rows.saturating_sub(header + footer) as usize;
25-
let max = length.saturating_sub(visible.max(1));
23+
let max = lines.len().saturating_sub(visible.max(1));
2624
*start = (*start as isize + direction).clamp(0, max as isize) as usize;
2725
}
2826
}
@@ -68,11 +66,9 @@ fn show_text(panel: &mut PanelState, path: &Path, wrap_width: u16) {
6866
.into_iter()
6967
.map(|l| l.to_string())
7068
.collect();
71-
let length = lines.len();
7269
panel.mode = PanelMode::QuickView(QuickViewMode::Text {
7370
lines: highlight(lines),
7471
start: 0,
75-
length,
7672
});
7773
} else {
7874
panel.mode = PanelMode::QuickView(QuickViewMode::NotSupported);
@@ -174,8 +170,8 @@ fn show_image(panel: &mut PanelState, path: &Path) {
174170
.map(|t| t.to_rgb8())
175171
.or_else(|| load_from_memory(&buf).ok().map(|i| i.to_rgb8()));
176172
crate::logger::log(&format!("Image loading took: {:?}", now.elapsed()));
177-
if let Some(px) = pixels {
178-
panel.mode = PanelMode::QuickView(QuickViewMode::Image(px.into_raw(), buf));
173+
if pixels.is_some() {
174+
panel.mode = PanelMode::QuickView(QuickViewMode::Image(buf));
179175
} else {
180176
panel.mode = PanelMode::QuickView(QuickViewMode::NotSupported);
181177
}
@@ -211,26 +207,15 @@ fn human_size(bytes: u64) -> String {
211207
#[cfg(test)]
212208
mod tests {
213209
use super::*;
210+
use crate::test_support::create_temp_dir;
214211
use std::fs;
215212

216-
fn create_temp_dir(prefix: &str) -> PathBuf {
217-
let mut path = std::env::temp_dir();
218-
let nanos = std::time::SystemTime::now()
219-
.duration_since(std::time::UNIX_EPOCH)
220-
.unwrap()
221-
.as_nanos();
222-
path.push(format!("{}_{}", prefix, nanos));
223-
fs::create_dir_all(&path).unwrap();
224-
path
225-
}
226-
227213
#[test]
228214
fn test_scroll_clamps_bounds() {
229215
let mut panel = PanelState::new();
230216
panel.mode = PanelMode::QuickView(QuickViewMode::Text {
231217
lines: vec!["a".to_string(); 50],
232218
start: 0,
233-
length: 50,
234219
});
235220
scroll(&mut panel, 100, 10, 1, 1);
236221
if let PanelMode::QuickView(QuickViewMode::Text { start, .. }) = panel.mode {

src/infrastructure/favorites_adapter.rs

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,79 +34,56 @@ pub fn add_favorite(items: &mut Vec<String>, path: &Path) -> bool {
3434
#[cfg(test)]
3535
mod tests {
3636
use super::*;
37+
use crate::test_support::{CwdGuard, create_temp_dir};
3738
use std::fs;
38-
use std::path::PathBuf;
3939
use std::sync::Mutex;
4040

4141
static CWD_LOCK: Mutex<()> = Mutex::new(());
4242

43-
fn create_temp_dir(prefix: &str) -> PathBuf {
44-
let mut path = std::env::temp_dir();
45-
let nanos = std::time::SystemTime::now()
46-
.duration_since(std::time::UNIX_EPOCH)
47-
.unwrap()
48-
.as_nanos();
49-
path.push(format!("{}_{}", prefix, nanos));
50-
fs::create_dir_all(&path).unwrap();
51-
path
52-
}
53-
5443
#[test]
5544
fn test_load_favorites_missing_file_returns_empty() {
5645
let _guard = CWD_LOCK.lock().unwrap();
5746
let base = create_temp_dir("favorites_missing");
58-
let original = std::env::current_dir().unwrap();
59-
std::env::set_current_dir(&base).unwrap();
47+
let _cwd = CwdGuard::change_to(&base);
6048

6149
let favorites = load_favorites();
6250
assert!(favorites.is_empty());
63-
64-
std::env::set_current_dir(original).unwrap();
6551
}
6652

6753
#[test]
6854
fn test_save_and_load_favorites_round_trip() {
6955
let _guard = CWD_LOCK.lock().unwrap();
7056
let base = create_temp_dir("favorites_roundtrip");
71-
let original = std::env::current_dir().unwrap();
72-
std::env::set_current_dir(&base).unwrap();
57+
let _cwd = CwdGuard::change_to(&base);
7358

7459
let items = vec!["/tmp/a".to_string(), "/tmp/b".to_string()];
7560
save_favorites(&items);
7661
let loaded = load_favorites();
7762
assert_eq!(loaded, items);
78-
79-
std::env::set_current_dir(original).unwrap();
8063
}
8164

8265
#[test]
8366
fn test_add_favorite_de_dupes_and_persists() {
8467
let _guard = CWD_LOCK.lock().unwrap();
8568
let base = create_temp_dir("favorites_add");
86-
let original = std::env::current_dir().unwrap();
87-
std::env::set_current_dir(&base).unwrap();
69+
let _cwd = CwdGuard::change_to(&base);
8870

8971
let mut items = vec![];
9072
let path = Path::new("/tmp/test");
9173
assert!(add_favorite(&mut items, path));
9274
assert!(!add_favorite(&mut items, path));
9375
let loaded = load_favorites();
9476
assert_eq!(loaded, vec!["/tmp/test".to_string()]);
95-
96-
std::env::set_current_dir(original).unwrap();
9777
}
9878

9979
#[test]
10080
fn test_load_favorites_ignores_empty_lines() {
10181
let _guard = CWD_LOCK.lock().unwrap();
10282
let base = create_temp_dir("favorites_empty_lines");
103-
let original = std::env::current_dir().unwrap();
104-
std::env::set_current_dir(&base).unwrap();
83+
let _cwd = CwdGuard::change_to(&base);
10584

10685
fs::write("favorites.txt", "\n/a\n\n/b\n").unwrap();
10786
let loaded = load_favorites();
10887
assert_eq!(loaded, vec!["/a".to_string(), "/b".to_string()]);
109-
110-
std::env::set_current_dir(original).unwrap();
11188
}
11289
}

src/infrastructure/fs_adapter.rs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,8 @@ impl FileSystemPort for StdFileSystem {
6969
#[cfg(test)]
7070
mod tests {
7171
use super::*;
72+
use crate::test_support::create_temp_dir;
7273
use std::fs;
73-
use std::path::PathBuf;
74-
75-
fn create_temp_dir(prefix: &str) -> PathBuf {
76-
let mut path = std::env::temp_dir();
77-
let nanos = std::time::SystemTime::now()
78-
.duration_since(std::time::UNIX_EPOCH)
79-
.unwrap()
80-
.as_nanos();
81-
path.push(format!("{}_{}", prefix, nanos));
82-
fs::create_dir_all(&path).unwrap();
83-
path
84-
}
8574

8675
#[test]
8776
fn test_list_dir_formats_entries() {

src/infrastructure/search_adapter.rs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -131,19 +131,8 @@ fn parse_rg_output(output: &str) -> Vec<String> {
131131
#[cfg(test)]
132132
mod tests {
133133
use super::*;
134+
use crate::test_support::create_temp_dir;
134135
use std::fs;
135-
use std::path::PathBuf;
136-
137-
fn create_temp_dir(prefix: &str) -> PathBuf {
138-
let mut path = std::env::temp_dir();
139-
let nanos = std::time::SystemTime::now()
140-
.duration_since(std::time::UNIX_EPOCH)
141-
.unwrap()
142-
.as_nanos();
143-
path.push(format!("{}_{}", prefix, nanos));
144-
fs::create_dir_all(&path).unwrap();
145-
path
146-
}
147136

148137
#[test]
149138
fn test_parse_fd_output_marks_directories() {

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ pub mod domain;
44
pub mod infrastructure;
55
pub mod logger;
66
pub mod presentation;
7+
pub mod test_support;

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ mod domain;
77
mod infrastructure;
88
mod logger;
99
mod presentation;
10+
#[cfg(test)]
11+
mod test_support;
1012

1113
use application::AppState;
1214
use application::use_cases::navigate;

0 commit comments

Comments
 (0)