Files
tolaria/src/hooks/vaultLoaderCommands.ts
2026-05-12 19:33:53 +02:00

262 lines
9.4 KiB
TypeScript

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<string, unknown>
mockArgs?: Record<string, unknown>
}
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<T>({ command, tauriArgs, mockArgs }: TauriCallOptions): Promise<T> {
return isTauri() ? invoke<T>(command, tauriArgs) : mockInvoke<T>(command, mockArgs ?? tauriArgs)
}
export async function checkVaultPathAvailability({ vaultPath }: VaultPathOptions): Promise<boolean | null> {
if (!hasVaultPath({ vaultPath })) return false
try {
return await tauriCall<boolean>({
command: 'check_vault_exists',
tauriArgs: { path: vaultPath },
})
} catch {
return null
}
}
function loadVaultEntriesWithCommand({ vaultPath, command }: VaultPathOptions & { command: string }): Promise<VaultEntry[]> {
return tauriCall<unknown>({ 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<string, VaultOption>,
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<VaultEntry[]> {
const workspace = workspaceIdentityFromVault(vault, { defaultWorkspacePath })
return tauriCall<unknown>({ command, tauriArgs: { path: vault.path } })
.then((entries) => normalizeVaultEntries(entries, vault.path, workspace))
}
export function loadWorkspaceEntries(
vault: VaultOption,
defaultWorkspacePath?: string | null,
options: WorkspaceEntryLoadOptions = {},
): Promise<VaultEntry[]> {
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<string, VaultOption>()
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<VaultEntry[]> {
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<VaultEntry[]> {
const command = isTauri() ? 'reload_vault' : 'list_vault'
return loadVaultEntriesWithCommand({ vaultPath, command })
}
export function reloadVaultEntries({ vaultPath, vaults, defaultWorkspacePath }: MountedVaultEntriesOptions): Promise<VaultEntry[]> {
if (vaults?.length) return loadMountedVaultEntries({ vaultPath, vaults, defaultWorkspacePath, forceReload: true })
return loadVaultEntriesWithCommand({ vaultPath, command: 'reload_vault' })
}
export function loadVaultFolders({ vaultPath }: VaultPathOptions): Promise<FolderNode[]> {
return tauriCall<FolderNode[]>({ command: 'list_vault_folders', tauriArgs: { path: vaultPath } })
}
export async function loadMountedVaultFolders(options: MountedVaultFoldersOptions): Promise<FolderNode[]> {
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<ViewFile[]> {
return tauriCall<unknown>({ command: 'list_views', tauriArgs: { vaultPath } })
.then(normalizeViewFiles)
}
export async function loadMountedVaultViews(options: MountedVaultViewsOptions): Promise<ViewFile[]> {
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<LoadedVaultData> {
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<LoadedVaultChrome> {
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<GitPushResult> {
if (!isTauri()) {
await mockInvoke<string>('git_commit', { message })
return mockInvoke<GitPushResult>('git_push', {})
}
await invoke<string>('git_commit', { vaultPath, message })
return invoke<GitPushResult>('git_push', { vaultPath })
}