A single-binary terminal TUI for search-and-replace across directory trees. Searches are literal by default; regex syntax is opt-in via --regex. No TUI framework — raw ANSI escapes + platform FFI for terminal control.
main.rs → CLI parse, pattern compile, search, model init, app::run()
lib.rs → pub mod re-exports (enables integration tests)
cli.rs → Manual arg parsing (no clap). CliArgs struct. Flags: -v/--version, -i, -r/--regex, -e, --hidden, --no-ignore.
model.rs → SearchResult { file_path, line_num, line_text }, AppState, AppMode, Model
search.rs → Pipelined file walking + parallel bytes::Regex search, literal prefilter, hidden/gitignore skipping
replace.rs → Atomic file replacement via temp file + rename
term.rs → Raw mode FFI (Windows kernel32 / Unix termios), ANSI escapes, Key enum, paint()
ui.rs → Screen rendering into a String (render() → term::paint())
app.rs → Event loop: read_key → dispatch → render cycle
integration.rs → Editor integration helpers (Vim result-file writer)
exclude.rs → Path exclusion (dir/, *.ext, exact filename patterns). Uses Cow for path normalization.
filedetect.rs → Text vs binary detection (64 text extensions + 512-byte content probe + SIMD null-byte scan)
gitignore.rs → .gitignore/.ignore/.grefignore parsing (glob→regex), hierarchical rule merging, ancestor discovery
Data flow: cli::parse() → search::compile_search_pattern() → search::perform_search_adaptive() → Model::new() → app::run() (loop: ui::render() → term::paint() → term::read_key() → state update) → replace::perform_replacements_with_options().
- Zero external TUI deps: Terminal is managed via direct platform FFI (
SetConsoleModeon Windows,tcsetattron Unix). Seeterm.rsplatform modules. - Unix raw-mode invariant:
term.rsstill uses hard-codedtermiosbyte offsets. On Linux,c_cc[VTIME]is byte 22 andc_cc[VMIN]is byte 23; raw mode must keepVTIME=1,VMIN=0so loneEscresolves via timeout instead of blocking the event loop. - Minimal deps:
regex = "1"+memchr = "2"(memchr is already a transitive dep of regex) — no clap, crossterm, walkdir, or rayon. - Flicker-free rendering:
term::paint()uses cursor-home + per-line clear-to-EOL + clear-to-EOS in a single locked stdout write. Never useCLEAR_SCREEN(\x1b[2J). - UTF-8 safe slicing: Horizontal offset uses
char_indices().nth()— never byte-index into display strings (seeui.rs). - Literal default / regex opt-in:
search::compile_search_pattern()escapes user patterns unless--regexis set. In literal mode, replacement text is literal too, so$1is written as$1; capture expansion is a regex-mode behavior. - Compiled-pattern-consistent UI highlighting:
ui.rsmust render highlights and selected replacement previews withModel.pattern, not rawpattern_str, so regex searches such as1.2.0visibly mark the actual match. - Atomic replacement:
replace_in_file()writes to a temp file (.gref_tmp_*) then renames over the original. - Bounded byte-preserving replacement:
replace.rsrewrites selected lines withregex::bytes::Regexsemantics, preserving non-UTF-8 bytes outside matches and streaming output directly to the temp file. Selected lines are buffered only up toMAX_REPLACE_LINE_BYTES(64 MiB); larger selected lines fail cleanly instead of aborting on allocator growth. - Parallel search: Pipelined walk+search —
walk_and_dispatchsends(PathBuf, u64)to a job channel as files are discovered; worker threads start searching immediately viaArc<Mutex<Receiver>>. Workers accumulate results locally inVecand return them (no result channel). - Bytes-level regex engine:
search.rsusesregex::bytes::Regexinternally (from theregexcrate, no extra dep), including escaped literal patterns. Only matching lines are converted to UTF-8 viafrom_utf8_lossy. Non-matching lines skip UTF-8 conversion entirely. - Whole-buffer regex search:
search_file()feeds the entire file buffer topattern.find_iter(content)instead of iterating line-by-line. This lets the regex engine's internal SIMD/Teddy/Aho-Corasick optimizations work on the full buffer. Line boundaries are resolved only for matching lines using SIMD-acceleratedmemchr::memrchr(backward) andmemchr::memchr(forward). Incremental line counting usesmemchr::memchr_iter. - SIMD literal prefilter:
extract_longest_literal()finds the longest ≥3-char literal substring in a regex pattern. Amemchr::memmem::Finderis pre-built once (viainto_owned()for'staticlifetime), shared across worker threads viaArc, and used for whole-file quick-reject before engaging the regex engine. - Skip strategy: Hidden files/dirs (name starts with
.) are skipped by default outside Git repo roots. When the search root contains a.gitdirectory,search::default_skip_hidden()includes hidden items by default so paths such as.github/are searchable;.gititself is still always skipped viaSKIP_DIRS. The walker also detects nested Git repo roots while descending from a non-repo parent and stops skipping hidden entries from that repo root downward..gitignore,.ignore, and.grefignorefiles are parsed and applied hierarchically — ancestor ignore files up to the repo root are loaded at walk start, per-directory ignore files are merged during walk viamerge_dir().--hiddenincludes hidden items,--no-ignoredisables ignore-file parsing. Seegitignore.rs. - Zero-copy path filtering: During directory walk, OsStr-based checks (hidden prefix, SKIP_DIRS binary search, gitignore basename match) run on
entry.file_name()beforeentry.path()allocates the full PathBuf. Files that will be discarded never trigger path allocation. - Deferred binary detection: Known extensions are classified without I/O. Files with unknown extensions are dispatched to workers and binary-checked via SIMD-accelerated
memchr(0, ...)on the first 512 bytes of the already-loaded buffer — no separate file open. - Vim integration:
--vim-result <file>enables Vim popup hosting.main.rswrites a small atomic status protocol viaintegration.rs:selected\nline\ncolumn\npath,none,error\nmessage,replaced, orcancelled. Vim runtime files live incontrib/vim/and use built-interm_start()+popup_create()only. On terminal exit, the callback must runstopinsertand queue an unmappedEsc;stopinsertalone leaves Vim's physical terminal screen stale and can duplicate the statusline. Result callbacks schedule a delayedredraw!only after restoring the original window and, for replacements, runningchecktime. In Vim-hosted mode,Enteropens a selected search result and thevexternal-editor key is disabled. - Release packaging:
.github/workflows/build.ymlpackages each platform as a directory archive containingbin/,vim/plugin,vim/autoload,install.sh,install.ps1,README.md, andLICENSE; tagged releases also publishSHA256SUMS. Rootinstall.sh/install.ps1are bootstrap installers for curl/PowerShell download-and-verify installs, and also install from an extracted release archive. Release tags usevX.Y.Zand CI fails early if the tag does not matchCargo.tomlversion = "X.Y.Z". - Avoid allocations in hot paths:
exclude.rsusesCowfor path normalization (only allocates on Windows when backslashes present).filedetect.rsusesString::with_capacity+pushinstead offormat!.ui.rsusesstr::match_indices()instead of compiling a regex per render frame.
cargo build # dev build
cargo build --release # release (strip=true, lto=true, opt-level=3)
cargo test # unit + stress/edge-case + Vim runtime tests
cargo clippy # must pass with 0 warnings- Unit tests: Inline
#[cfg(test)] mod testsincli.rs,exclude.rs,filedetect.rs,gitignore.rs,search.rs,replace.rs - Integration/stress tests:
tests/stress_tests.rs— usesgref::(lib) imports, covers all modules including UI rendering and key-handling simulation - Vim runtime test:
tests/vim_runtime_tests.rslaunchesvim -Nu NONE -n -esto validate Vimscript parsing and result protocol; skips cleanly whenvimis unavailable - Tests use
std::env::temp_dir()for file I/O withgref_stress_*/gref_test_*prefixed filenames make_result(file, line_num, line_text)helper createsSearchResultin tests (3 args, no match_text)
- "Thou Shalt Not Search Line By Line": Feeding regex one line at a time defeats its internal SIMD optimizations. Always search the whole buffer at once with
find_iter(), then resolve line boundaries only for matches. SKIP_DIRS: 14 hardcoded dirs skipped during walk (.git,node_modules,target,.venv, etc.) — sorted forbinary_searchwalk_and_dispatch: Usesentry.file_type()fromread_dir(avoids redundant stat syscalls vspath.is_dir()). Skips hidden entries whenskip_hidden=true; CLI callers derive this viasearch::default_skip_hidden(), which disables hidden skipping for roots containing.git. Loads.gitignore,.ignore,.grefignoreper directory viamerge_dir()and checks rules viaGitIgnore::is_ignored(). Stack carriesArc<GitIgnore>—Arc::cloneis O(1) when no ignore files found. OsStr-based checks run beforeentry.path()to avoid allocation for discarded entries.gitignore.rs: Glob-to-regex conversion handles*,**,?,[...],\. Basename-only matching (patterns with/in middle are skipped). Negation via!, dir-only via trailing/.load_ancestor_gitignores()walks up from root to repo root (.gitdir) loading.gitignore,.ignore,.grefignoreat each level. Hierarchical merging: parent rules first, child rules last (last match wins). Priority order:.gitignore<.ignore<.grefignore.search_file: Unified search function — reads file into memory, whole-file literal reject viamemmem::Finder(SIMD), whole-bufferfind_iter(), SIMD line boundary detection viamemchr/memrchr, incremental line counting viamemchr_iter, dedup matches on same lineperform_search_adaptive: Accepts either a directory root or an explicit single-file root. Single-file roots reuse the same whole-buffersearch_file()path and are used by:GrefBuffer.MAX_FILE_SIZE: 256 MB — files larger than this are skipped entirelymemmem::Finderis pre-built once withinto_owned()for'staticlifetime, wrapped inArcfor thread sharing- No separate small/large file paths — single unified
search_filefor all file sizes
- All public API in modules;
lib.rsre-exports,main.rsconsumes viause gref::* - Error handling:
Result<(), String>for fallible ops;eprintln!+process::exit(1)only inmain() - No
unwrap()in library code paths that touch user files — propagate errors - Style helpers in
term.rs(style_red,style_green, etc.) wrap text with ANSI codes + RESET Modelis the single source of truth — all state mutation happens inapp.rskey handlers;ui.rsis pure rendering- When adding new file extensions, maintain sorted order in
filedetect.rsarrays (binary search) - Prefer
Cow/ non-allocating checks overformat!/Stringin hot paths - Keep clippy at 0 warnings — use
strip_suffix/strip_prefixinstead of manual slicing, collapse nested ifs
- Treat this file as the repo's durable memory.
- When you learn a new repo-specific fact that is likely to matter for future work, add it here or fold it into the most relevant existing section.
- Keep additions compact and high-signal: record only important architecture, invariants, workflows, performance constraints, or test conventions.
- Do not add temporary notes, one-off debugging details, obvious observations, or duplicate information.