import { invoke } from '@tauri-apps/api/core' import { isTauri, mockInvoke } from '../mock-tauri' import type { FolderNode, GitPushResult, VaultEntry, ViewFile } from '../types' import type { VaultOption } from '../components/status-bar/types' import { normalizeVaultEntries, normalizeViewFiles } from '../utils/vaultMetadataNormalization' import { workspaceIdentityFromVault } from '../utils/workspaces' interface TauriCallOptions { command: string tauriArgs: Record mockArgs?: Record } interface VaultPathOptions { vaultPath: string } interface EmptyResultReloadOptions { forceReload?: boolean reloadIfEmpty?: boolean } type VaultEntriesOptions = VaultPathOptions & EmptyResultReloadOptions interface MountedVaultEntriesOptions extends VaultPathOptions, EmptyResultReloadOptions { defaultWorkspacePath?: string | null includeFallbackVault?: boolean vaults?: VaultOption[] } type MountedVaultFoldersOptions = MountedVaultEntriesOptions type MountedVaultViewsOptions = MountedVaultEntriesOptions type WorkspaceEntryLoadOptions = EmptyResultReloadOptions interface CommitWithPushOptions extends VaultPathOptions { message: string } interface LoadedVaultData { entries: VaultEntry[] } interface LoadedVaultChrome { folders: FolderNode[] views: ViewFile[] } export function hasVaultPath({ vaultPath }: VaultPathOptions): boolean { return vaultPath.trim().length > 0 } export function tauriCall({ command, tauriArgs, mockArgs }: TauriCallOptions): Promise { return isTauri() ? invoke(command, tauriArgs) : mockInvoke(command, mockArgs ?? tauriArgs) } export async function checkVaultPathAvailability({ vaultPath }: VaultPathOptions): Promise { if (!hasVaultPath({ vaultPath })) return false try { return await tauriCall({ command: 'check_vault_exists', tauriArgs: { path: vaultPath }, }) } catch { return null } } function loadVaultEntriesWithCommand({ vaultPath, command }: VaultPathOptions & { command: string }): Promise { return tauriCall({ command, tauriArgs: { path: vaultPath } }) .then((entries) => normalizeVaultEntries(entries, vaultPath)) } function shouldIncludeVault(vault: VaultOption, primaryPaths: Set): boolean { if (!vault.path.trim() || vault.available === false) return false return vault.mounted !== false || primaryPaths.has(vault.path) } function shouldReloadEmptyResult(entries: VaultEntry[], options: EmptyResultReloadOptions): boolean { return entries.length === 0 && options.reloadIfEmpty === true && !options.forceReload && isTauri() } function shouldIncludeFallbackVault( byPath: Map, vaultPath: string, includeFallbackVault: boolean, ): boolean { if (!includeFallbackVault) return false if (!vaultPath.trim()) return false return !byPath.has(vaultPath) } function loadWorkspaceEntriesWithCommand( vault: VaultOption, command: 'list_vault' | 'reload_vault', defaultWorkspacePath?: string | null, ): Promise { const workspace = workspaceIdentityFromVault(vault, { defaultWorkspacePath }) return tauriCall({ command, tauriArgs: { path: vault.path } }) .then((entries) => normalizeVaultEntries(entries, vault.path, workspace)) } export function loadWorkspaceEntries( vault: VaultOption, defaultWorkspacePath?: string | null, options: WorkspaceEntryLoadOptions = {}, ): Promise { const command = options.forceReload && isTauri() ? 'reload_vault' : 'list_vault' return loadWorkspaceEntriesWithCommand(vault, command, defaultWorkspacePath) .then((entries) => shouldReloadEmptyResult(entries, options) ? loadWorkspaceEntriesWithCommand(vault, 'reload_vault', defaultWorkspacePath) : entries) } function uniqueMountedVaults({ defaultWorkspacePath, vaultPath, vaults = [], includeFallbackVault = true }: MountedVaultEntriesOptions): VaultOption[] { const byPath = new Map() const primaryPaths = new Set([vaultPath, defaultWorkspacePath ?? ''].filter((path) => path.trim())) for (const vault of vaults) { if (!shouldIncludeVault(vault, primaryPaths)) continue byPath.set(vault.path, vault) } if (shouldIncludeFallbackVault(byPath, vaultPath, includeFallbackVault)) { byPath.set(vaultPath, { label: vaultPath.split('/').filter(Boolean).pop() || 'Workspace', path: vaultPath, mounted: true, available: true }) } return [...byPath.values()] } function loadMountedVaultEntries(options: MountedVaultEntriesOptions): Promise { const mountedVaults = uniqueMountedVaults(options) if (mountedVaults.length <= 1) { const onlyVault = mountedVaults[0] return onlyVault ? loadWorkspaceEntries(onlyVault, options.defaultWorkspacePath, { forceReload: options.forceReload, reloadIfEmpty: options.reloadIfEmpty, }) : loadVaultEntries({ vaultPath: options.vaultPath, reloadIfEmpty: options.reloadIfEmpty }) } return Promise.all(mountedVaults.map((vault) => ( loadWorkspaceEntries(vault, options.defaultWorkspacePath, { forceReload: options.forceReload, reloadIfEmpty: options.reloadIfEmpty, }) ))) .then((groups) => groups.flat()) } function attachFolderRootPath(folders: FolderNode[], rootPath: string): FolderNode[] { return folders.map((folder) => ({ ...folder, rootPath, children: attachFolderRootPath(folder.children, rootPath), })) } function attachViewRootPath( views: ViewFile[], vault: VaultOption, defaultWorkspacePath?: string | null, ): ViewFile[] { const workspace = workspaceIdentityFromVault(vault, { defaultWorkspacePath }) return views.map((view) => ({ ...view, rootPath: vault.path, workspace, })) } function loadVaultEntries({ vaultPath, forceReload = true, reloadIfEmpty = false, }: VaultEntriesOptions): Promise { const command = forceReload && isTauri() ? 'reload_vault' : 'list_vault' return loadVaultEntriesWithCommand({ vaultPath, command }) .then((entries) => shouldReloadEmptyResult(entries, { forceReload, reloadIfEmpty }) ? loadVaultEntriesWithCommand({ vaultPath, command: 'reload_vault' }) : entries) } export function reloadVaultEntries({ vaultPath, vaults, defaultWorkspacePath }: MountedVaultEntriesOptions): Promise { if (vaults?.length) return loadMountedVaultEntries({ vaultPath, vaults, defaultWorkspacePath, forceReload: true }) return loadVaultEntriesWithCommand({ vaultPath, command: 'reload_vault' }) } export function loadVaultFolders({ vaultPath }: VaultPathOptions): Promise { return tauriCall({ command: 'list_vault_folders', tauriArgs: { path: vaultPath } }) } export async function loadMountedVaultFolders(options: MountedVaultFoldersOptions): Promise { const mountedVaults = uniqueMountedVaults(options) if (mountedVaults.length === 0) return [] if (mountedVaults.length === 1) { const [vault] = mountedVaults if (vault.path === options.vaultPath) return loadVaultFolders({ vaultPath: vault.path }) const identity = workspaceIdentityFromVault(vault, { defaultWorkspacePath: options.defaultWorkspacePath }) const children = await loadVaultFolders({ vaultPath: vault.path }) .then((folders) => attachFolderRootPath(folders ?? [], vault.path)) .catch(() => [] as FolderNode[]) return [{ name: identity.label, path: '', rootPath: vault.path, children, }] } const folderGroups = await Promise.all(mountedVaults.map(async (vault) => { const identity = workspaceIdentityFromVault(vault, { defaultWorkspacePath: options.defaultWorkspacePath }) const children = await loadVaultFolders({ vaultPath: vault.path }) .then((folders) => attachFolderRootPath(folders ?? [], vault.path)) .catch(() => [] as FolderNode[]) return { name: identity.label, path: '', rootPath: vault.path, children, } })) return folderGroups } export function loadVaultViews({ vaultPath }: VaultPathOptions): Promise { return tauriCall({ command: 'list_views', tauriArgs: { vaultPath } }) .then(normalizeViewFiles) } export async function loadMountedVaultViews(options: MountedVaultViewsOptions): Promise { const mountedVaults = uniqueMountedVaults(options) if (mountedVaults.length === 0) return [] if (mountedVaults.length === 1 && mountedVaults[0].path === options.vaultPath) { return loadVaultViews({ vaultPath: options.vaultPath }) } const viewGroups = await Promise.all(mountedVaults.map(async (vault) => { const views = await loadVaultViews({ vaultPath: vault.path }).catch(() => [] as ViewFile[]) return attachViewRootPath(views ?? [], vault, options.defaultWorkspacePath) })) return viewGroups.flat() } export async function loadVaultData({ vaultPath, vaults, defaultWorkspacePath, forceReload, reloadIfEmpty, }: MountedVaultEntriesOptions): Promise { if (!isTauri()) console.info('[mock] Using mock Tauri data for browser testing') const entries = vaults?.length ? await loadMountedVaultEntries({ vaultPath, vaults, defaultWorkspacePath, forceReload, reloadIfEmpty }) : await loadVaultEntries({ vaultPath, forceReload, reloadIfEmpty }) console.log(`Vault scan complete: ${entries.length} entries found`) return { entries } } export async function loadVaultChrome({ defaultWorkspacePath, vaultPath, vaults, }: MountedVaultEntriesOptions): Promise { const [folders, views] = await Promise.all([ loadVaultFolders({ vaultPath }).catch(() => [] as FolderNode[]), vaults?.length ? loadMountedVaultViews({ defaultWorkspacePath, vaultPath, vaults }).catch(() => [] as ViewFile[]) : loadVaultViews({ vaultPath }).catch(() => [] as ViewFile[]), ]) return { folders: folders ?? [], views: views ?? [], } } export async function commitWithPush({ vaultPath, message }: CommitWithPushOptions): Promise { if (!isTauri()) { await mockInvoke('git_commit', { message }) return mockInvoke('git_push', {}) } await invoke('git_commit', { vaultPath, message }) return invoke('git_push', { vaultPath }) }