Skip to content

Commit a7dd8ca

Browse files
committed
Make directories sortable by size
- Make them actually sort by size if the sort order is that for files - Add a "Directory sort setting" to settings to turn this off
1 parent a768c03 commit a7dd8ca

23 files changed

Lines changed: 822 additions & 66 deletions

apps/desktop/src-tauri/src/commands/file_system.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ use crate::file_system::write_operations::{
77
resolve_write_conflict as ops_resolve_write_conflict, start_scan_preview as ops_start_scan_preview,
88
};
99
use crate::file_system::{
10-
FileEntry, ListingStartResult, ListingStats, OperationStatus, OperationSummary, ResortResult, ScanConflict,
11-
SortColumn, SortOrder, StreamingListingStartResult, VolumeCopyConfig, VolumeCopyScanResult, WriteOperationConfig,
12-
WriteOperationError, WriteOperationStartResult, cancel_listing as ops_cancel_listing,
10+
DirectorySortMode, FileEntry, ListingStartResult, ListingStats, OperationStatus, OperationSummary, ResortResult,
11+
ScanConflict, SortColumn, SortOrder, StreamingListingStartResult, VolumeCopyConfig, VolumeCopyScanResult,
12+
WriteOperationConfig, WriteOperationError, WriteOperationStartResult, cancel_listing as ops_cancel_listing,
1313
cancel_write_operation as ops_cancel_write_operation, copy_between_volumes as ops_copy_between_volumes,
1414
copy_files_start as ops_copy_files_start, delete_files_start as ops_delete_files_start,
1515
find_file_index as ops_find_file_index, get_file_at as ops_get_file_at, get_file_range as ops_get_file_range,
@@ -132,23 +132,27 @@ pub fn list_directory_start(
132132
include_hidden: bool,
133133
sort_by: SortColumn,
134134
sort_order: SortOrder,
135+
directory_sort_mode: Option<DirectorySortMode>,
135136
) -> Result<ListingStartResult, String> {
136137
let expanded_path = expand_tilde(&path);
137138
let path_buf = PathBuf::from(&expanded_path);
138-
ops_list_directory_start_with_volume("root", &path_buf, include_hidden, sort_by, sort_order)
139+
let dir_sort_mode = directory_sort_mode.unwrap_or_default();
140+
ops_list_directory_start_with_volume("root", &path_buf, include_hidden, sort_by, sort_order, dir_sort_mode)
139141
.map_err(|e| format!("Failed to start directory listing '{}': {}", path, e))
140142
}
141143

142144
/// Returns immediately; reads in background.
143145
/// Emits listing-progress, listing-complete, listing-error, listing-cancelled.
144146
#[tauri::command]
147+
#[allow(clippy::too_many_arguments, reason = "Tauri commands require top-level arguments")]
145148
pub async fn list_directory_start_streaming(
146149
app: tauri::AppHandle,
147150
volume_id: String,
148151
path: String,
149152
include_hidden: bool,
150153
sort_by: SortColumn,
151154
sort_order: SortOrder,
155+
directory_sort_mode: Option<DirectorySortMode>,
152156
listing_id: String,
153157
) -> Result<StreamingListingStartResult, String> {
154158
// Only expand tilde for local volumes (not MTP)
@@ -158,13 +162,15 @@ pub async fn list_directory_start_streaming(
158162
path.clone()
159163
};
160164
let path_buf = PathBuf::from(&expanded_path);
165+
let dir_sort_mode = directory_sort_mode.unwrap_or_default();
161166
ops_list_directory_start_streaming(
162167
app,
163168
&volume_id,
164169
&path_buf,
165170
include_hidden,
166171
sort_by,
167172
sort_order,
173+
dir_sort_mode,
168174
listing_id,
169175
)
170176
.await
@@ -176,11 +182,13 @@ pub fn cancel_listing(listing_id: String) {
176182
ops_cancel_listing(&listing_id);
177183
}
178184

185+
#[allow(clippy::too_many_arguments, reason = "Tauri commands require top-level arguments")]
179186
#[tauri::command]
180187
pub fn resort_listing(
181188
listing_id: String,
182189
sort_by: SortColumn,
183190
sort_order: SortOrder,
191+
directory_sort_mode: Option<DirectorySortMode>,
184192
cursor_filename: Option<String>,
185193
include_hidden: bool,
186194
selected_indices: Option<Vec<usize>>,
@@ -190,6 +198,7 @@ pub fn resort_listing(
190198
&listing_id,
191199
sort_by,
192200
sort_order,
201+
directory_sort_mode.unwrap_or_default(),
193202
cursor_filename.as_deref(),
194203
include_hidden,
195204
selected_indices.as_deref(),

apps/desktop/src-tauri/src/file_system/listing/caching.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::path::PathBuf;
55
use std::sync::{LazyLock, RwLock};
66

77
use crate::file_system::listing::metadata::FileEntry;
8-
use crate::file_system::listing::sorting::{SortColumn, SortOrder};
8+
use crate::file_system::listing::sorting::{DirectorySortMode, SortColumn, SortOrder};
99

1010
/// Cache for directory listings (on-demand virtual scrolling).
1111
/// Key: listing_id, Value: cached listing with all entries.
@@ -29,6 +29,8 @@ pub(crate) struct CachedListing {
2929
pub sort_by: SortColumn,
3030
/// Current sort order
3131
pub sort_order: SortOrder,
32+
/// How directories are sorted relative to the current sort column
33+
pub directory_sort_mode: DirectorySortMode,
3234
}
3335

3436
/// Cached directory listing for on-demand virtual scrolling.
@@ -44,4 +46,6 @@ pub(crate) struct CachedListing {
4446
pub sort_by: SortColumn,
4547
/// Current sort order
4648
pub sort_order: SortOrder,
49+
/// How directories are sorted relative to the current sort column
50+
pub directory_sort_mode: DirectorySortMode,
4751
}

apps/desktop/src-tauri/src/file_system/listing/hidden_files_test.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
use super::caching::{CachedListing, LISTING_CACHE};
77
use super::operations::{find_file_index, get_file_at, get_file_range, get_total_count, list_directory_end};
8+
use super::sorting::DirectorySortMode;
89
use super::{FileEntry, SortColumn, SortOrder};
910
use crate::file_system::volume::{InMemoryVolume, Volume};
1011
use std::path::Path;
@@ -69,6 +70,7 @@ fn test_get_total_count_with_hidden_includes_all() {
6970
entries,
7071
sort_by: SortColumn::Name,
7172
sort_order: SortOrder::Ascending,
73+
directory_sort_mode: DirectorySortMode::LikeFiles,
7274
},
7375
);
7476
}
@@ -99,6 +101,7 @@ fn test_get_total_count_without_hidden_excludes_dot_files() {
99101
entries,
100102
sort_by: SortColumn::Name,
101103
sort_order: SortOrder::Ascending,
104+
directory_sort_mode: DirectorySortMode::LikeFiles,
102105
},
103106
);
104107
}
@@ -133,6 +136,7 @@ fn test_get_file_range_with_hidden_returns_all() {
133136
entries,
134137
sort_by: SortColumn::Name,
135138
sort_order: SortOrder::Ascending,
139+
directory_sort_mode: DirectorySortMode::LikeFiles,
136140
},
137141
);
138142
}
@@ -168,6 +172,7 @@ fn test_get_file_range_without_hidden_excludes_dot_files() {
168172
entries,
169173
sort_by: SortColumn::Name,
170174
sort_order: SortOrder::Ascending,
175+
directory_sort_mode: DirectorySortMode::LikeFiles,
171176
},
172177
);
173178
}
@@ -207,6 +212,7 @@ fn test_get_file_range_pagination_respects_hidden_filter() {
207212
entries,
208213
sort_by: SortColumn::Name,
209214
sort_order: SortOrder::Ascending,
215+
directory_sort_mode: DirectorySortMode::LikeFiles,
210216
},
211217
);
212218
}
@@ -253,6 +259,7 @@ fn test_find_file_index_hidden_file_with_hidden_enabled() {
253259
entries,
254260
sort_by: SortColumn::Name,
255261
sort_order: SortOrder::Ascending,
262+
directory_sort_mode: DirectorySortMode::LikeFiles,
256263
},
257264
);
258265
}
@@ -282,6 +289,7 @@ fn test_find_file_index_hidden_file_with_hidden_disabled() {
282289
entries,
283290
sort_by: SortColumn::Name,
284291
sort_order: SortOrder::Ascending,
292+
directory_sort_mode: DirectorySortMode::LikeFiles,
285293
},
286294
);
287295
}
@@ -311,6 +319,7 @@ fn test_find_file_index_visible_file_index_changes_with_hidden_setting() {
311319
entries,
312320
sort_by: SortColumn::Name,
313321
sort_order: SortOrder::Ascending,
322+
directory_sort_mode: DirectorySortMode::LikeFiles,
314323
},
315324
);
316325
}
@@ -357,6 +366,7 @@ fn test_get_file_at_index_0_with_hidden_enabled() {
357366
entries,
358367
sort_by: SortColumn::Name,
359368
sort_order: SortOrder::Ascending,
369+
directory_sort_mode: DirectorySortMode::LikeFiles,
360370
},
361371
);
362372
}
@@ -392,6 +402,7 @@ fn test_get_file_at_index_0_with_hidden_disabled() {
392402
entries,
393403
sort_by: SortColumn::Name,
394404
sort_order: SortOrder::Ascending,
405+
directory_sort_mode: DirectorySortMode::LikeFiles,
395406
},
396407
);
397408
}
@@ -436,6 +447,7 @@ fn test_directory_with_only_hidden_files() {
436447
entries,
437448
sort_by: SortColumn::Name,
438449
sort_order: SortOrder::Ascending,
450+
directory_sort_mode: DirectorySortMode::LikeFiles,
439451
},
440452
);
441453
}
@@ -470,6 +482,7 @@ fn test_directory_with_no_hidden_files() {
470482
entries,
471483
sort_by: SortColumn::Name,
472484
sort_order: SortOrder::Ascending,
485+
directory_sort_mode: DirectorySortMode::LikeFiles,
473486
},
474487
);
475488
}

apps/desktop/src-tauri/src/file_system/listing/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub use operations::{
1515
get_max_filename_width, get_total_count, list_directory_end, list_directory_start_with_volume, resort_listing,
1616
};
1717
pub use reading::{get_single_entry, list_directory_core};
18-
pub use sorting::{SortColumn, SortOrder};
18+
pub use sorting::{DirectorySortMode, SortColumn, SortOrder};
1919
pub use streaming::{StreamingListingStartResult, cancel_listing, list_directory_start_streaming};
2020

2121
// macOS-only exports (used by drag operations)

apps/desktop/src-tauri/src/file_system/listing/operations.rs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use uuid::Uuid;
1212
use crate::benchmark;
1313
use crate::file_system::listing::caching::{CachedListing, LISTING_CACHE};
1414
use crate::file_system::listing::metadata::FileEntry;
15-
use crate::file_system::listing::sorting::{SortColumn, SortOrder, sort_entries};
15+
use crate::file_system::listing::sorting::{DirectorySortMode, SortColumn, SortOrder, sort_entries};
1616
use crate::file_system::watcher::{start_watching, stop_watching};
1717

1818
/// Returns true if the entry is not a hidden dotfile.
@@ -39,7 +39,14 @@ pub struct ListingStartResult {
3939
/// Reads the directory once, caches it, and returns listing ID + total count.
4040
/// Frontend then fetches visible ranges on demand via `get_file_range`.
4141
pub fn list_directory_start(path: &Path, include_hidden: bool) -> Result<ListingStartResult, std::io::Error> {
42-
list_directory_start_with_volume("root", path, include_hidden, SortColumn::Name, SortOrder::Ascending)
42+
list_directory_start_with_volume(
43+
"root",
44+
path,
45+
include_hidden,
46+
SortColumn::Name,
47+
SortOrder::Ascending,
48+
DirectorySortMode::LikeFiles,
49+
)
4350
}
4451

4552
/// Starts a new directory listing using a specific volume.
@@ -51,6 +58,7 @@ pub fn list_directory_start_with_volume(
5158
include_hidden: bool,
5259
sort_by: SortColumn,
5360
sort_order: SortOrder,
61+
dir_sort_mode: DirectorySortMode,
5462
) -> Result<ListingStartResult, std::io::Error> {
5563
// Reset benchmark epoch for this navigation
5664
benchmark::reset_epoch();
@@ -80,9 +88,13 @@ pub fn list_directory_start_with_volume(
8088
all_entries.iter().filter(|e| is_visible(e)).count()
8189
};
8290

83-
// Sort the entries
91+
// Enrich directory entries with index data (recursive_size etc.) before sorting,
92+
// so that sort-by-size works correctly for directories.
8493
let mut all_entries = all_entries;
85-
sort_entries(&mut all_entries, sort_by, sort_order);
94+
crate::indexing::enrich_entries_with_index(&mut all_entries);
95+
96+
// Sort the entries
97+
sort_entries(&mut all_entries, sort_by, sort_order, dir_sort_mode);
8698

8799
// Cache the entries FIRST (watcher will read from here)
88100
if let Ok(mut cache) = LISTING_CACHE.write() {
@@ -94,6 +106,7 @@ pub fn list_directory_start_with_volume(
94106
entries: all_entries.clone(),
95107
sort_by,
96108
sort_order,
109+
directory_sort_mode: dir_sort_mode,
97110
},
98111
);
99112
}
@@ -318,10 +331,15 @@ pub struct ResortResult {
318331
/// Re-sorts an existing cached listing in-place.
319332
///
320333
/// More efficient than creating a new listing when you just want to change the sort order.
334+
#[allow(
335+
clippy::too_many_arguments,
336+
reason = "Resort requires sort params, cursor tracking, and selection state"
337+
)]
321338
pub fn resort_listing(
322339
listing_id: &str,
323340
sort_by: SortColumn,
324341
sort_order: SortOrder,
342+
dir_sort_mode: DirectorySortMode,
325343
cursor_filename: Option<&str>,
326344
include_hidden: bool,
327345
selected_indices: Option<&[usize]>,
@@ -351,9 +369,13 @@ pub fn resort_listing(
351369
})
352370
};
353371

372+
// Refresh index data before re-sorting (cache entries may not have fresh sizes)
373+
crate::indexing::enrich_entries_with_index(&mut listing.entries);
374+
354375
// Re-sort the entries
355-
sort_entries(&mut listing.entries, sort_by, sort_order);
376+
sort_entries(&mut listing.entries, sort_by, sort_order, dir_sort_mode);
356377
listing.sort_by = sort_by;
378+
listing.directory_sort_mode = dir_sort_mode;
357379
listing.sort_order = sort_order;
358380

359381
// Find the new cursor position
@@ -417,7 +439,13 @@ pub(crate) fn update_listing_entries(listing_id: &str, entries: Vec<FileEntry>)
417439
&& let Some(listing) = cache.get_mut(listing_id)
418440
{
419441
let mut entries = entries;
420-
sort_entries(&mut entries, listing.sort_by, listing.sort_order);
442+
crate::indexing::enrich_entries_with_index(&mut entries);
443+
sort_entries(
444+
&mut entries,
445+
listing.sort_by,
446+
listing.sort_order,
447+
listing.directory_sort_mode,
448+
);
421449
listing.entries = entries;
422450
}
423451
}

apps/desktop/src-tauri/src/file_system/listing/reading.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use std::path::Path;
1414

1515
use crate::benchmark;
1616
use crate::file_system::listing::metadata::{ExtendedMetadata, FileEntry, get_group_name, get_icon_id, get_owner_name};
17-
use crate::file_system::listing::sorting::{SortColumn, SortOrder, sort_entries};
17+
use crate::file_system::listing::sorting::{DirectorySortMode, SortColumn, SortOrder, sort_entries};
1818

1919
/// Lists the contents of a directory with full metadata (including macOS extended metadata).
2020
///
@@ -112,7 +112,12 @@ pub fn list_directory_core(path: &Path) -> Result<Vec<FileEntry>, std::io::Error
112112

113113
// Sort: directories first, then files, both alphabetically (using natural sort)
114114
benchmark::log_event("sort START");
115-
sort_entries(&mut entries, SortColumn::Name, SortOrder::Ascending);
115+
sort_entries(
116+
&mut entries,
117+
SortColumn::Name,
118+
SortOrder::Ascending,
119+
DirectorySortMode::LikeFiles,
120+
);
116121
benchmark::log_event("sort END");
117122

118123
let total_time = overall_start.elapsed();

0 commit comments

Comments
 (0)