Skip to content

Commit 5e10fa9

Browse files
committed
Drive indexing: wire it up WIP
Now it displays the sizes! Yay!
1 parent 34c34c2 commit 5e10fa9

12 files changed

Lines changed: 744 additions & 28 deletions

File tree

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,15 +151,19 @@ pub fn get_file_range(
151151
.ok_or_else(|| format!("Listing not found: {}", listing_id))?;
152152

153153
// Filter entries if not including hidden
154-
if include_hidden {
154+
let mut entries: Vec<FileEntry> = if include_hidden {
155155
let end = (start + count).min(listing.entries.len());
156-
Ok(listing.entries[start..end].to_vec())
156+
listing.entries[start..end].to_vec()
157157
} else {
158158
// Need to filter and then slice
159159
let visible: Vec<&FileEntry> = listing.entries.iter().filter(|e| is_visible(e)).collect();
160160
let end = (start + count).min(visible.len());
161-
Ok(visible[start..end].iter().cloned().cloned().collect())
162-
}
161+
visible[start..end].iter().cloned().cloned().collect()
162+
};
163+
164+
crate::indexing::enrich_entries_with_index(&mut entries);
165+
166+
Ok(entries)
163167
}
164168

165169
/// Gets total count of entries in a cached listing.

apps/desktop/src/lib/file-explorer/pane/DualPaneExplorer.svelte

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
getSelfDragFileInfos,
7777
endSelfDragSession,
7878
} from '../drag/drag-drop'
79+
import { initIndexEvents, prioritizeDir, cancelNavPriority } from '$lib/indexing/index'
7980
import { resolveDropTarget } from '../drag/drop-target-hit-testing'
8081
import DragOverlay from '../drag/DragOverlay.svelte'
8182
import { showOverlay, updateOverlay, hideOverlay, type OverlayFileInfo } from '../drag/drag-overlay.svelte.js'
@@ -118,6 +119,7 @@
118119
let unlistenDragDrop: UnlistenFn | undefined
119120
let unlistenDragImageSize: UnlistenFn | undefined
120121
let unlistenDragModifiers: UnlistenFn | undefined
122+
let unlistenIndexEvents: UnlistenFn | undefined
121123
122124
// Drag image size from the source app (macOS only, via swizzle).
123125
// If the source provides a large preview (like Finder), we suppress our overlay.
@@ -275,10 +277,18 @@
275277
// --- Unified handler functions ---
276278
277279
function handlePathChange(pane: 'left' | 'right', path: string) {
280+
const oldPath = getPanePath(pane)
278281
setPanePath(pane, path)
279282
setPaneHistory(pane, pushPath(getPaneHistory(pane), path))
280283
void saveAppStatus({ [paneKey(pane, 'path')]: path })
281284
void saveLastUsedPathForVolume(getPaneVolumeId(pane), path)
285+
286+
// Update index priorities: cancel old dir, prioritize new dir
287+
if (oldPath !== path) {
288+
void cancelNavPriority(oldPath)
289+
void prioritizeDir(path, 'current_dir')
290+
}
291+
282292
containerElement?.focus()
283293
}
284294
@@ -331,7 +341,8 @@
331341
volumePath: string,
332342
targetPath: string,
333343
) {
334-
void saveLastUsedPathForVolume(getPaneVolumeId(pane), getPanePath(pane))
344+
const oldPath = getPanePath(pane)
345+
void saveLastUsedPathForVolume(getPaneVolumeId(pane), oldPath)
335346
336347
if (!volumes.find((v) => v.id === volumeId)) {
337348
volumes = await listVolumes()
@@ -343,6 +354,10 @@
343354
otherPanePath: getPanePath(other),
344355
})
345356
357+
// Update index priorities: cancel old dir, prioritize new dir
358+
void cancelNavPriority(oldPath)
359+
void prioritizeDir(pathToNavigate, 'current_dir')
360+
346361
setPaneVolumeId(pane, volumeId)
347362
setPanePath(pane, pathToNavigate)
348363
setPaneHistory(pane, push(getPaneHistory(pane), { volumeId, path: pathToNavigate }))
@@ -652,6 +667,44 @@
652667
dropTargetFolderEl = null
653668
}
654669
670+
/** Ensures a path ends with '/' for correct prefix matching. */
671+
function ensureTrailingSlash(path: string): string {
672+
return path.endsWith('/') ? path : path + '/'
673+
}
674+
675+
/** Called when the drive index updates directory stats. Re-fetches panes whose visible entries changed. */
676+
function handleIndexDirUpdated(paths: string[]) {
677+
const leftDir = ensureTrailingSlash(leftPath)
678+
const rightDir = ensureTrailingSlash(rightPath)
679+
680+
let refreshLeft = false
681+
let refreshRight = false
682+
683+
for (const updatedPath of paths) {
684+
// An updated path is relevant if it is a direct child or deeper descendant
685+
// whose parent is displayed in the pane's current directory.
686+
// We check if parentOf(updatedPath) === panePath (i.e. the updated path itself
687+
// is a direct child entry visible in the listing).
688+
const updatedWithSlash = ensureTrailingSlash(updatedPath)
689+
if (!refreshLeft && updatedWithSlash.startsWith(leftDir) && updatedWithSlash !== leftDir) {
690+
refreshLeft = true
691+
}
692+
if (!refreshRight && updatedWithSlash.startsWith(rightDir) && updatedWithSlash !== rightDir) {
693+
refreshRight = true
694+
}
695+
if (refreshLeft && refreshRight) break
696+
}
697+
698+
if (refreshLeft) {
699+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
700+
leftPaneRef?.refreshView?.()
701+
}
702+
if (refreshRight) {
703+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
704+
rightPaneRef?.refreshView?.()
705+
}
706+
}
707+
655708
function handleResizeForDevTools() {
656709
void recalculateWebviewOffset()
657710
}
@@ -780,6 +833,13 @@
780833
setAltHeld(event.payload.altHeld)
781834
})
782835
836+
// Listen for index directory updates to refresh panes when sizes change
837+
unlistenIndexEvents = await initIndexEvents(handleIndexDirUpdated)
838+
839+
// Prioritize scanning the initial directories of both panes
840+
void prioritizeDir(leftPath, 'current_dir')
841+
void prioritizeDir(rightPath, 'current_dir')
842+
783843
// Register drag-and-drop target handler for external and pane-to-pane drops
784844
unlistenDragDrop = await getCurrentWebview().onDragDropEvent((event) => {
785845
const { type } = event.payload
@@ -844,9 +904,16 @@
844904
newHistory: NavigationHistory,
845905
targetPath: string,
846906
) {
907+
const oldPath = getPanePath(pane)
847908
const entry = getCurrentEntry(newHistory)
848909
const paneRef = getPaneRef(pane)
849910
911+
// Update index priorities: cancel old dir, prioritize new dir
912+
if (oldPath !== targetPath) {
913+
void cancelNavPriority(oldPath)
914+
void prioritizeDir(targetPath, 'current_dir')
915+
}
916+
850917
setPaneHistory(pane, newHistory)
851918
setPanePath(pane, targetPath)
852919
if (entry.volumeId !== getPaneVolumeId(pane)) {
@@ -907,6 +974,7 @@
907974
unlistenDragImageSize?.()
908975
unlistenDragModifiers?.()
909976
unlistenDragDrop?.()
977+
unlistenIndexEvents?.()
910978
cleanupNetworkDiscovery()
911979
stopModifierTracking()
912980
window.removeEventListener('resize', handleResizeForDevTools) // No-op in non-dev, safe to always call

apps/desktop/src/lib/file-explorer/pane/FilePane.svelte

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
import * as benchmark from '$lib/benchmark'
7979
import { handleNavigationShortcut } from '../navigation/keyboard-shortcuts'
8080
import { resolveValidPath } from '../navigation/path-navigation'
81+
import { prioritizeDir } from '$lib/indexing/index'
8182
8283
interface Props {
8384
initialPath: string
@@ -1247,6 +1248,13 @@
12471248
if (e.key === ' ') {
12481249
e.preventDefault()
12491250
selection.toggleAt(cursorIndex, hasParent)
1251+
1252+
// Request index priority scan when selecting a directory
1253+
const entry = getEntryUnderCursor()
1254+
if (entry?.isDirectory && entry.name !== '..') {
1255+
void prioritizeDir(entry.path, 'user_selected')
1256+
}
1257+
12501258
return true
12511259
}
12521260
// Cmd+A - select all (Cmd+Shift+A - deselect all)

apps/desktop/src/lib/file-explorer/selection/SelectionInfo.svelte

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
} from './selection-info-utils'
1313
import { measureDateColumnWidth } from '../views/full-list-utils'
1414
import { formatFileSize, formatDateTime } from '$lib/settings/reactive-settings.svelte'
15+
import { isScanning } from '$lib/indexing/index-state.svelte'
1516
1617
interface Props {
1718
/** View mode: 'brief' or 'full' */
@@ -183,6 +184,9 @@
183184
// Selection summary mode
184185
// ========================================================================
185186
187+
// Drive index scanning state — used for stale indicator when dirs are selected
188+
const scanning = $derived(isScanning())
189+
186190
// Computed values for selection summary
187191
const selectedFiles = $derived(stats?.selectedFiles ?? 0)
188192
const selectedDirs = $derived(stats?.selectedDirs ?? 0)
@@ -195,6 +199,9 @@
195199
const hasDirs = $derived(totalDirs > 0)
196200
const hasOnlyDirs = $derived(!hasFiles && hasDirs)
197201
202+
// When directories are selected during scanning, sizes might be incomplete
203+
const showSelectionStale = $derived(scanning && selectedDirs > 0)
204+
198205
const sizePercentage = $derived(calculatePercentage(selectedFileSize, totalFileSize))
199206
const filePercentage = $derived(calculatePercentage(selectedFiles, totalFiles))
200207
const dirPercentage = $derived(calculatePercentage(selectedDirs, totalDirs))
@@ -232,9 +239,12 @@
232239
<!-- Selection summary -->
233240
<span class="summary-text" title={selectionSizeTooltip}>
234241
{#if hasOnlyDirs}
235-
<!-- Only dirs, no files: can't show size -->
242+
<!-- Only dirs, no files -->
236243
{formatNumber(selectedDirs)} of {formatNumber(totalDirs)}
237244
{pluralize(totalDirs, 'dir', 'dirs')} ({dirPercentage}%) selected.
245+
{#if showSelectionStale}
246+
<span class="stale-indicator" title="Might be outdated. Currently scanning...">⚠️</span>
247+
{/if}
238248
{:else if hasFiles}
239249
<!-- Has files: show full summary -->
240250
{#each selectedSizeTriads as triad, i (i)}<span class={triad.tierClass}>{triad.value}</span>{/each}
@@ -244,6 +254,9 @@
244254
{pluralize(totalFiles, 'file', 'files')} ({filePercentage}%){#if hasDirs}
245255
&nbsp;and {formatNumber(selectedDirs)} of {formatNumber(totalDirs)}
246256
{pluralize(totalDirs, 'dir', 'dirs')} ({dirPercentage}%){/if}.
257+
{#if showSelectionStale}
258+
<span class="stale-indicator" title="Might be outdated. Currently scanning...">⚠️</span>
259+
{/if}
247260
{/if}
248261
</span>
249262
{/if}
@@ -291,4 +304,9 @@
291304
overflow: hidden;
292305
text-overflow: ellipsis;
293306
}
307+
308+
.stale-indicator {
309+
font-size: 10px;
310+
cursor: help;
311+
}
294312
</style>

apps/desktop/src/lib/file-explorer/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export interface FileEntry {
1616
iconId: string
1717
/** Whether extended metadata (addedAt, openedAt) has been loaded */
1818
extendedMetadataLoaded: boolean
19+
recursiveSize?: number
20+
recursiveFileCount?: number
21+
recursiveDirCount?: number
1922
}
2023

2124
/** Cloud sync status for files in Dropbox/iCloud/etc. folders */

apps/desktop/src/lib/file-explorer/views/BriefList.svelte

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
shouldResetCache,
1818
refetchIconsForEntries,
1919
} from './file-list-utils'
20-
import { getRowHeight } from '$lib/settings/reactive-settings.svelte'
20+
import { getRowHeight, formatFileSize } from '$lib/settings/reactive-settings.svelte'
2121
import { getSetting } from '$lib/settings/settings-store'
22+
import { formatNumber, pluralize } from '../selection/selection-info-utils'
23+
import { isScanning } from '$lib/indexing/index-state.svelte'
2224
import { extensionCacheCleared } from '$lib/icon-cache'
2325
import type { RenameState } from '../rename/rename-state.svelte'
2426
@@ -89,6 +91,9 @@
8991
let cachedRange = $state({ start: 0, end: 0 })
9092
let isFetching = $state(false)
9193
94+
// Drive index scanning state — used for directory size tooltips
95+
const scanning = $derived(isScanning())
96+
9297
// ==== Layout constants ====
9398
// Row height is reactive based on UI density setting
9499
const rowHeight = $derived(getRowHeight())
@@ -418,6 +423,17 @@
418423
}
419424
})
420425
426+
/** Build tooltip for a directory entry showing recursive size info. */
427+
function buildDirTooltip(file: FileEntry): string | undefined {
428+
if (!file.isDirectory) return undefined
429+
if (file.recursiveSize !== undefined) {
430+
const sizeInfo = `${formatFileSize(file.recursiveSize)} · ${formatNumber(file.recursiveFileCount ?? 0)} ${pluralize(file.recursiveFileCount ?? 0, 'file', 'files')} · ${formatNumber(file.recursiveDirCount ?? 0)} ${pluralize(file.recursiveDirCount ?? 0, 'folder', 'folders')}`
431+
return scanning ? `${sizeInfo} — Might be outdated` : sizeInfo
432+
}
433+
if (scanning) return 'Scanning...'
434+
return undefined
435+
}
436+
421437
// Report visible range to parent for MCP state sync
422438
$effect(() => {
423439
// Calculate visible item range from column range
@@ -495,6 +511,7 @@
495511
class:is-under-cursor={globalIndex === cursorIndex}
496512
class:is-selected={selectedIndices.has(globalIndex)}
497513
data-drop-target-path={file.isDirectory && file.name !== '..' ? file.path : undefined}
514+
title={buildDirTooltip(file)}
498515
style="height: {rowHeight}px;"
499516
onmousedown={(e: MouseEvent) => {
500517
handleMouseDown(e, globalIndex)

0 commit comments

Comments
 (0)