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 MountedVaultEntriesOptions extends VaultPathOptions { defaultWorkspacePath?: string | null forceReload?: boolean includeFallbackVault?: boolean vaults?: VaultOption[] } type MountedVaultFoldersOptions = MountedVaultEntriesOptions type MountedVaultViewsOptions = MountedVaultEntriesOptions interface WorkspaceEntryLoadOptions { forceReload?: boolean reloadIfEmpty?: boolean } 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 shouldSkipMountedVault(vault: VaultOption): boolean { return vault.available === false || vault.mounted === false || !vault.path.trim() } function shouldReloadEmptyWorkspaceResult(entries: VaultEntry[], options: WorkspaceEntryLoadOptions): 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) => shouldReloadEmptyWorkspaceResult(entries, options) ? loadWorkspaceEntriesWithCommand(vault, 'reload_vault', defaultWorkspacePath) : entries) } function uniqueMountedVaults({ vaultPath, vaults = [], includeFallbackVault = true }: MountedVaultEntriesOptions): VaultOption[] { const byPath = new Map() for (const vault of vaults) { if (shouldSkipMountedVault(vault)) 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 }) : loadVaultEntries({ vaultPath: options.vaultPath }) } return Promise.all(mountedVaults.map((vault) => ( loadWorkspaceEntries(vault, options.defaultWorkspacePath, { forceReload: options.forceReload }) ))) .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 }: VaultPathOptions): Promise { const command = isTauri() ? 'reload_vault' : 'list_vault' return loadVaultEntriesWithCommand({ vaultPath, command }) } 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, includeFallbackVault: false }) 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, includeFallbackVault: false }) 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 }: MountedVaultEntriesOptions): Promise { if (!isTauri()) console.info('[mock] Using mock Tauri data for browser testing') const entries = vaults?.length ? await loadMountedVaultEntries({ vaultPath, vaults, defaultWorkspacePath, forceReload }) : await loadVaultEntries({ vaultPath }) 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 }) }