#!/usr/bin/env bun /** * Interactive file finder demo * * Usage: * bunx fff-demo [directory] * bun examples/search.ts [directory] * * Indexes the specified directory (or cwd) and provides an interactive * search prompt with detailed metadata about results. * * Modes: * :files - fuzzy file search (default) * :dirs - fuzzy directory search * :mixed - mixed file + directory search */ import { FileFinder } from "../src/index"; import type { DirItem, FileItem, MixedItem, Score } from "../src/index"; import * as readline from "node:readline"; import { join } from "node:path"; import { homedir } from "node:os"; const RESET = "\x1b[0m"; const BOLD = "\x1b[1m"; const DIM = "\x1b[2m"; const GREEN = "\x1b[32m"; const YELLOW = "\x1b[33m"; const BLUE = "\x1b[34m"; const CYAN = "\x1b[36m"; const MAGENTA = "\x1b[35m"; const RED = "\x1b[31m"; type SearchMode = "files" | "dirs" | "mixed"; function formatGitStatus(status: string): string { switch (status) { case "modified": return `${YELLOW}M${RESET}`; case "untracked": return `${GREEN}?${RESET}`; case "added": return `${GREEN}A${RESET}`; case "deleted": return `${RED}D${RESET}`; case "renamed": return `${BLUE}R${RESET}`; case "clean": case "current": return `${DIM} ${RESET}`; default: return `${DIM}${status.charAt(0)}${RESET}`; } } /** Pad a plain string first, then wrap with ANSI color. */ function padColor( value: string, width: number, color: string, align: "left" | "right" = "right", ): string { const padded = align === "right" ? value.padStart(width) : value.padEnd(width); return `${color}${padded}${RESET}`; } function scoreColor(score: number): string { if (score >= 100) return GREEN; if (score >= 50) return YELLOW; return DIM; } function formatSize(bytes: number): string { if (bytes < 1024) return `${bytes}B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`; return `${(bytes / 1024 / 1024).toFixed(1)}M`; } function formatTime(unixSeconds: number): string { if (unixSeconds === 0) return "unknown"; const date = new Date(unixSeconds * 1000); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return "just now"; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); } function formatModeTag(mode: SearchMode): string { switch (mode) { case "files": return `${CYAN}files${RESET}`; case "dirs": return `${MAGENTA}dirs${RESET}`; case "mixed": return `${YELLOW}mixed${RESET}`; } } function printScoreBreakdown(score: Score, indent: string) { const breakdown: string[] = []; if (score.baseScore > 0) breakdown.push(`base:${score.baseScore}`); if (score.filenameBonus > 0) breakdown.push(`filename:+${score.filenameBonus}`); if (score.frecencyBoost > 0) breakdown.push(`frecency:+${score.frecencyBoost}`); if (score.comboMatchBoost > 0) breakdown.push(`combo:+${score.comboMatchBoost}`); if (score.distancePenalty < 0) breakdown.push(`distance:${score.distancePenalty}`); if (score.exactMatch) breakdown.push(`${GREEN}exact${RESET}`); if (score.matchType) breakdown.push(`[${score.matchType}]`); if (breakdown.length > 0) { console.log(`${DIM}${indent}└─ ${breakdown.join(", ")}${RESET}`); } } function printFileResult(item: FileItem, score: Score, showBreakdown: boolean) { const git = formatGitStatus(item.gitStatus); const sc = padColor(String(score.total), 5, scoreColor(score.total)); const size = formatSize(item.size).padStart(6); const modified = formatTime(item.modified).padEnd(10); console.log(` ${git} │ ${sc} │ ${size} │ ${modified} │ ${item.relativePath}`); if (showBreakdown && score.total > 0) { printScoreBreakdown(score, " │ │ │ │ "); } } function printDirResult(item: DirItem, score: Score, showBreakdown: boolean) { const sc = padColor(String(score.total), 5, scoreColor(score.total)); const frecency = ( item.maxAccessFrecency > 0 ? `f:${item.maxAccessFrecency}` : "-" ).padStart(6); console.log( ` ${MAGENTA}D${RESET} │ ${sc} │ ${frecency} │ ${"".padEnd(10)} │ ${MAGENTA}${item.relativePath}${RESET}`, ); if (showBreakdown && score.total > 0) { printScoreBreakdown(score, " │ │ │ │ "); } } function printMixedResult(mixed: MixedItem, score: Score, showBreakdown: boolean) { if (mixed.type === "file") { printFileResult(mixed.item, score, showBreakdown); } else { printDirResult(mixed.item, score, showBreakdown); } } async function main() { const targetDir = process.argv[2] || process.cwd(); console.log(`${BOLD}${CYAN}fff - Fast File Finder Demo${RESET}\n`); // Check library availability if (!FileFinder.isAvailable()) { console.error(`${RED}Error: Native library not found.${RESET}`); console.error("Build with: cargo build --release -p fff-c"); process.exit(1); } // Use the same frecency + history databases as the Neovim plugin // so the demo benefits from real access history. const nvimCache = process.env.XDG_CACHE_HOME || join(homedir(), ".cache", "nvim"); const nvimData = process.env.XDG_DATA_HOME || join(homedir(), ".local", "share", "nvim"); const frecencyDbPath = join(nvimCache, "fff_nvim"); const historyDbPath = join(nvimData, "fff_queries"); console.log(`${DIM}Initializing index for: ${targetDir}${RESET}`); console.log(`${DIM}Frecency DB: ${frecencyDbPath}${RESET}`); const createResult = FileFinder.create({ basePath: targetDir, frecencyDbPath, historyDbPath, useUnsafeNoLock: true, }); if (!createResult.ok) { console.error(`${RED}Init failed: ${createResult.error}${RESET}`); process.exit(1); } const finder = createResult.value; // Wait for scan with progress process.stdout.write(`${DIM}Scanning files...${RESET}`); const startTime = Date.now(); let lastCount = 0; while (finder.isScanning()) { const progress = finder.getScanProgress(); if (progress.ok && progress.value.scannedFilesCount !== lastCount) { lastCount = progress.value.scannedFilesCount; process.stdout.write(`\r${DIM}Scanning files... ${lastCount}${RESET} `); } await new Promise((r) => setTimeout(r, 50)); } const scanTime = Date.now() - startTime; const finalProgress = finder.getScanProgress(); const totalFiles = finalProgress.ok ? finalProgress.value.scannedFilesCount : 0; console.log( `\r${GREEN}✓${RESET} Indexed ${BOLD}${totalFiles}${RESET} files in ${scanTime}ms\n`, ); // Show index info const health = finder.healthCheck(); if (health.ok) { console.log(`${DIM}Version:${RESET} ${health.value.version}`); console.log(`${DIM}Base path:${RESET} ${health.value.filePicker.basePath}`); if (health.value.git.repositoryFound) { console.log(`${DIM}Git root:${RESET} ${health.value.git.workdir}`); } } let mode: SearchMode = "files"; // Interactive search loop const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); console.log(`\n${BOLD}Commands:${RESET}`); console.log(` ${DIM}:files${RESET} - file search mode (default)`); console.log(` ${DIM}:dirs${RESET} - directory search mode`); console.log(` ${DIM}:mixed${RESET} - mixed files + directories mode`); console.log(` ${DIM}q${RESET} - quit`); console.log(); const prompt = () => { rl.question(`${CYAN}[${formatModeTag(mode)}]>${RESET} `, (query) => { const trimmed = query.trim(); if (trimmed.toLowerCase() === "q" || trimmed.toLowerCase() === "quit") { console.log(`\n${DIM}Goodbye!${RESET}`); finder.destroy(); rl.close(); process.exit(0); } // Mode switching commands if (trimmed === ":files") { mode = "files"; console.log(`${GREEN}Switched to file search mode${RESET}\n`); prompt(); return; } if (trimmed === ":dirs") { mode = "dirs"; console.log(`${MAGENTA}Switched to directory search mode${RESET}\n`); prompt(); return; } if (trimmed === ":mixed") { mode = "mixed"; console.log(`${YELLOW}Switched to mixed search mode${RESET}\n`); prompt(); return; } const searchStart = Date.now(); if (mode === "files") { const result = finder.fileSearch(trimmed, { pageSize: 15 }); const searchTime = Date.now() - searchStart; if (!result.ok) { console.log(`${RED}Search error: ${result.error}${RESET}\n`); prompt(); return; } const { items, scores, totalMatched, totalFiles } = result.value; console.log(); console.log( `${DIM}Found ${BOLD}${totalMatched}${RESET}${DIM} matches in ${totalFiles} files (${searchTime}ms)${RESET}`, ); console.log(); if (items.length === 0) { console.log(`${DIM}No matches found.${RESET}\n`); prompt(); return; } console.log(`${DIM} Git │ Score │ Size │ Modified │ Path${RESET}`); for (let i = 0; i < items.length; i++) { printFileResult(items[i], scores[i], i < 3); } if (totalMatched > items.length) { console.log( `${DIM} │ │ │ │ ... and ${totalMatched - items.length} more${RESET}`, ); } } else if (mode === "dirs") { const result = finder.directorySearch(trimmed, { pageSize: 15 }); const searchTime = Date.now() - searchStart; if (!result.ok) { console.log(`${RED}Search error: ${result.error}${RESET}\n`); prompt(); return; } const { items, scores, totalMatched, totalDirs } = result.value; console.log(); console.log( `${DIM}Found ${BOLD}${totalMatched}${RESET}${DIM} matches in ${totalDirs} directories (${searchTime}ms)${RESET}`, ); console.log(); if (items.length === 0) { console.log(`${DIM}No matches found.${RESET}\n`); prompt(); return; } console.log(`${DIM} Dir │ Score │ Frecnc │ │ Path${RESET}`); for (let i = 0; i < items.length; i++) { printDirResult(items[i], scores[i], i < 3); } if (totalMatched > items.length) { console.log( `${DIM} │ │ │ │ ... and ${totalMatched - items.length} more${RESET}`, ); } } else { // mixed mode const result = finder.mixedSearch(trimmed, { pageSize: 20 }); const searchTime = Date.now() - searchStart; if (!result.ok) { console.log(`${RED}Search error: ${result.error}${RESET}\n`); prompt(); return; } const { items, scores, totalMatched, totalFiles, totalDirs } = result.value; console.log(); console.log( `${DIM}Found ${BOLD}${totalMatched}${RESET}${DIM} matches (${totalFiles} files, ${totalDirs} dirs) (${searchTime}ms)${RESET}`, ); console.log(); if (items.length === 0) { console.log(`${DIM}No matches found.${RESET}\n`); prompt(); return; } console.log(`${DIM} Type │ Score │ Info │ Modified │ Path${RESET}`); for (let i = 0; i < items.length; i++) { printMixedResult(items[i], scores[i], i < 5); } if (totalMatched > items.length) { console.log( `${DIM} │ │ │ │ ... and ${totalMatched - items.length} more${RESET}`, ); } } console.log(); prompt(); }); }; prompt(); } main().catch((err) => { console.error(`${RED}Fatal error: ${err.message}${RESET}`); process.exit(1); });