Fix: WSL host UNC path handling on Windows

- Normalize extended UNC paths for modern context menu
- Treat //wsl.localhost as a virtual directory listing WSL distros
This commit is contained in:
aleksey-hoffman committed 2026-07-06 12:49:16 +03:00
1 parent 041a55b678
commit dbee91384d
11 files changed
+375 -93

No files matched your search

+45 -3
View File
@@ -78,6 +78,16 @@ pub fn common_parent_directory_for_selections(
Ok(expected_parent.clone())
}
pub fn strip_extended_path_prefix(path: &str) -> String {
if let Some(rest) = path.strip_prefix(r"\\?\UNC\") {
format!(r"\\{rest}")
} else if let Some(rest) = path.strip_prefix(r"\\?\") {
rest.to_string()
} else {
path.to_string()
}
}
pub fn prepare_shell_path(file_path: &str) -> String {
#[cfg(windows)]
let native_path = path_for_selection(file_path);
@@ -92,7 +102,8 @@ pub fn prepare_shell_path(file_path: &str) -> String {
#[cfg(windows)]
{
resolved.to_string_lossy().replace('/', "\\")
let resolved_path = strip_extended_path_prefix(&resolved.to_string_lossy());
resolved_path.replace('/', "\\")
}
#[cfg(not(windows))]
{
@@ -143,10 +154,39 @@ pub fn load_png_as_base64(path: &std::path::Path) -> Option<String> {
None
}
#[cfg(all(test, windows))]
#[cfg(test)]
mod shell_path_tests {
use super::{path_for_selection, prepare_shell_path};
use super::{path_for_selection, prepare_shell_path, strip_extended_path_prefix};
#[test]
fn strip_extended_path_prefix_strips_extended_unc_prefix() {
assert_eq!(
strip_extended_path_prefix(r"\\?\UNC\wsl.localhost\Ubuntu-24.04\"),
r"\\wsl.localhost\Ubuntu-24.04\",
);
assert_eq!(
strip_extended_path_prefix(r"\\?\UNC\wsl.localhost\docker-desktop\"),
r"\\wsl.localhost\docker-desktop\",
);
}
#[test]
fn strip_extended_path_prefix_strips_extended_local_prefix() {
assert_eq!(
strip_extended_path_prefix(r"\\?\C:\Users\aleks\file.txt"),
r"C:\Users\aleks\file.txt",
);
}
#[test]
fn strip_extended_path_prefix_leaves_regular_paths_unchanged() {
assert_eq!(
strip_extended_path_prefix(r"\\wsl.localhost\Ubuntu-24.04\"),
r"\\wsl.localhost\Ubuntu-24.04\",
);
}
#[cfg(windows)]
#[test]
fn path_for_selection_converts_unc_paths_to_backslashes() {
assert_eq!(
@@ -155,6 +195,7 @@ mod shell_path_tests {
);
}
#[cfg(windows)]
#[test]
fn prepare_shell_path_converts_unc_paths_to_backslashes() {
assert_eq!(
@@ -163,6 +204,7 @@ mod shell_path_tests {
);
}
#[cfg(windows)]
#[test]
fn prepare_shell_path_converts_local_windows_paths_to_backslashes() {
assert_eq!(
@@ -10,7 +10,7 @@
// pipeline.
use crate::open_with::types::{GetShellContextMenuResult, OpenWithResult, ShellContextMenuItem};
use crate::open_with::utils::canonicalize_path;
use crate::open_with::utils::shell_path_candidates;
use crate::open_with::windows::icon_utils::icon_location_to_data_uri;
use std::collections::HashMap;
use std::path::Path;
@@ -358,23 +358,37 @@ unsafe fn read_owned_pwstr(value: windows::core::Result<PWSTR>) -> Option<String
Some(text)
}
unsafe fn parse_shell_item_id_list(path: &str) -> Result<*mut ITEMIDLIST, String> {
let mut last_error = String::from("No paths provided");
for candidate_path in shell_path_candidates(path) {
let wide_path = HSTRING::from(&candidate_path);
let mut id_list: *mut ITEMIDLIST = std::ptr::null_mut();
match SHParseDisplayName(&wide_path, None, &mut id_list, 0, None) {
Ok(_) => return Ok(id_list),
Err(parse_error) => {
last_error = format!(
"Failed to parse path '{}': {}",
candidate_path, parse_error
);
}
}
}
Err(last_error)
}
unsafe fn create_shell_item_array(file_paths: &[String]) -> Result<IShellItemArray, String> {
let mut id_lists: Vec<*mut ITEMIDLIST> = Vec::with_capacity(file_paths.len());
for path in file_paths {
let normalized_path = canonicalize_path(Path::new(path));
let wide_path = HSTRING::from(&normalized_path);
let mut id_list: *mut ITEMIDLIST = std::ptr::null_mut();
match SHParseDisplayName(&wide_path, None, &mut id_list, 0, None) {
Ok(_) => id_lists.push(id_list),
match parse_shell_item_id_list(path) {
Ok(id_list) => id_lists.push(id_list),
Err(parse_error) => {
for allocated in &id_lists {
CoTaskMemFree(Some(*allocated as *const _));
}
return Err(format!(
"Failed to parse path '{}': {}",
normalized_path, parse_error
));
return Err(parse_error);
}
}
}
@@ -14,7 +14,6 @@ import {
markRaw,
} from 'vue';
import { useI18n } from 'vue-i18n';
import { invoke } from '@tauri-apps/api/core';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
@@ -46,8 +45,11 @@ import { useShortcutsStore } from '@/stores/runtime/shortcuts';
import { usePlatformStore } from '@/stores/runtime/platform';
import normalizePath, { getPathDisplayName, getPathSegments, isUncPath } from '@/utils/normalize-path';
import {
buildLocationsDirectoryFromDrives,
isVirtualDirectoryPath,
isVirtualLocationPath,
LOCATIONS_VIRTUAL_PATH,
resolveDirectoryContents,
shouldPrependLocationsCrumb,
} from '@/utils/virtual-locations';
import { useDrives } from '@/modules/home/composables/use-drives';
@@ -67,7 +69,9 @@ const { t } = useI18n();
const shortcutsStore = useShortcutsStore();
const platformStore = usePlatformStore();
const { drives } = useDrives();
const locationsDrivePaths = computed(() => drives.value.map(drive => normalizePath(drive.path)));
const locationsDirectoryPaths = computed(() => {
return buildLocationsDirectoryFromDrives(drives.value).entries.map(entry => entry.path);
});
const { openCopiedPath } = useOpenCopiedPath({
openDirectory: path => emit('navigate', path),
openFile: path => emit('openFile', path),
@@ -83,7 +87,9 @@ interface AddressBarPart {
path: string;
name: string;
isLast: boolean;
virtual?: boolean;
disableDropTarget?: boolean;
displayRootIcon?: boolean;
useLocationsDropdown?: boolean;
}
const addressParts = computed(() => {
@@ -94,7 +100,9 @@ const addressParts = computed(() => {
path: LOCATIONS_VIRTUAL_PATH,
name: t('locations'),
isLast: true,
virtual: true,
disableDropTarget: true,
displayRootIcon: true,
useLocationsDropdown: true,
}];
}
@@ -128,6 +136,7 @@ const addressParts = computed(() => {
path: fullPath,
name: part,
isLast: index === parts.length - 1,
disableDropTarget: isVirtualDirectoryPath(fullPath),
});
});
@@ -140,7 +149,9 @@ const addressParts = computed(() => {
path: LOCATIONS_VIRTUAL_PATH,
name: t('locations'),
isLast: false,
virtual: true,
disableDropTarget: true,
displayRootIcon: true,
useLocationsDropdown: true,
},
...formattedParts,
];
@@ -162,12 +173,12 @@ async function loadSeparatorDirectories(index: number) {
const part = addressParts.value[index];
if (!part) return;
if (part.virtual) {
if (part.useLocationsDropdown) {
return;
}
try {
const result = await invoke<DirContents>('read_dir', { path: part.path });
const result = await resolveDirectoryContents(part.path);
const directories = result.entries
.filter(entry => entry.is_dir)
.map(entry => ({
@@ -301,18 +312,18 @@ onUnmounted(() => {
<DirEntryInteractive
:path="part.path"
:is-file="false"
:disable-drop-target="part.virtual"
:disable-drop-target="part.disableDropTarget"
:is-current-directory-context="part.isLast"
>
<button
class="address-bar__part"
:class="{ 'address-bar__part--last': part.isLast }"
:title="part.virtual ? t('locations') : part.path"
:aria-label="part.virtual ? t('locations') : part.name"
:title="part.displayRootIcon ? t('locations') : part.path"
:aria-label="part.displayRootIcon ? t('locations') : part.name"
@click.stop="!part.isLast && navigateToPart(part.path)"
>
<FolderRootIcon
v-if="part.virtual"
v-if="part.displayRootIcon"
:size="14"
class="address-bar__part-icon"
/>
@@ -354,7 +365,7 @@ onUnmounted(() => {
class="address-bar__separator-menu-scroll"
>
<DropdownMenuItem
v-for="dirPath in part.virtual ? locationsDrivePaths : separatorDropdowns[index]"
v-for="dirPath in part.useLocationsDropdown ? locationsDirectoryPaths : separatorDropdowns[index]"
:key="dirPath"
@select="handleSeparatorNavigate(dirPath)"
>
@@ -26,15 +26,14 @@ import { usePlatformStore } from '@/stores/runtime/platform';
import { DIR_SIZE_CONSTANTS } from '@/constants';
import normalizePath, {
getPathDisplayName,
isWslHostRootUncPath,
} from '@/utils/normalize-path';
import { resolveNavigableItemTarget } from '@/utils/resolve-navigable-item-target';
import {
buildVirtualDirectoryFromDrives,
getNavigableParentPath,
isVirtualDirectoryPath,
isVirtualLocationPath,
resolveDirectoryContents,
buildLocationsDirectoryFromDrives,
getLocationsDriveListDisplaySignature,
getLocationsEntriesDisplaySignature,
} from '@/utils/virtual-locations';
import { sharedDrives } from '@/modules/home/composables/use-drives';
@@ -95,7 +94,7 @@ export function useFileBrowserNavigation(
}
async function directoryPathExists(path: string): Promise<boolean> {
if (isVirtualLocationPath(path)) {
if (isVirtualDirectoryPath(path)) {
return true;
}
@@ -258,12 +257,18 @@ export function useFileBrowserNavigation(
}
watch(sharedDrives, () => {
if (!isVirtualLocationPath(currentPath.value) || !dirContents.value) {
if (!isVirtualDirectoryPath(currentPath.value) || !dirContents.value) {
return;
}
const displayedSignature = getLocationsEntriesDisplaySignature(dirContents.value.entries);
const liveSignature = getLocationsDriveListDisplaySignature(sharedDrives.value);
const liveDirectory = buildVirtualDirectoryFromDrives(currentPath.value, sharedDrives.value);
if (!liveDirectory) {
return;
}
const liveSignature = getLocationsEntriesDisplaySignature(liveDirectory.entries);
if (displayedSignature === liveSignature) {
return;
@@ -295,8 +300,9 @@ export function useFileBrowserNavigation(
isRefreshing.value = true;
try {
const result = isVirtualLocationPath(refreshPath)
? buildLocationsDirectoryFromDrives(sharedDrives.value)
const virtualDirectory = buildVirtualDirectoryFromDrives(refreshPath, sharedDrives.value);
const result = virtualDirectory
? virtualDirectory
: await loadDirectoryContents(refreshPath, createReadDirOptions());
if (currentPath.value !== refreshPath) {
@@ -318,7 +324,7 @@ export function useFileBrowserNavigation(
.slice(0, DIR_SIZE_CONSTANTS.BATCH_LIMIT)
.map(entry => entry.path);
if (dirPaths.length > 0 && !isCopyOrMoveInProgress() && !isVirtualLocationPath(result.path)) {
if (dirPaths.length > 0 && !isCopyOrMoveInProgress() && !isVirtualDirectoryPath(result.path)) {
dirSizesStore.requestSizesBatch(dirPaths);
}
}
@@ -431,6 +437,9 @@ export function useFileBrowserNavigation(
if (isVirtualLocationPath(result.path)) {
currentTab.name = result.path;
}
else if (isVirtualDirectoryPath(result.path)) {
currentTab.name = getPathDisplayName(result.path);
}
else {
try {
currentTab.name = await basename(result.path);
@@ -461,11 +470,11 @@ export function useFileBrowserNavigation(
.slice(0, DIR_SIZE_CONSTANTS.BATCH_LIMIT)
.map(entry => entry.path);
if (dirPaths.length > 0 && !isCopyOrMoveInProgress() && !isVirtualLocationPath(result.path)) {
if (dirPaths.length > 0 && !isCopyOrMoveInProgress() && !isVirtualDirectoryPath(result.path)) {
dirSizesStore.requestSizesBatch(dirPaths);
}
if (!isVirtualLocationPath(result.path)) {
if (!isVirtualDirectoryPath(result.path)) {
logDirWatcherDiag('readDir starting watcher after load', {
path: result.path,
entryCount: result.entries.length,
@@ -485,13 +494,9 @@ export function useFileBrowserNavigation(
async function navigateToPath(path: string) {
cancelPendingDirectoryRecord();
const normalizedTarget = normalizePath(path);
await readDir(path);
if (error.value && isWslHostRootUncPath(normalizedTarget)) {
await navigateToHome();
}
else if (!error.value) {
if (!error.value) {
schedulePendingDirectoryRecord(path);
}
}
@@ -513,10 +518,7 @@ export function useFileBrowserNavigation(
cancelPendingDirectoryRecord();
await readDir(targetPath);
if (error.value && isWslHostRootUncPath(targetPath)) {
await navigateToHome();
}
else if (!error.value) {
if (!error.value) {
schedulePendingDirectoryRecord(targetPath);
}
}
@@ -33,6 +33,7 @@ describe('entry-action-policy', () => {
describe('getEntryActionPolicy', () => {
it('classifies virtual, mount-root, and normal paths', () => {
expect(getEntryActionPolicy(LOCATIONS_VIRTUAL_PATH, 'windows')).toBe('virtual-location');
expect(getEntryActionPolicy('//wsl.localhost', 'windows')).toBe('virtual-location');
expect(getEntryActionPolicy('C:/', 'windows')).toBe('system-mount-root');
expect(getEntryActionPolicy('C:/Users', 'windows')).toBe('normal');
expect(getEntryActionPolicy('//wsl.localhost/Ubuntu-24.04', 'windows')).toBe('system-mount-root');
@@ -11,7 +11,7 @@ import {
isBrowsingVirtualLocations,
resolveActionDirectoryPath,
} from '@/utils/virtual-location-action-target';
import { LOCATIONS_VIRTUAL_PATH } from '@/utils/virtual-path-constants';
import { LOCATIONS_VIRTUAL_PATH, WSL_HOST_VIRTUAL_PATH } from '@/utils/virtual-path-constants';
function createEntry(overrides: Partial<DirEntry> & Pick<DirEntry, 'path'>): DirEntry {
return {
@@ -35,6 +35,7 @@ describe('virtual-location-action-target', () => {
describe('isBrowsingVirtualLocations', () => {
it('detects the locations virtual path', () => {
expect(isBrowsingVirtualLocations(LOCATIONS_VIRTUAL_PATH)).toBe(true);
expect(isBrowsingVirtualLocations(WSL_HOST_VIRTUAL_PATH)).toBe(true);
expect(isBrowsingVirtualLocations('C:/Users')).toBe(false);
});
});
@@ -97,6 +98,13 @@ describe('virtual-location-action-target', () => {
expect(resolveActionDirectoryPath(undefined, 'C:/Users')).toBe('C:/Users');
});
it('uses the selected directory when browsing the wsl host virtual path', () => {
const selectedEntries = [createEntry({ path: '//wsl.localhost/docker-desktop' })];
expect(resolveActionDirectoryPath(selectedEntries, WSL_HOST_VIRTUAL_PATH))
.toBe('//wsl.localhost/docker-desktop');
});
it('uses the selected directory when browsing virtual locations', () => {
const selectedEntries = [createEntry({ path: '//wsl.localhost/docker-desktop' })];
+123 -2
View File
@@ -5,14 +5,19 @@
import { describe, expect, it } from 'vitest';
import {
LOCATIONS_VIRTUAL_PATH,
WSL_HOST_VIRTUAL_PATH,
buildLocationsDirectoryFromDrives,
buildVirtualDirectoryFromDrives,
buildWslHostDirectoryFromDrives,
createLocationsDirEntry,
driveInfoToDirEntry,
getLocationsDriveListDisplaySignature,
getLocationsEntriesDisplaySignature,
getNavigableParentPath,
getVirtualLocationDisplayName,
isVirtualDirectoryPath,
isVirtualLocationPath,
isWslHostVirtualPath,
shouldPrependLocationsCrumb,
virtualLocationPathExists,
} from '@/utils/virtual-locations';
@@ -25,8 +30,18 @@ describe('virtual-locations', () => {
expect(isVirtualLocationPath('C:/Users')).toBe(false);
});
it('returns the localized display name for virtual locations', () => {
it('detects the wsl host virtual path', () => {
expect(isWslHostVirtualPath('//wsl.localhost')).toBe(true);
expect(isWslHostVirtualPath('//wsl.localhost/')).toBe(true);
expect(isWslHostVirtualPath('//wsl$')).toBe(true);
expect(isVirtualDirectoryPath('//wsl.localhost')).toBe(true);
expect(isWslHostVirtualPath('//wsl.localhost/Ubuntu')).toBe(false);
expect(virtualLocationPathExists('//wsl.localhost/')).toBe(true);
});
it('returns display names for virtual directories', () => {
expect(getVirtualLocationDisplayName('sfm://locations', key => `t:${key}`)).toBe('t:locations');
expect(getVirtualLocationDisplayName('//wsl.localhost', key => `t:${key}`)).toBe('wsl.localhost');
expect(getVirtualLocationDisplayName('C:/', key => `t:${key}`)).toBeNull();
});
@@ -87,6 +102,111 @@ describe('virtual-locations', () => {
expect(contents.entries[0]?.path).toBe('C:/');
});
it('keeps wsl distribution drives flat in locations', () => {
const localDrive: DriveInfo = {
name: 'Local Disk (C:)',
path: 'C:/',
mount_point: 'C:\\',
file_system: 'NTFS',
drive_type: 'SSD',
total_space: 1000,
available_space: 500,
used_space: 500,
percent_used: 50,
is_removable: false,
is_read_only: false,
is_mounted: true,
device_path: '\\\\.\\C:',
};
const wslDrive: DriveInfo = {
name: 'Ubuntu-24.04',
path: '//wsl.localhost/Ubuntu-24.04/',
mount_point: '//wsl.localhost/Ubuntu-24.04/',
file_system: 'WSL',
drive_type: 'WSL',
total_space: 0,
available_space: 0,
used_space: 0,
percent_used: 0,
is_removable: false,
is_read_only: false,
is_mounted: true,
device_path: '//wsl.localhost/Ubuntu-24.04/',
};
const contents = buildLocationsDirectoryFromDrives([localDrive, wslDrive]);
expect(contents.entries.map(entry => entry.path)).toEqual([
'C:/',
'//wsl.localhost/Ubuntu-24.04/',
]);
expect(contents.entries[1]?.item_count).toBeNull();
});
it('builds wsl host directory contents from wsl distribution drives only', () => {
const localDrive: DriveInfo = {
name: 'Local Disk (C:)',
path: 'C:/',
mount_point: 'C:\\',
file_system: 'NTFS',
drive_type: 'SSD',
total_space: 1000,
available_space: 500,
used_space: 500,
percent_used: 50,
is_removable: false,
is_read_only: false,
is_mounted: true,
device_path: '\\\\.\\C:',
};
const wslDrive: DriveInfo = {
name: 'Ubuntu-24.04',
path: '//wsl.localhost/Ubuntu-24.04/',
mount_point: '//wsl.localhost/Ubuntu-24.04/',
file_system: 'WSL',
drive_type: 'WSL',
total_space: 0,
available_space: 0,
used_space: 0,
percent_used: 0,
is_removable: false,
is_read_only: false,
is_mounted: true,
device_path: '//wsl.localhost/Ubuntu-24.04/',
};
const contents = buildWslHostDirectoryFromDrives([localDrive, wslDrive]);
expect(contents.path).toBe(WSL_HOST_VIRTUAL_PATH);
expect(contents.entries).toHaveLength(1);
expect(contents.entries[0]?.path).toBe('//wsl.localhost/Ubuntu-24.04/');
});
it('builds virtual directories from drive snapshots by path', () => {
const wslDrive: DriveInfo = {
name: 'Ubuntu-24.04',
path: '//wsl.localhost/Ubuntu-24.04/',
mount_point: '//wsl.localhost/Ubuntu-24.04/',
file_system: 'WSL',
drive_type: 'WSL',
total_space: 0,
available_space: 0,
used_space: 0,
percent_used: 0,
is_removable: false,
is_read_only: false,
is_mounted: true,
device_path: '//wsl.localhost/Ubuntu-24.04/',
};
const locationsContents = buildVirtualDirectoryFromDrives(LOCATIONS_VIRTUAL_PATH, [wslDrive]);
expect(locationsContents?.path).toBe(LOCATIONS_VIRTUAL_PATH);
expect(locationsContents?.entries[0]?.path).toBe('//wsl.localhost/Ubuntu-24.04/');
expect(buildVirtualDirectoryFromDrives(WSL_HOST_VIRTUAL_PATH, [wslDrive])?.path)
.toBe(WSL_HOST_VIRTUAL_PATH);
expect(buildVirtualDirectoryFromDrives('C:/', [wslDrive])).toBeNull();
});
it('detects drive metadata changes in locations display signatures', () => {
const baseDrive: DriveInfo = {
name: 'Local Disk (C:)',
@@ -129,7 +249,8 @@ describe('virtual-locations', () => {
expect(getNavigableParentPath('C:/', 'windows')).toBe(LOCATIONS_VIRTUAL_PATH);
expect(getNavigableParentPath('C:/Users', 'windows')).toBe('C:/');
expect(getNavigableParentPath('//wsl.localhost', 'windows')).toBe(LOCATIONS_VIRTUAL_PATH);
expect(getNavigableParentPath('//wsl.localhost/Ubuntu', 'windows')).toBe(LOCATIONS_VIRTUAL_PATH);
expect(getNavigableParentPath('//wsl$', 'windows')).toBe(LOCATIONS_VIRTUAL_PATH);
expect(getNavigableParentPath('//wsl.localhost/Ubuntu', 'windows')).toBe('//wsl.localhost');
expect(getNavigableParentPath('//wsl.localhost/Ubuntu/home', 'windows')).toBe('//wsl.localhost/Ubuntu');
});
+3 -3
View File
@@ -5,7 +5,7 @@
import type { DirEntry } from '@/types/dir-entry';
import type { ContextMenuAction } from '@/modules/navigator/components/file-browser/types';
import { isProtectedSystemPath } from '@/utils/is-protected-system-path';
import { isVirtualLocationPath } from '@/utils/virtual-path-constants';
import { isVirtualDirectoryPath } from '@/utils/virtual-path-constants';
export type EntryActionPolicy = 'virtual-location' | 'system-mount-root' | 'normal';
@@ -33,7 +33,7 @@ const SYSTEM_MOUNT_ROOT_BLOCKED_ACTIONS = new Set<ContextMenuAction>([
]);
export function getEntryActionPolicy(path: string, platform: string | null): EntryActionPolicy {
if (isVirtualLocationPath(path)) {
if (isVirtualDirectoryPath(path)) {
return 'virtual-location';
}
@@ -45,7 +45,7 @@ export function getEntryActionPolicy(path: string, platform: string | null): Ent
}
export function isSelectionVirtualLocation(entries: DirEntry[]): boolean {
return entries.some(entry => isVirtualLocationPath(entry.path));
return entries.some(entry => isVirtualDirectoryPath(entry.path));
}
export function isActionBlockedByEntryPolicy(
+5 -5
View File
@@ -3,7 +3,7 @@
// Copyright © 2021 - present Aleksey Hoffman. All rights reserved.
import type { DirEntry } from '@/types/dir-entry';
import { isVirtualLocationPath } from '@/utils/virtual-path-constants';
import { isVirtualDirectoryPath } from '@/utils/virtual-path-constants';
export type VirtualLocationActionContext = {
isBrowsingVirtualLocations: boolean;
@@ -15,7 +15,7 @@ export type VirtualLocationActionContext = {
export function isBrowsingVirtualLocations(
currentDirectoryPath: string | null | undefined,
): boolean {
return isVirtualLocationPath(currentDirectoryPath ?? '');
return isVirtualDirectoryPath(currentDirectoryPath ?? '');
}
export function getDirectoryPathFromEntry(entry: DirEntry): string | null {
@@ -45,7 +45,7 @@ export function getActionTargetEntries(
return entries;
}
return entries.filter(entry => !isVirtualLocationPath(entry.path));
return entries.filter(entry => !isVirtualDirectoryPath(entry.path));
}
export function resolveActionDirectoryPath(
@@ -63,7 +63,7 @@ export function resolveActionDirectoryPath(
const selectionPath = getDirectoryPathFromEntry(firstTargetEntry);
if (!selectionPath || isVirtualLocationPath(selectionPath)) {
if (!selectionPath || isVirtualDirectoryPath(selectionPath)) {
return null;
}
@@ -73,7 +73,7 @@ export function resolveActionDirectoryPath(
if (entries.length > 0) {
const selectionPath = getDirectoryPathFromEntry(entries[0]);
if (selectionPath && !isVirtualLocationPath(selectionPath)) {
if (selectionPath && !isVirtualDirectoryPath(selectionPath)) {
return selectionPath;
}
}
+109 -37
View File
@@ -4,10 +4,12 @@
import { invoke } from '@tauri-apps/api/core';
import type { DirContents, DirEntry, ReadDirOptions } from '@/types/dir-entry';
import type { DriveEntryMetadata, DriveInfo } from '@/types/drive-info';
import type { DriveInfo } from '@/types/drive-info';
import normalizePath, {
getParentPath,
getPathDisplayName,
isUncShareRootPath,
isWslPath,
} from '@/utils/normalize-path';
import { isProtectedSystemPath } from '@/utils/is-protected-system-path';
import {
@@ -15,23 +17,36 @@ import {
isWindowsLocationsScopePath,
} from '@/utils/system-mount-roots';
import {
isVirtualDirectoryPath,
isVirtualLocationPath,
isWslHostVirtualPath,
LOCATIONS_VIRTUAL_PATH,
WSL_HOST_VIRTUAL_PATH,
} from '@/utils/virtual-path-constants';
import { createDriveEntryMetadata } from '@/utils/drive-icon';
export { isVirtualLocationPath, LOCATIONS_VIRTUAL_PATH } from '@/utils/virtual-path-constants';
export {
isVirtualDirectoryPath,
isVirtualLocationPath,
isWslHostVirtualPath,
LOCATIONS_VIRTUAL_PATH,
WSL_HOST_VIRTUAL_PATH,
} from '@/utils/virtual-path-constants';
export function virtualLocationPathExists(path: string): boolean {
return isVirtualLocationPath(path);
return isVirtualDirectoryPath(path);
}
export function getVirtualLocationDisplayName(path: string, translate: (key: string) => string): string | null {
if (!isVirtualLocationPath(path)) {
return null;
if (isVirtualLocationPath(path)) {
return translate('locations');
}
return translate('locations');
if (isWslHostVirtualPath(path)) {
return getPathDisplayName(WSL_HOST_VIRTUAL_PATH);
}
return null;
}
export function driveInfoToDirEntry(drive: DriveInfo): DirEntry {
@@ -56,16 +71,16 @@ export function driveInfoToDirEntry(drive: DriveInfo): DirEntry {
};
}
export function createLocationsDirEntry(): DirEntry {
function createVirtualDirectoryEntry(path: string, name: string, itemCount: number | null = null): DirEntry {
return {
name: '',
path: LOCATIONS_VIRTUAL_PATH,
name,
path,
is_file: false,
is_dir: true,
is_hidden: false,
is_symlink: false,
size: 0,
item_count: null,
item_count: itemCount,
created_time: 0,
modified_time: 0,
accessed_time: 0,
@@ -74,6 +89,27 @@ export function createLocationsDirEntry(): DirEntry {
};
}
export function createLocationsDirEntry(): DirEntry {
return createVirtualDirectoryEntry(LOCATIONS_VIRTUAL_PATH, '');
}
export function createWslHostDirEntry(distributionCount: number | null = null): DirEntry {
return createVirtualDirectoryEntry(
WSL_HOST_VIRTUAL_PATH,
getPathDisplayName(WSL_HOST_VIRTUAL_PATH),
distributionCount,
);
}
export function isWslDistributionDrive(drive: DriveInfo): boolean {
const normalizedPath = normalizePath(drive.path);
return isWslPath(normalizedPath) && isUncShareRootPath(normalizedPath);
}
export function getWslDistributionDrives(drives: DriveInfo[]): DriveInfo[] {
return drives.filter(drive => isWslDistributionDrive(drive));
}
export function buildLocationsDirectoryFromDrives(drives: DriveInfo[]): DirContents {
const entries = drives.map(drive => driveInfoToDirEntry(drive));
const directoryCount = entries.length;
@@ -92,44 +128,27 @@ export function buildLocationsDirectoryFromDrives(drives: DriveInfo[]): DirConte
};
}
function formatLocationsDriveDisplaySignaturePart(
path: string,
name: string,
size: number,
driveMetadata: DriveEntryMetadata | null | undefined,
): string {
function formatLocationsEntryDisplaySignature(entry: DirEntry): string {
return [
normalizePath(path),
name,
String(size),
driveMetadata?.drive_type ?? '',
driveMetadata?.is_removable ? '1' : '0',
normalizePath(entry.path),
entry.name,
String(entry.size),
entry.drive_metadata?.drive_type ?? '',
entry.drive_metadata?.is_removable ? '1' : '0',
entry.item_count === null ? '' : String(entry.item_count),
].join('\x1f');
}
export function getLocationsDriveListDisplaySignature(drives: DriveInfo[]): string {
return drives
.map((drive) => {
const entry = driveInfoToDirEntry(drive);
return formatLocationsDriveDisplaySignaturePart(
entry.path,
entry.name,
entry.size,
entry.drive_metadata,
);
})
return buildLocationsDirectoryFromDrives(drives).entries
.map(entry => formatLocationsEntryDisplaySignature(entry))
.sort()
.join('\0');
}
export function getLocationsEntriesDisplaySignature(entries: DirEntry[]): string {
return entries
.map(entry => formatLocationsDriveDisplaySignaturePart(
entry.path,
entry.name,
entry.size,
entry.drive_metadata,
))
.map(entry => formatLocationsEntryDisplaySignature(entry))
.sort()
.join('\0');
}
@@ -139,6 +158,41 @@ export async function readLocationsDirectory(): Promise<DirContents> {
return buildLocationsDirectoryFromDrives(drives);
}
export function buildWslHostDirectoryFromDrives(drives: DriveInfo[]): DirContents {
const entries = getWslDistributionDrives(drives).map(drive => driveInfoToDirEntry(drive));
const directoryCount = entries.length;
return {
path: WSL_HOST_VIRTUAL_PATH,
entries,
total_count: directoryCount,
dir_count: directoryCount,
file_count: 0,
opened_directory_times: {
modified_time: 0,
accessed_time: 0,
created_time: 0,
},
};
}
export async function readWslHostDirectory(): Promise<DirContents> {
const drives = await invoke<DriveInfo[]>('get_system_drives');
return buildWslHostDirectoryFromDrives(drives);
}
export function buildVirtualDirectoryFromDrives(path: string, drives: DriveInfo[]): DirContents | null {
if (isVirtualLocationPath(path)) {
return buildLocationsDirectoryFromDrives(drives);
}
if (isWslHostVirtualPath(path)) {
return buildWslHostDirectoryFromDrives(drives);
}
return null;
}
export async function resolveDirectoryContents(
path: string,
options?: ReadDirOptions,
@@ -147,6 +201,10 @@ export async function resolveDirectoryContents(
return readLocationsDirectory();
}
if (isWslHostVirtualPath(path)) {
return readWslHostDirectory();
}
return invoke<DirContents>('read_dir', {
path,
options,
@@ -161,6 +219,10 @@ export async function resolveDirEntry(
return createLocationsDirEntry();
}
if (isWslHostVirtualPath(path)) {
return createWslHostDirEntry();
}
try {
return await invoke<DirEntry>('get_dir_entry_with_timeout', {
path,
@@ -184,8 +246,18 @@ export function getNavigableParentPath(path: string, platform: string | null): s
return null;
}
if (isWslHostVirtualPath(normalizedPath)) {
return LOCATIONS_VIRTUAL_PATH;
}
const pathWithoutTrailingSlash = normalizedPath.replace(/\/+$/, '');
if (platform === 'windows'
&& isWslPath(pathWithoutTrailingSlash)
&& isUncShareRootPath(pathWithoutTrailingSlash)) {
return WSL_HOST_VIRTUAL_PATH;
}
if (isProtectedSystemPath(pathWithoutTrailingSlash, platform)) {
return LOCATIONS_VIRTUAL_PATH;
}
+11
View File
@@ -3,7 +3,18 @@
// Copyright © 2021 - present Aleksey Hoffman. All rights reserved.
export const LOCATIONS_VIRTUAL_PATH = 'sfm://locations';
export const WSL_HOST_VIRTUAL_PATH = '//wsl.localhost';
export function isVirtualLocationPath(path: string): boolean {
return path.replace(/\\/g, '/') === LOCATIONS_VIRTUAL_PATH;
}
export function isWslHostVirtualPath(path: string): boolean {
const normalizedPath = path.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
return normalizedPath === WSL_HOST_VIRTUAL_PATH || normalizedPath === '//wsl$';
}
export function isVirtualDirectoryPath(path: string): boolean {
return isVirtualLocationPath(path) || isWslHostVirtualPath(path);
}