README.md

May 10, 2026 · View on GitHub

treegrep

treegrep is a regex pattern matcher that displays results in a tree structure with an interface to jump to matched text.

test release

demo video

examples, editor integrations, and help.

crates.io | GitHub | AUR | NetBSD

installation

  • cargo: cargo install treegrep
  • releases: Download from releases
  • manual:
    git clone https://github.com/4imothy/treegrep
    cd treegrep
    cargo build --release
    

editor integrations

neovim
return {
    '4imothy/treegrep',
    build = function()
        require('treegrep').build_tgrep()
    end,
    config = function()
        require('treegrep').setup({
            selection_file = '/tmp/tgrep-select',
            repeat_file = '/tmp/tgrep-repeat',
        })
        vim.keymap.set('n', '<leader>tt', function() require('treegrep').tgrep_with('--menu --live') end)
        vim.keymap.set('n', '<leader>tr', function() require('treegrep').tgrep_with('--repeat --select --live') end)
        vim.keymap.set('n', '<leader>tf', function() require('treegrep').tgrep_with('--files --select --live') end)
    end,
}
helix
  • sample keybind to run treegrep and open selection
space.t = [
    ':sh rm -f /tmp/tgrep-select',
    ':insert-output tgrep --menu --live --selection-file=/tmp/tgrep-select --repeat-file=/tmp/tgrep-repeat > /dev/tty',
    ':open %sh{ f=$(sed -n 1p /tmp/tgrep-select); l=$(sed -n 2p /tmp/tgrep-select); [ -n "$l" ] && echo "$f:$l" || echo "$f"; }',
    ':redraw',
    ':set-option mouse false',
    ':set-option mouse true',
]
vim
Plug '4imothy/treegrep', {'do': {-> TgrepBuild()}}

let g:tgrep_selection_file = '/tmp/tgrep-select'
let g:tgrep_repeat_file = '/tmp/tgrep-repeat'

nnoremap <leader>tt :call TgrepWith('--menu')<cr>
nnoremap <leader>tr :call TgrepWith('--repeat --select')<cr>
nnoremap <leader>tf :call TgrepWith('--files --select')<cr>

examples

tgrep --regexp \bstruct\s+\w+ --regexp \bimpl\s+\w+ --path src --line-number --context=1 --count
src: 9
├──term.rs: 1
│  ├──-1: 
│  ├──15: pub struct Term<'a> {
│  ╰──+1:     pub height: u16,
├──style.rs: 1
│  ├──-1: 
│  ├──23: pub struct DisplayRepeater<T>(T, usize);
│  ╰──+1: impl<T: Display> Display for DisplayRepeater<T> {
├──matcher.rs: 3
│  ├──-1: 
│  ├──29: struct Matcher {
│  ├──+1:     combined: RegexMatcher,
│  ├──-1: 
│  ├──34: impl Matcher {
│  ├──+1:     fn new(patterns: &[String]) -> Result<Self, Message> {
│  ├──-1: 
│  ├──53: struct MatchSink<'a> {
│  ╰──+1:     lines: Vec<Line>,
├──errors.rs: 4
│  ├──-1: 
│  ├──14: pub struct Message {
│  ├──+1:     pub mes: String,
│  ├──-1: }
│  ├──17: impl Error for Message {}
│  ├──+1: 
│  ├──-1: 
│  ├──34: impl fmt::Debug for Message {
│  ├──+1:     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
│  ├──-1: 
│  ├──40: impl fmt::Display for Message {
│  ╰──+1:     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
├──match_system.rs: 8
│  ├──-1: 
│  ├──23: pub struct Directory {
│  ├──+1:     pub path: PathBuf,
│  ├──-1: 
│  ├──30: impl Directory {
│  ├──+1:     pub fn new(path: &Path, links: bool) -> Result<Self, Message> {
│  ├──-1: 
│  ├──41: pub struct File {
│  ├──+1:     pub path: PathBuf,
│  ├──-1: 
│  ├──47: impl File {
│  ├──+1:     pub fn from_pathbuf(path: PathBuf, links: bool) -> Result<Self, Message> {
│  ├──-1: #[cfg_attr(test, derive(PartialEq, Debug))]
│  ├──73: pub struct Match {
│  ├──+1:     pub regexp_id: usize,
│  ├──-1: 
│  ├──79: impl Match {
│  ├──+1:     pub fn new(regexp_id: usize, start: usize, end: usize) -> Self {
│  ├──-1: 
│  ├──104: pub struct Line {
│  ├──+1:     pub content: String,
│  ├──-1: 
│  ├──111: impl Line {
│  ╰──+1:     pub fn new(content: String, mut matches: Vec<Match>, line_num: usize) -> Self {
├──args.rs: 6
│  ├──-1: 
│  ├──25: impl ValueEnum for OpenStrategy {
│  ├──+1:     fn value_variants<'a>() -> &'a [Self] {
│  ├──-1: #[derive(Clone)]
│  ├──83: pub struct ColorParser;
│  ├──+1: 
│  ├──85: impl clap::builder::TypedValueParser for ColorParser {
│  ├──+1:     type Value = Color;
│  ├──-1: #[derive(Clone)]
│  ├──142: pub struct KeyCodeParser;
│  ├──+1: 
│  ├──144: impl clap::builder::TypedValueParser for KeyCodeParser {
│  ├──+1:     type Value = KeyCode;
│  ├──-1: )]
│  ├──310: pub struct Args {
│  ╰──+1:     #[arg(
├──config.rs: 7
│  ├──-1: #[derive(Clone)]
│  ├──28: pub struct KeyBindings {
│  ├──+1:     pub down: Vec<KeyCode>,
│  ├──-1: #[derive(Clone)]
│  ├──52: pub struct Characters {
│  ├──+1:     pub bl: char,
│  ├──-1: #[derive(Clone)]
│  ├──73: pub struct Colors {
│  ├──+1:     pub file: Color,
│  ├──-1: 
│  ├──85: impl args::Color {
│  ├──+1:     fn get(&self) -> Color {
│  ├──-1: #[derive(Clone)]
│  ├──104: pub struct CoreConfig {
│  ├──+1:     pub selection_file: Option<PathBuf>,
│  ├──-1: #[derive(Clone)]
│  ├──121: pub struct Config {
│  ├──+1:     pub path: PathBuf,
│  ├──-1: 
│  ├──410: impl Config {
│  ╰──+1:     pub fn get_styling(matches: &ArgMatches) -> (bool, bool) {
├──writer.rs: 19
│  ├──-1: 
│  ├──57: impl HighlightEvent<'_> {
│  ├──+1:     fn priority(&self) -> u8 {
│  ├──-1: 
│  ├──158: pub struct OpenInfo<'a> {
│  ├──+1:     pub path: &'a Path,
│  ├──-1: 
│  ├──171: pub struct WithFilter<'a> {
│  ├──+1:     pub entry: &'a dyn Entry,
│  ├──-1: 
│  ├──176: impl Display for WithFilter<'_> {
│  ├──+1:     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
│  ├──-1: 
│  ├──182: struct PathDisplay {
│  ├──+1:     prefix: Option<Vec<PrefixComponent>>,
│  ├──-1: 
│  ├──193: impl PathDisplay {
│  ├──+1:     fn new(
│  ├──-1: 
│  ├──226: impl Entry for PathDisplay {
│  ├──+1:     fn render(&self, f: &mut fmt::Formatter, filter: &str) -> fmt::Result {
│  ├──-1: 
│  ├──265: impl Display for PathDisplay {
│  ├──+1:     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
│  ├──-1: 
│  ├──323: struct LineDisplay {
│  ├──+1:     prefix: Vec<PrefixComponent>,
│  ├──-1: 
│  ├──334: impl Entry for LineDisplay {
│  ├──+1:     fn render(&self, f: &mut fmt::Formatter, filter: &str) -> fmt::Result {
│  ├──-1: 
│  ├──453: impl Display for LineDisplay {
│  ├──+1:     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
│  ├──-1: 
│  ├──459: struct LongBranchDisplay {
│  ├──+1:     prefix: Vec<PrefixComponent>,
│  ├──-1: 
│  ├──466: impl Entry for LongBranchDisplay {
│  ├──+1:     fn render(&self, f: &mut fmt::Formatter, filter: &str) -> fmt::Result {
│  ├──-1: 
│  ├──517: impl Display for LongBranchDisplay {
│  ├──+1:     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
│  ├──-1: 
│  ├──523: struct OverviewDisplay {
│  ├──+1:     dirs: usize,
│  ├──-1: 
│  ├──532: impl Entry for OverviewDisplay {
│  ├──+1:     fn render(&self, f: &mut fmt::Formatter, _filter: &str) -> fmt::Result {
│  ├──-1: 
│  ├──572: impl Display for OverviewDisplay {
│  ├──+1:     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
│  ├──-1: 
│  ├──594: impl Directory {
│  ├──+1:     fn to_lines(
│  ├──-1: 
│  ├──699: impl File {
│  ╰──+1:     fn to_lines(
╰──menu.rs: 9
   ├──-1: 
   ├──59: impl ViewAnchor {
   ├──+1:     fn next(&mut self) {
   ├──-1: 
   ├──69: struct DoubleClick {
   ├──+1:     down_row: u16,
   ├──-1: 
   ├──74: impl DoubleClick {
   ├──+1:     fn new() -> Self {
   ├──-1: 
   ├──101: struct Window {
   ├──+1:     first: isize,
   ├──-1: 
   ├──106: impl Window {
   ├──+1:     fn new() -> Self {
   ├──-1: 
   ├──339: struct CurrentResults {
   ├──+1:     lines: Vec<Box<dyn Entry>>,
   ├──-1: 
   ├──343: impl CurrentResults {
   ├──+1:     fn new(matches: Matches, config: Arc<Config>) -> io::Result<Self> {
   ├──-1: 
   ├──351: pub struct Menu<'a, 'b> {
   ├──+1:     in_menu: bool,
   ├──-1: 
   ├──1496: impl OpenStrategy {
   ╰──+1:     fn from(editor: &str) -> Self {
tgrep Print src/menu.rs --trim --line-number --char-vertical=| --char-horizontal=- --char-top-left=+ --char-top-right=+ --char-bottom-left=+ --char-bottom-right=+ --char-tee=+
menu.rs
+--19: style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor},
+--253: Print(format!(
+--264: Print(format!(
+--276: Print(format!(
+--528: queue!(self.term, Print(WithFilter { entry, filter }))?;
+--531: queue!(self.term, Print(&cfg.chars.ellipsis))?;
+--550: Print(style::style_with(
+--558: queue!(self.term, Print(cfg.chars.selected_indicator.as_str()))?;
+--579: Print(cfg.chars.selected_indicator_clear.as_str()),
+--932: Print(format!("{:<width$}", top, width = width.min(top.len() + 1))),
+--949: Print(format!(
+--1042: Print(line)
+--1064: queue!(self.term, cursor::MoveTo(0, y), Print(msg))?;
+--1279: Print(cfg.chars.selected_indicator_clear.as_str())
tgrep --files --hidden --glob=!.git
treegrep
├──src
│  ├──match_system.rs
│  ├──menu.rs
│  ├──args.rs
│  ├──writer.rs
│  ├──main.rs
│  ├──errors.rs
│  ├──style.rs
│  ├──log.rs
│  ├──config.rs
│  ├──matcher.rs
│  ╰──term.rs
├──doc
│  ├──treegrep.vim9.txt
│  ╰──treegrep.nvim.txt
├──.github
│  ╰──workflows
│     ├──update_readme
│     ├──test.yml
│     ├──update_readme.yml
│     ╰──cr.yml
├──benchmarks
│  ├──runner
│  ╰──times
├──lua
│  ╰──treegrep.lua
├──plugin
│  ╰──treegrep.vim
├──tests
│  ├──pool
│  │  ╰──alice_adventures_in_wonderland_by_lewis_carroll.txt
│  ├──targets
│  │  ├──files_1
│  │  ├──wide_2
│  │  ├──links_4
│  │  ├──links_3
│  │  ├──links_2
│  │  ├──files_long_branch_expr_2
│  │  ├──glob_exclusion
│  │  ├──no_matches
│  │  ├──files_long_branch_1
│  │  ├──context_b1
│  │  ├──context_a1
│  │  ├──files_long_branch_expr_count_2
│  │  ├──overview_dir
│  │  ├──wide_1
│  │  ├──files_2
│  │  ├──line_number
│  │  ├──deep
│  │  ├──context_c1
│  │  ├──links_1
│  │  ├──count
│  │  ├──overview_file
│  │  ├──files_long_branch_expr_1
│  │  ├──overlapping
│  │  ├──file
│  │  ├──max_depth
│  │  ├──files_long_branch_2
│  │  ├──glob_inclusion
│  │  ├──files_long_branch_expr_count_1
│  │  ╰──files_with_expr
│  ├──utils.rs
│  ├──tests.rs
│  ╰──file_system.rs
├──.gitignore
├──README.md
├──Cargo.lock
├──LICENSE
├──rustfmt.toml
╰──Cargo.toml
tgrep --files --branch-each=5 --hidden --glob=!.git
treegrep
├──src
│  ├──match_system.rs, menu.rs, args.rs, writer.rs, main.rs
│  ├──errors.rs, style.rs, log.rs, config.rs, matcher.rs
│  ╰──term.rs
├──doc
│  ╰──treegrep.vim9.txt, treegrep.nvim.txt
├──.github
│  ╰──workflows
│     ╰──update_readme, test.yml, update_readme.yml, cr.yml
├──benchmarks
│  ╰──runner, times
├──lua
│  ╰──treegrep.lua
├──plugin
│  ╰──treegrep.vim
├──tests
│  ├──pool
│  │  ╰──alice_adventures_in_wonderland_by_lewis_carroll.txt
│  ├──targets
│  │  ├──files_1, wide_2, links_4, links_3, links_2
│  │  ├──files_long_branch_expr_2, glob_exclusion, no_matches, files_long_branch_1, context_b1
│  │  ├──context_a1, files_long_branch_expr_count_2, overview_dir, wide_1, files_2
│  │  ├──line_number, deep, context_c1, links_1, count
│  │  ├──overview_file, files_long_branch_expr_1, overlapping, file, max_depth
│  │  ╰──files_long_branch_2, glob_inclusion, files_long_branch_expr_count_1, files_with_expr
│  ╰──utils.rs, tests.rs, file_system.rs
├──.gitignore, README.md, Cargo.lock, LICENSE, rustfmt.toml
╰──Cargo.toml

--help

tgrep 2.0.0

by Timothy Cronin

home page: https://github.com/4imothy/treegrep

regex pattern matcher that displays results in a tree structure with an interface to jump to matched text

tgrep [OPTIONS] <POSITIONAL_REGEXP|--regexp <>|--files|--completions <>|--menu|--repeat> [POSITIONAL_PATH]

arguments:
  [POSITIONAL_REGEXP]
          a regex expression to search for

  [POSITIONAL_PATH]
          the path to search, if not provided, search the current directory

options:
  -e, --regexp <>
          a regex expression to search for

  -p, --path <>
          the path to search, if not provided, search the current directory

  -s, --select
          results are shown in a selection interface for opening

  -m, --menu
          open a search and selection interface

  -f, --files
          if an expression is given, hide matched content, otherwise, show the files that would be searched

  -., --hidden
          search hidden files

  -n, --line-number
          show the line numbers of matches

  -c, --count
          display number of files matched in directory and number of lines matched in a file

  -g, --glob <>
          rules match .gitignore globs, but ! has inverted meaning, overrides other ignore logic

  -l, --links
          search linked paths

  -o, --overview
          conclude results with an overview

  -d, --max-depth <>
          the max depth to search

  -C, --context <>
          number of lines to show before and after each match

  -B, --before-context <>
          number of lines to show before each match

  -A, --after-context <>
          number of lines to show after each match

      --live
          trigger search on every keystroke in the menu

      --max-length <>
          set the max length for a matched line

      --no-ignore
          don't use ignore files

      --trim
          trim whitespace at the beginning of lines

      --threads <>
          set the number of threads to use

      --editor <>
          command used to open selections

      --auto-open
          if there is only one match, open it in the configured editor

      --open-like <>
          command line syntax for opening a file at a line
          
          [possible values: vi, hx, code, jed, default]

      --completions <>
          generate completions for given shell
          
          [possible values: bash, elvish, fish, powershell, zsh]

      --selection-file <>
          file to write selection to (first line: file path, second line: line number if applicable)

      --repeat-file <>
          file used to save the most recent successful search, with searches saved from the command line or the menu

      --repeat
          repeats the last saved search

      --no-color
          don't use colors

      --no-bold
          don't bold anything

      --file-color <>
          black, white, red, green, yellow, blue, magenta, cyan, grey, rgb(_._._), ansi(_)

      --dir-color <>
          black, white, red, green, yellow, blue, magenta, cyan, grey, rgb(_._._), ansi(_)

      --text-color <>
          black, white, red, green, yellow, blue, magenta, cyan, grey, rgb(_._._), ansi(_)

      --branch-color <>
          black, white, red, green, yellow, blue, magenta, cyan, grey, rgb(_._._), ansi(_)

      --line-number-color <>
          black, white, red, green, yellow, blue, magenta, cyan, grey, rgb(_._._), ansi(_)

      --match-colors <>
          black, white, red, green, yellow, blue, magenta, cyan, grey, rgb(_._._), ansi(_)

      --selected-indicator-color <>
          black, white, red, green, yellow, blue, magenta, cyan, grey, rgb(_._._), ansi(_)

      --selected-bg-color <>
          black, white, red, green, yellow, blue, magenta, cyan, grey, rgb(_._._), ansi(_)

      --filter-highlight-color <>
          black, white, red, green, yellow, blue, magenta, cyan, grey, rgb(_._._), ansi(_)

      --prefix-len <>
          number of characters to show before a match
          
          [default: 3]

      --branch-each <>
          number of files to print on each branch
          
          [default: 1]

      --char-vertical <>
          vertical branch character
          
          [default: │]

      --char-horizontal <>
          horizontal branch character
          
          [default: ─]

      --char-top-left <>
          top-left corner character
          
          [default: ╭]

      --char-top-right <>
          top-right corner character
          
          [default: ╮]

      --char-bottom-left <>
          bottom-left corner character
          
          [default: ╰]

      --char-bottom-right <>
          bottom-right corner character
          
          [default: ╯]

      --char-tee <>
          tee branch character
          
          [default: ├]

      --ellipsis <>
          folded indicator
          
          [default: ⤵]

      --search-prompt <>
          search mode prompt
          
          [default: "➜ "]

      --search-prompt-inactive <>
          search prompt when not searching
          
          [default: "- "]

      --filter-prompt <>
          filter mode prompt
          
          [default: /]

      --selected-indicator <>
          selected indicator characters
          
          [default: "─❱ "]

      --key-down <>
          move down
          
          [default: down j n]

      --key-up <>
          move up
          
          [default: up k p]

      --key-big-down <>
          big jump down
          
          [default: J N]

      --key-big-up <>
          big jump up
          
          [default: K P]

      --key-down-path <>
          move down to the next path
          
          [default: } ]]

      --key-up-path <>
          move up to the previous path
          
          [default: { []

      --key-down-same-depth <>
          move down to the next path at same depth
          
          [default: ) d]

      --key-up-same-depth <>
          move up to the previous path at same depth
          
          [default: ( u]

      --key-top <>
          move to the top
          
          [default: home g <]

      --key-bottom <>
          move to the bottom
          
          [default: end G >]

      --key-page-down <>
          page down
          
          [default: pagedown f]

      --key-page-up <>
          page up
          
          [default: pageup b]

      --key-cycle-view <>
          cycle cursor position (top/center/bottom)
          
          [default: z l]

      --key-help <>
          show help
          
          [default: h]

      --key-quit <>
          quit
          
          [default: q]

      --key-open <>
          open selection
          
          [default: enter]

      --key-fold <>
          fold/unfold path
          
          [default: tab]

      --key-filter <>
          filter within results
          
          [default: / s]

      --key-search <>
          enter search mode
          
          [default: :]

      --key-submit-search <>
          submit search query
          
          [default: enter]

  -h, --help
          

  -V, --version
          

arguments are prefixed with the contents of the TREEGREP_DEFAULT_OPTS environment variable