Skip to content

Commit b302d0e

Browse files
committed
Indexing: Smart size display with tooltips
- Expose both logical and physical sizes to the frontend via `FileEntry.physicalSize`, `FileEntry.recursivePhysicalSize`, `DirStats.recursivePhysicalSize`, and `ListingStats` physical totals - Add `listing.sizeDisplay` setting: Smart (default, shows `min(logical, physical)`), Content, On disk - Add dual-size tooltips on all size displays: "Content: X · On disk: Y" when sizes differ >1% - `SelectionInfo` selection summary respects the display mode - `BriefList` dir tooltips now use shared `buildDirSizeTooltip` (was inline duplicate) - `getDisplaySize` falls back to logical when physical is unavailable (MTP, broken entries)
1 parent 3da61f3 commit b302d0e

37 files changed

Lines changed: 479 additions & 49 deletions

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ fn make_entry(name: &str, is_dir: bool) -> FileEntry {
2121
is_directory: is_dir,
2222
is_symlink: false,
2323
size: if is_dir { None } else { Some(100) },
24+
physical_size: None,
2425
modified_at: Some(1_700_000_000),
2526
created_at: Some(1_700_000_000),
2627
added_at: None,
@@ -31,6 +32,7 @@ fn make_entry(name: &str, is_dir: bool) -> FileEntry {
3132
icon_id: if is_dir { "dir".to_string() } else { "file".to_string() },
3233
extended_metadata_loaded: true,
3334
recursive_size: None,
35+
recursive_physical_size: None,
3436
recursive_file_count: None,
3537
recursive_dir_count: None,
3638
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ pub struct FileEntry {
7474
pub is_directory: bool,
7575
pub is_symlink: bool,
7676
pub size: Option<u64>,
77+
/// Physical size on disk in bytes (st_blocks * 512 on Unix, same as size on other platforms)
78+
#[serde(default, skip_serializing_if = "Option::is_none")]
79+
pub physical_size: Option<u64>,
7780
pub modified_at: Option<u64>,
7881
pub created_at: Option<u64>,
7982
/// When the file was added to its current directory (macOS only)
@@ -91,6 +94,9 @@ pub struct FileEntry {
9194
/// Recursive size in bytes (from drive index, None if not indexed)
9295
#[serde(default, skip_serializing_if = "Option::is_none")]
9396
pub recursive_size: Option<u64>,
97+
/// Recursive physical size on disk in bytes (from drive index, None if not indexed)
98+
#[serde(default, skip_serializing_if = "Option::is_none")]
99+
pub recursive_physical_size: Option<u64>,
94100
/// Recursive file count (from drive index, None if not indexed)
95101
#[serde(default, skip_serializing_if = "Option::is_none")]
96102
pub recursive_file_count: Option<u64>,

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

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -522,14 +522,18 @@ pub struct ListingStats {
522522
/// Not including directories.
523523
pub total_files: usize,
524524
pub total_dirs: usize,
525-
/// Total size in bytes (files + directory recursive sizes).
525+
/// Total logical size in bytes (files + directory recursive sizes).
526526
pub total_size: u64,
527+
/// Total physical (on-disk) size in bytes. Mirrors `total_size` but uses `physical_size` / `recursive_physical_size`.
528+
pub total_physical_size: u64,
527529
/// Present only if `selected_indices` was provided.
528530
pub selected_files: Option<usize>,
529531
/// Present only if `selected_indices` was provided.
530532
pub selected_dirs: Option<usize>,
531-
/// Total size of selected entries in bytes (files + directory recursive sizes). Present only if `selected_indices` was provided.
533+
/// Total logical size of selected entries in bytes. Present only if `selected_indices` was provided.
532534
pub selected_size: Option<u64>,
535+
/// Total physical size of selected entries in bytes. Present only if `selected_indices` was provided.
536+
pub selected_physical_size: Option<u64>,
533537
}
534538

535539
/// Gets statistics about a cached listing.
@@ -558,26 +562,35 @@ pub fn get_listing_stats(
558562
let mut total_files: usize = 0;
559563
let mut total_dirs: usize = 0;
560564
let mut total_size: u64 = 0;
565+
let mut total_physical_size: u64 = 0;
561566

562567
for entry in &visible_entries {
563568
if entry.is_directory {
564569
total_dirs += 1;
565570
if let Some(size) = entry.recursive_size {
566571
total_size += size;
567572
}
573+
if let Some(size) = entry.recursive_physical_size {
574+
total_physical_size += size;
575+
}
568576
} else {
569577
total_files += 1;
570578
if let Some(size) = entry.size {
571579
total_size += size;
572580
}
581+
if let Some(size) = entry.physical_size {
582+
total_physical_size += size;
583+
}
573584
}
574585
}
575586

576587
// Calculate selection stats if indices provided
577-
let (selected_files, selected_dirs, selected_size) = if let Some(indices) = selected_indices {
588+
let (selected_files, selected_dirs, selected_size, selected_physical_size) = if let Some(indices) = selected_indices
589+
{
578590
let mut sel_files: usize = 0;
579591
let mut sel_dirs: usize = 0;
580592
let mut sel_size: u64 = 0;
593+
let mut sel_physical_size: u64 = 0;
581594

582595
for &idx in indices {
583596
if let Some(entry) = visible_entries.get(idx) {
@@ -586,27 +599,35 @@ pub fn get_listing_stats(
586599
if let Some(size) = entry.recursive_size {
587600
sel_size += size;
588601
}
602+
if let Some(size) = entry.recursive_physical_size {
603+
sel_physical_size += size;
604+
}
589605
} else {
590606
sel_files += 1;
591607
if let Some(size) = entry.size {
592608
sel_size += size;
593609
}
610+
if let Some(size) = entry.physical_size {
611+
sel_physical_size += size;
612+
}
594613
}
595614
}
596615
}
597616

598-
(Some(sel_files), Some(sel_dirs), Some(sel_size))
617+
(Some(sel_files), Some(sel_dirs), Some(sel_size), Some(sel_physical_size))
599618
} else {
600-
(None, None, None)
619+
(None, None, None, None)
601620
};
602621

603622
Ok(ListingStats {
604623
total_files,
605624
total_dirs,
606625
total_size,
626+
total_physical_size,
607627
selected_files,
608628
selected_dirs,
609629
selected_size,
630+
selected_physical_size,
610631
})
611632
}
612633

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ fn list_directory_core_impl(
108108
is_directory: false,
109109
is_symlink,
110110
size: None,
111+
physical_size: None,
111112
modified_at: None,
112113
created_at: None,
113114
added_at: None,
@@ -122,6 +123,7 @@ fn list_directory_core_impl(
122123
},
123124
extended_metadata_loaded: true, // Nothing to load for broken entries
124125
recursive_size: None,
126+
recursive_physical_size: None,
125127
recursive_file_count: None,
126128
recursive_dir_count: None,
127129
});
@@ -199,12 +201,19 @@ pub fn get_single_entry(path: &Path) -> Result<FileEntry, std::io::Error> {
199201
let owner = get_owner_name(uid);
200202
let group = get_group_name(gid);
201203

204+
let physical_size = if metadata.is_file() {
205+
Some(metadata.blocks() * 512)
206+
} else {
207+
None
208+
};
209+
202210
Ok(FileEntry {
203211
name: name.clone(),
204212
path: path.to_string_lossy().to_string(),
205213
is_directory: is_dir,
206214
is_symlink,
207215
size: if metadata.is_file() { Some(metadata.len()) } else { None },
216+
physical_size,
208217
modified_at: modified,
209218
created_at: created,
210219
added_at: None,
@@ -215,6 +224,7 @@ pub fn get_single_entry(path: &Path) -> Result<FileEntry, std::io::Error> {
215224
icon_id: get_icon_id(is_dir, is_symlink, &name),
216225
extended_metadata_loaded: false,
217226
recursive_size: None,
227+
recursive_physical_size: None,
218228
recursive_file_count: None,
219229
recursive_dir_count: None,
220230
})
@@ -258,12 +268,19 @@ pub(crate) fn process_dir_entry(entry: &fs::DirEntry) -> Option<FileEntry> {
258268
let owner = get_owner_name(uid);
259269
let group = get_group_name(gid);
260270

271+
let physical_size = if metadata.is_file() {
272+
Some(metadata.blocks() * 512)
273+
} else {
274+
None
275+
};
276+
261277
Some(FileEntry {
262278
name: name.clone(),
263279
path: entry.path().to_string_lossy().to_string(),
264280
is_directory: is_dir,
265281
is_symlink,
266282
size: if metadata.is_file() { Some(metadata.len()) } else { None },
283+
physical_size,
267284
modified_at: modified,
268285
created_at: created,
269286
added_at: None,
@@ -274,6 +291,7 @@ pub(crate) fn process_dir_entry(entry: &fs::DirEntry) -> Option<FileEntry> {
274291
icon_id: get_icon_id(is_dir, is_symlink, &name),
275292
extended_metadata_loaded: false,
276293
recursive_size: None,
294+
recursive_physical_size: None,
277295
recursive_file_count: None,
278296
recursive_dir_count: None,
279297
})

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ fn make_entry(name: &str, is_dir: bool, size: Option<u64>, modified: Option<u64>
1515
is_directory: is_dir,
1616
is_symlink: false,
1717
size,
18+
physical_size: None,
1819
modified_at: modified,
1920
created_at: modified, // Use same value for simplicity
2021
added_at: None,
@@ -25,6 +26,7 @@ fn make_entry(name: &str, is_dir: bool, size: Option<u64>, modified: Option<u64>
2526
icon_id: if is_dir { "dir".to_string() } else { "file".to_string() },
2627
extended_metadata_loaded: true,
2728
recursive_size: None,
29+
recursive_physical_size: None,
2830
recursive_file_count: None,
2931
recursive_dir_count: None,
3032
}
@@ -402,6 +404,7 @@ fn make_symlink(name: &str, size: Option<u64>) -> FileEntry {
402404
is_directory: false,
403405
is_symlink: true,
404406
size,
407+
physical_size: None,
405408
modified_at: None,
406409
created_at: None,
407410
added_at: None,
@@ -412,6 +415,7 @@ fn make_symlink(name: &str, size: Option<u64>) -> FileEntry {
412415
icon_id: "symlink".to_string(),
413416
extended_metadata_loaded: true,
414417
recursive_size: None,
418+
recursive_physical_size: None,
415419
recursive_file_count: None,
416420
recursive_dir_count: None,
417421
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ impl MockFileSystemProvider {
2828
is_directory: is_dir,
2929
is_symlink: i % 50 == 0, // Every 50th is a symlink for testing
3030
size: Some(1024 * (i as u64)),
31+
physical_size: None,
3132
modified_at: Some(1640000000 + i as u64),
3233
created_at: Some(1639000000 + i as u64),
3334
added_at: Some(1638000000 + i as u64),
@@ -42,6 +43,7 @@ impl MockFileSystemProvider {
4243
},
4344
extended_metadata_loaded: true,
4445
recursive_size: None,
46+
recursive_physical_size: None,
4547
recursive_file_count: None,
4648
recursive_dir_count: None,
4749
}
@@ -71,6 +73,7 @@ mod tests {
7173
is_directory: false,
7274
is_symlink: false,
7375
size: Some(1024),
76+
physical_size: None,
7477
modified_at: Some(1640000000),
7578
created_at: Some(1639000000),
7679
added_at: Some(1638000000),
@@ -81,6 +84,7 @@ mod tests {
8184
icon_id: "ext:txt".to_string(),
8285
extended_metadata_loaded: true,
8386
recursive_size: None,
87+
recursive_physical_size: None,
8488
recursive_file_count: None,
8589
recursive_dir_count: None,
8690
},
@@ -90,6 +94,7 @@ mod tests {
9094
is_directory: true,
9195
is_symlink: false,
9296
size: None,
97+
physical_size: None,
9398
modified_at: Some(1640000000),
9499
created_at: Some(1639000000),
95100
added_at: Some(1638000000),
@@ -100,6 +105,7 @@ mod tests {
100105
icon_id: "dir".to_string(),
101106
extended_metadata_loaded: true,
102107
recursive_size: None,
108+
recursive_physical_size: None,
103109
recursive_file_count: None,
104110
recursive_dir_count: None,
105111
},

apps/desktop/src-tauri/src/file_system/volume/in_memory.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ impl InMemoryVolume {
7878
is_directory: is_dir,
7979
is_symlink: i % 50 == 0,
8080
size: Some(1024 * (i as u64)),
81+
physical_size: None,
8182
modified_at: Some(1_640_000_000 + i as u64),
8283
created_at: Some(1_639_000_000 + i as u64),
8384
added_at: None,
@@ -92,6 +93,7 @@ impl InMemoryVolume {
9293
},
9394
extended_metadata_loaded: true,
9495
recursive_size: None,
96+
recursive_physical_size: None,
9597
recursive_file_count: None,
9698
recursive_dir_count: None,
9799
}
@@ -221,6 +223,7 @@ impl Volume for InMemoryVolume {
221223
is_directory: false,
222224
is_symlink: false,
223225
size: Some(content.len() as u64),
226+
physical_size: None,
224227
modified_at: Some(Self::now_secs()),
225228
created_at: Some(Self::now_secs()),
226229
added_at: None,
@@ -231,6 +234,7 @@ impl Volume for InMemoryVolume {
231234
icon_id: "file".to_string(),
232235
extended_metadata_loaded: true,
233236
recursive_size: None,
237+
recursive_physical_size: None,
234238
recursive_file_count: None,
235239
recursive_dir_count: None,
236240
};
@@ -265,6 +269,7 @@ impl Volume for InMemoryVolume {
265269
is_directory: true,
266270
is_symlink: false,
267271
size: None,
272+
physical_size: None,
268273
modified_at: Some(Self::now_secs()),
269274
created_at: Some(Self::now_secs()),
270275
added_at: None,
@@ -275,6 +280,7 @@ impl Volume for InMemoryVolume {
275280
icon_id: "dir".to_string(),
276281
extended_metadata_loaded: true,
277282
recursive_size: None,
283+
recursive_physical_size: None,
278284
recursive_file_count: None,
279285
recursive_dir_count: None,
280286
};

0 commit comments

Comments
 (0)