#tree-search #tree #grep #search-tree #regex

app treegrep

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

11 releases (6 stable)

Uses new Rust 2024

new 2.0.0 May 10, 2026
1.3.0 Oct 19, 2025
1.1.0 Jul 1, 2025
1.0.0 Jun 24, 2025
0.1.3 Jan 22, 2024

#190 in Text processing

MIT license

185KB
4.5K SLoC

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

Dependencies

~11–19MB
~419K SLoC