#!/usr/bin/env bun /** * Interactive live grep demo * * Usage: * bun examples/grep.ts [directory] [--mode=plain|regex|fuzzy] * * Indexes the specified directory (or cwd) and provides an interactive * content search prompt with match highlighting. */ import { FileFinder } from "../src/index"; import type { GrepMode } from "../src/types"; import * as readline from "node:readline"; 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 RED = "\x1b[31m"; const BG_YELLOW = "\x1b[43m"; const BLACK = "\x1b[30m"; 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}`; } } /** * Highlight match ranges within line content using ANSI escape codes. * The match_ranges are byte offsets into line_content. */ function highlightLine(content: string, ranges: [number, number][]): string { if (ranges.length === 0) return content; // Convert the string to a buffer to work with byte offsets const buf = Buffer.from(content, "utf-8"); const parts: string[] = []; let lastEnd = 0; for (const [start, end] of ranges) { // Clamp to valid range const s = Math.max(0, Math.min(start, buf.length)); const e = Math.max(s, Math.min(end, buf.length)); if (s > lastEnd) { parts.push(buf.subarray(lastEnd, s).toString("utf-8")); } parts.push(`${BG_YELLOW}${BLACK}${buf.subarray(s, e).toString("utf-8")}${RESET}`); lastEnd = e; } if (lastEnd < buf.length) { parts.push(buf.subarray(lastEnd).toString("utf-8")); } return parts.join(""); } function parseArgs(): { directory: string; mode: GrepMode } { let directory = process.cwd(); let mode: GrepMode = "plain"; for (const arg of process.argv.slice(2)) { if (arg.startsWith("--mode=")) { const m = arg.slice(7); if (m === "plain" || m === "regex" || m === "fuzzy") { mode = m; } else { console.error(`Unknown mode: ${m}. Use plain, regex, or fuzzy.`); process.exit(1); } } else if (!arg.startsWith("-")) { directory = arg; } } return { directory, mode }; } async function main() { const { directory, mode } = parseArgs(); console.log(`${BOLD}${CYAN}fff - Live Grep Demo${RESET}`); console.log(`${DIM}Mode: ${mode}${RESET}\n`); 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); } console.log(`${DIM}Initializing index for: ${directory}${RESET}`); const createResult = FileFinder.create({ basePath: directory, }); if (!createResult.ok) { console.error(`${RED}Init failed: ${createResult.error}${RESET}`); process.exit(1); } const finder = createResult.value; // Wait for scan process.stdout.write(`${DIM}Scanning files...${RESET}`); const startTime = Date.now(); while (finder.isScanning()) { const progress = finder.getScanProgress(); if (progress.ok) { process.stdout.write( `\r${DIM}Scanning files... ${progress.value.scannedFilesCount}${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; const warmupDone = finalProgress.ok ? finalProgress.value.isWarmupComplete : false; console.log( `\r${GREEN}✓${RESET} Indexed ${BOLD}${totalFiles}${RESET} files in ${scanTime}ms`, ); console.log( ` Content index: ${warmupDone ? `${GREEN}ready${RESET}` : `${YELLOW}building in background...${RESET}`}\n`, ); console.log( `${BOLD}Enter a search pattern${RESET} (or 'q' to quit, ':mode plain|regex|fuzzy' to switch):\n`, ); console.log( `${DIM}Tip: prefix with *.ext to filter by extension, e.g. "*.ts useState"${RESET}\n`, ); let currentMode = mode; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const prompt = () => { const modeLabel = currentMode === "plain" ? "txt" : currentMode === "regex" ? "re" : "fzy"; const prog = finder.getScanProgress(); const indexed = prog.ok && prog.value.isWarmupComplete; const indexLabel = indexed ? `${GREEN}idx${RESET}` : `${YELLOW}no-idx${RESET}`; rl.question(`${CYAN}grep[${modeLabel}|${indexLabel}]>${RESET} `, (query) => { if (query.toLowerCase() === "q" || query.toLowerCase() === "quit") { console.log(`\n${DIM}Goodbye!${RESET}`); finder.destroy(); rl.close(); process.exit(0); } // Handle mode switching if (query.startsWith(":mode ")) { const newMode = query.slice(6).trim(); if (newMode === "plain" || newMode === "regex" || newMode === "fuzzy") { currentMode = newMode; console.log(`${DIM}Switched to ${currentMode} mode${RESET}\n`); } else { console.log( `${RED}Unknown mode: ${newMode}. Use plain, regex, or fuzzy.${RESET}\n`, ); } prompt(); return; } if (query.trim() === "") { prompt(); return; } const searchStart = Date.now(); const result = finder.grep(query, { mode: currentMode, pageLimit: 30, timeBudgetMs: 5000, }); const searchTime = Date.now() - searchStart; if (!result.ok) { console.log(`${RED}Grep error: ${result.error}${RESET}\n`); prompt(); return; } const { items, totalMatched, totalFilesSearched, totalFiles: _, filteredFileCount, nextCursor, regexFallbackError, } = result.value; console.log(); if (regexFallbackError) { console.log( `${YELLOW}Regex error: ${regexFallbackError} (fell back to literal match)${RESET}`, ); } console.log( `${DIM}${BOLD}${totalMatched}${RESET}${DIM} matches across ${totalFilesSearched}/${filteredFileCount} files (${searchTime}ms)${RESET}`, ); console.log(); if (items.length === 0) { console.log(`${DIM}No matches found.${RESET}\n`); prompt(); return; } // Group matches by file for display let lastFile = ""; for (const match of items) { if (match.relativePath !== lastFile) { lastFile = match.relativePath; const git = formatGitStatus(match.gitStatus); console.log(`${BOLD}${BLUE}${match.relativePath}${RESET} ${git}`); } const lineNum = String(match.lineNumber).padStart(4); const highlighted = highlightLine(match.lineContent, match.matchRanges); let suffix = ""; if (match.fuzzyScore !== undefined) { suffix = ` ${DIM}(score: ${match.fuzzyScore})${RESET}`; } console.log(`${DIM}${lineNum}:${RESET} ${highlighted}${suffix}`); } if (nextCursor) { console.log(`\n${DIM}... more results available${RESET}`); } console.log(); prompt(); }); }; prompt(); } main().catch((err) => { console.error(`${RED}Fatal error: ${err.message}${RESET}`); process.exit(1); });