├── hooks
├── uninstall.fish
└── install.fish
├── completions
└── fuzzy_cd.fish
├── functions
├── fcd_shortest.fish
├── fcd_warn.fish
├── fcd_ffmark.fish
├── __fuzzy_cd.fish
├── fcd_ffdir.fish
└── fcd_jump.fish
├── LICENSE
├── init.fish
└── README.md
/hooks/uninstall.fish:
--------------------------------------------------------------------------------
1 | # fuzzy_cd uninstall hook
2 | #
3 | # You can use this file to do custom cleanup when the package is uninstalled.
4 | # You can use the variable $path to access the package path.
5 |
--------------------------------------------------------------------------------
/completions/fuzzy_cd.fish:
--------------------------------------------------------------------------------
1 | # Always provide completions for command line utilities.
2 | #
3 | # Check Fish documentation about completions:
4 | # http://fishshell.com/docs/current/commands.html#complete
5 | #
6 | # If your package doesn't provide any command line utility,
7 | # feel free to remove completions directory from the project.
--------------------------------------------------------------------------------
/functions/fcd_shortest.fish:
--------------------------------------------------------------------------------
1 | function fcd_shortest -d 'Return the shortest string in array'
2 | set -l args
3 | if not tty >/dev/null
4 | read args
5 | else
6 | set args $argv
7 | end
8 |
9 | set -l lines
10 | for str in $args
11 | if test (string length $str) -gt 0
12 | set lines $lines "$str"
13 | end
14 | end
15 |
16 | for line in $lines
17 | echo (string length "$line") "$line"
18 | end | sort -n | head -1 | cut -d' ' -f2-
19 | end
20 |
--------------------------------------------------------------------------------
/functions/fcd_warn.fish:
--------------------------------------------------------------------------------
1 | function fcd_warn -d "Echo to STDERR"
2 | set -l options 'e/error' 'w/warn' 'i/info' 'h/help'
3 |
4 | set -l error 0
5 | set -l color (set_color normal)
6 |
7 | argparse $options -- $argv
8 |
9 | if set -q _flag_help
10 | echo "Output to STDERR"
11 | echo "Options:"
12 | echo " -e, --error Error level message"
13 | echo " -w, --warn Warning level message"
14 | echo " -i, --info Info level message"
15 | return
16 | end
17 |
18 | if set -q _flag_error
19 | set color (set_color -b brred brwhite)
20 | else if set -q _flag_warn
21 | set color (set_color -b bryellow black)
22 | else if set -q _flag_info
23 | set color (set_color brgreen)
24 | end
25 | set -l prompt (set_color bryellow)
26 | printf '%s>%s %s%s%s\n' $prompt $prompt $color "$argv" (set_color normal) >&2
27 | end
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2022 Brett Terpstra
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/init.fish:
--------------------------------------------------------------------------------
1 | # fuzzy_cd initialization hook
2 | #
3 | # You can use the following variables in this file:
4 | # * $package package name
5 | # * $path package path
6 | # * $dependencies package dependencies
7 | if functions -q __fuzzy_cd
8 | if test -d ~/.marks && test (which fasd)
9 | if not functions -q __fuzzy_wrapped_cd
10 | functions -c cd __fuzzy_wrapped_cd
11 | end
12 | functions -e cd
13 | functions -c __fuzzy_cd cd
14 | end
15 | end
16 |
17 | function fcd_shortest_common
18 | set -l root $argv[1]
19 | set -l results $argv[1]
20 | set -e argv[1]
21 | for path in (fcd_return_array $argv | sort)
22 | if not test (string match "$root*" $path)
23 | set root $path
24 | set -a results $path
25 | end
26 | end
27 | fcd_return_array $results
28 | end
29 |
30 | function fcd_dir_to_regex
31 | echo (printf '%s' (echo "$argv"|sed -E 's/ +//g'|sed -E 's/(.)/\1[^\/]*/g'))
32 | end
33 |
34 | function fcd_dir_regex
35 | set -l section
36 | set -l regex (fcd_dir_to_regex $argv[1])
37 | for arg in $argv[2..-1]
38 | set section (fcd_dir_to_regex $arg)
39 | set regex "$regex/[^.]*$section"
40 | end
41 | echo $regex
42 | end
43 |
44 | function fcd_return_array -d 'Echo out an array one line at a time'
45 | for item in $argv
46 | echo $item
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/functions/fcd_ffmark.fish:
--------------------------------------------------------------------------------
1 | function fcd_mark_to_regex
2 | echo (printf '%s' (echo "$argv"|sed -E 's/ +//g'|sed -E 's/(.)/\1[^\/]*/g'))
3 | end
4 |
5 | function fcd_mark_regex
6 | set -l section
7 | set -l regex (fcd_mark_to_regex $argv[1])
8 | for arg in $argv[2..-1]
9 | set section (fcd_mark_to_regex $arg)
10 | set regex "$regex/.*$section"
11 | end
12 | echo $regex
13 | end
14 |
15 | function fcd_ffmark -d "fuzzy find a jump mark"
16 | if test -e ~/.marks/$argv[1]
17 | echo -n (readlink ~/.marks/$argv[1])
18 | return 0
19 | end
20 |
21 | set -l new_path
22 | if test (count $argv) -gt 0
23 | set -l args $argv
24 | set -l found
25 | set -l case_sensitive
26 |
27 | set -l regex (fcd_mark_regex $args)
28 |
29 | # Search .marks
30 | set -l results (find -LE -s "$MARKPATH" -iregex "$MARKPATH/$regex" -type d -maxdepth 1 | head -n 1 | tr -d "\n")
31 | # Get shortest match
32 | set found (shortest $results)
33 |
34 | if test -n "$found"
35 | set found (string replace -r '^./' '' $found)
36 | set new_path $found
37 | else
38 | set -l results (find -LE -s "$MARKPATH" -iregex "$MARKPATH/.*$regex" -type d -maxdepth 1 | head -n 1 | tr -d "\n")
39 | # Get shortest match
40 | set found (shortest $results)
41 | if test -n "$found"
42 | set found (string replace -r '^./' '' $found)
43 | set new_path $found
44 | end
45 | end
46 | end
47 | echo -n (readlink "$new_path")
48 | end
49 |
50 |
51 |
--------------------------------------------------------------------------------
/hooks/install.fish:
--------------------------------------------------------------------------------
1 | function __fcd_warn -d "Echo to STDERR"
2 | set -l options 'e/error' 'w/warn' 'i/info' 'h/help'
3 |
4 | set -l error 0
5 | set -l color (set_color normal)
6 |
7 | argparse $options -- $argv
8 |
9 | if set -q _flag_help
10 | echo "Output to STDERR"
11 | echo "Options:"
12 | echo " -e, --error Error level message"
13 | echo " -w, --warn Warning level message"
14 | echo " -i, --info Info level message"
15 | return
16 | end
17 |
18 | if set -q _flag_error
19 | set color (set_color -b brred brwhite)
20 | else if set -q _flag_warn
21 | set color (set_color -b bryellow black)
22 | else if set -q _flag_info
23 | set color (set_color brgreen)
24 | end
25 | set -l prompt (set_color bryellow)
26 | printf '%s>%s %s%s%s\n' $prompt $prompt $color "$argv" (set_color normal) >&2
27 | end
28 |
29 | __fcd_warn "fuzzy_cd: testing prerequisites"
30 |
31 | if not test -d ~/.marks
32 | __fcd_warn -e "fuzzy_cd: jump doesn't appear to be installed. Please see https://github.com/oh-my-fish/plugin-jump"
33 | __fcd_warn "......... if you have oh-my-fish installed, you can use `omf install jump`"
34 | else
35 | __fcd_warn "......... jump is installed"
36 | end
37 |
38 | if not test (which fzf)
39 | __fcd_warn -e "fuzzy_cd: fzf is not installed/available in PATH. Please install https://github.com/junegunn/fzf"
40 | __fcd_warn "......... if you're on a Mac and have Homebrew installed, you can use `brew install fzf`"
41 | else
42 | __fcd_warn "......... fzf is available"
43 | end
44 |
45 |
46 | if not test (which fasd)
47 | __fcd_warn -e "fuzzy_cd: fasd is not installed/available in PATH. Please install https://github.com/clvv/fasd"
48 | __fcd_warn "......... if you're on a Mac and have Homebrew installed, you can use `brew install fasd`"
49 | else
50 | __fcd_warn "......... fasd is available"
51 | end
52 |
53 | set curr_dir (dirname (status --current-filename))
54 | source "$curr_dir/../init.fish"
55 |
--------------------------------------------------------------------------------
/functions/__fuzzy_cd.fish:
--------------------------------------------------------------------------------
1 | function __fuzzy_cd -d "fuzzy cd with jump bookmarks"
2 | function __fuzzy_cd_chdir
3 | set -l res
4 | set -l first_token $argv[1]
5 | set -l tokens (string split " " (string replace -a "/" " " (string join "/" $argv)))
6 | set -l try_mark true
7 |
8 | # if the first token contains a non alphanumeric symbol, don't match a jump mark
9 | if string match -q -r -- '[^A-Za-z0-9]' $first_token
10 | set try_mark false
11 | end
12 |
13 | set -l token
14 | for dir in $tokens
15 | switch $dir
16 | case '.'
17 | command cd .
18 | case '..'
19 | command cd ..
20 | case '*'
21 | set -a token $dir
22 | end
23 | end
24 |
25 | if test (string match --regex '\.{3,}' $first_token)
26 | set -l count (math (string length $first_token) - 1)
27 | set base '.'
28 | while test $count -gt 0
29 | set base "$base/.."
30 | set count (math $count - 1)
31 | end
32 |
33 | set -e token[1]
34 |
35 | if test (count $token) -eq 0
36 | __fuzzy_wrapped_cd $base
37 | return 0
38 | end
39 |
40 | set res (fcd_ffdir -i -d2 --multi --shortest $base $token)
41 | else if $try_mark
42 | # See if first position is a match for a jump bookmark
43 | set -l base (fcd_ffmark $first_token)
44 | if test -n "$base"
45 | set -e token[1]
46 | else
47 | set base '.'
48 | end
49 |
50 | if test (count $token) -eq 0
51 | __fuzzy_wrapped_cd $base
52 | return 0
53 | end
54 |
55 | set res (fcd_ffdir -i -d2 --multi --shortest $base $token)
56 | end
57 |
58 | if test -z "$res" || test (string match '.' $res) || test (string match $base $res)
59 | # switch to using fasd, keeping the base if we matched a jump mark
60 | set -l search
61 | if test (string match "." $base)
62 | set search $token
63 | else
64 | set search $base $token
65 | end
66 |
67 | set res (fasd -tldR0 $search| head -n 5 | awk '{print $0}')
68 | end
69 |
70 |
71 | # set -l result (shortest $res)
72 | set -l result (printf "%s\n" $res | fzf -1 -0 --height=8 --info=inline --tiebreak=begin,length)
73 |
74 | if test -n "$result"
75 | __fuzzy_wrapped_cd $result
76 | else
77 | if not $try_mark
78 | __fuzzy_cd_chdir $token
79 | else
80 | fcd_warn "No match found"
81 | end
82 | end
83 | end
84 |
85 | if test -z "$argv"
86 | __fuzzy_wrapped_cd
87 | else if string match '-' "$argv"
88 | __fuzzy_wrapped_cd -
89 | else if test (count $argv) -eq 1 && test -e $argv[1]
90 | __fuzzy_wrapped_cd $argv
91 | else
92 | __fuzzy_cd_chdir $argv
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | #### Fuzzy cd
4 |
5 | > A plugin for [Oh My Fish][omf-link] which replaces the `cd` command with a fuzzy searching version.
6 |
7 | [](/LICENSE)
8 | [](https://fishshell.com)
9 | [](https://www.github.com/oh-my-fish/oh-my-fish)
10 |
11 |
12 |
13 | ## Dependencies
14 |
15 | You'll definitely need to install `fasd`. This is easiest with Homebrew, just `brew install fasd`.
16 |
17 | You'll also want `fzf` available. Again, easiest with Homebrew: `brew install fzf`.
18 |
19 | To make use of the jump marks, you'll want to install [this particular jump plugin](https://github.com/oh-my-fish/plugin-jump) (via `omf install jump`, probably). The `__fuzzy_cd` function doesn't actually use it for jumping, but my replacement for the `jump` function does not include any functions for adding or listing marks. This version of jump creates symlinks in `~/.marks`, which is what fuzzy cd is set up to read.
20 |
21 | ## Install
22 |
23 | ```fish
24 | $ omf repositories add https://github.com/ttscoff/omf-packages
25 | $ omf install fuzzy_cd
26 | ```
27 |
28 |
29 | ## Usage
30 |
31 | ```fish
32 | $ cd JUMP_MARK [fuzzy [sub [dir [sequence]]]]
33 | ```
34 |
35 | Fuzzy cd replaces the cd command.
36 |
37 | If there's only one argument to the `cd` command and it matches a valid path, `cd` will behave normally.
38 |
39 | If the first argument is a jump bookmark, that will be used as the base directory. Bookmarks are fuzzy matched, so `cd bmark` would recognize a bookmark titled `bookmark` and use it.
40 |
41 | Any additional arguments will be used to fuzzy search subdirectories in sequence. So `cd desk pod ov 8` would locate `~/Desktop/Podcasts/Overtired/268` and cd to it.
42 |
43 | If the first argument is 3 or more dots, `cd` will navigate up the directory tree, allowing fuzzy directory searching from the base folder with additional arguments. 1 dot is the current directory, 2 dots is the next level up, each additional dot is one level further up. 3 dots is equivalent to `../..`, 4 dots is `../../..`, etc.
44 |
45 | If no valid path is found using this method, `cd` will fall back to using `fasd` to search recent directories.
46 |
47 | # License
48 |
49 | [MIT][mit] © [Brett Terpstra][author]
50 |
51 | [mit]: https://opensource.org/licenses/MIT
52 | [author]: https://github.com/ttscoff
53 | [omf-link]: https://www.github.com/oh-my-fish/oh-my-fish
54 |
55 | [license-badge]: https://img.shields.io/badge/license-MIT-007EC7.svg?style=flat-square
56 |
--------------------------------------------------------------------------------
/functions/fcd_ffdir.fish:
--------------------------------------------------------------------------------
1 | function fcd_ffdir -d "fuzzy find a directory, pass root dir and sequential search strings"
2 | set -l options "c/case-sensitive" "i/case-insensitive" "m/menu" "multi" "d/depth=" "shortest"
3 | argparse $options -- $argv
4 |
5 | set -l max_depth 2
6 | set -l new_path
7 | set -l args
8 | if test (count $argv) -gt 1
9 | set new_path $argv[1]
10 | set args $argv[2..-1]
11 | else
12 | set new_path '.'
13 | set args $argv
14 | end
15 |
16 | if set -q _flag_depth
17 | set max_depth $_flag_depth
18 | end
19 |
20 |
21 | if test (count $args) -gt 0
22 | # allow traversing to number of args * max count
23 | set max_depth (math (count $args)" * $max_depth")
24 | test $max_depth -gt 4 && set max_depth 4
25 | set -l found "."
26 | # if search string contains uppercase, make search case sensitive
27 | if string match -q --regex [A-Z] $args or set -q _flag_c
28 | set case_sensitive "regex"
29 | else
30 | set case_sensitive "iregex"
31 | end
32 | # search for directory containing string up to 2 levels deeper
33 | set -l regex (fcd_dir_regex $args)
34 | # start by looking for directories starting with first char of search
35 | # string, ignoring dot directories
36 | set -l results (find -EL -s "$new_path" -$case_sensitive ".*/[^.]*$regex.*" -type d -maxdepth $max_depth -mindepth 1 2> /dev/null)
37 | # choose shortest result
38 | if set -q _flag_menu
39 | set found (echo -e (string join "\n" $results) | fzf -1 -0)
40 | if test -z "$found"
41 | return
42 | end
43 | else
44 | if set -q _flag_multi
45 | set found $results
46 | else
47 | set found (fcd_shortest $results)
48 | end
49 | end
50 |
51 | # if we found a result, clean it up
52 | if test -n "$found"
53 | if set -q _flag_multi
54 | if set -q _flag_shortest
55 | set found (fcd_shortest_common $found)
56 | end
57 | fcd_return_array $found
58 | return
59 | end
60 | set found (echo -n "$found" | sed -e 's/^\.\///')
61 | set new_path $found
62 | else # if not, try again without the first char/dot requirement
63 | set results (find -EL -s "$new_path" -$case_sensitive ".*$regex.*" -type d -maxdepth $max_depth -mindepth 1 2> /dev/null)
64 | if set -q _flag_shortest
65 | set results (fcd_shortest_common $results)
66 | end
67 | if set -q _flag_menu
68 | set found (fcd_return_array $results | fzf -1 -0)
69 | if test -z "$found"
70 | return
71 | end
72 | else
73 | if set -q _flag_multi
74 | set found $results
75 | else
76 | set found (fcd_shortest $results)
77 | end
78 | end
79 | if test -n "$found"
80 | if set -q _flag_multi
81 | echo -e (string join "\n" $found)
82 | return
83 | end
84 | set found (echo "$found" | sed -e 's/^\.\///')
85 | set new_path $found
86 | else
87 | set -e new_path
88 | end
89 | end
90 | end
91 | echo "$new_path"
92 | end
93 |
--------------------------------------------------------------------------------
/functions/fcd_jump.fish:
--------------------------------------------------------------------------------
1 | # A replacement for "jump" from the jump package that
2 | # handles fuzzy subdirectory searching for additional
3 | # arguments.
4 | #
5 | # Requires that jump be installed (`omf install jump`).
6 | # Then add the following files to ~/.config/fish/functions/,
7 | # which should override the jump command from the package:
8 | #
9 | # - jump.fish
10 | # - fcd_ffmark.fish
11 | # - fcd_ffdir.fish
12 | # - fcd_shortest.fish
13 | #
14 | # Fist argument must be an existing bookmark (also fuzzy
15 | # matched), additional arguments are searched within the
16 | # bookmark directory, in argument order. If the first
17 | # argument is '.', subdirectories will be searched from
18 | # current working directory.
19 | #
20 | # Any part of the directory name can be matched, shortest
21 | # result is used.
22 | #
23 | # Subdirectory search string is separated by slashes or
24 | # spaces. Each segment of the string is a fuzzy directory
25 | # search. Segments can be matched up to two directory levels
26 | # apart.
27 | #
28 | # Assuming a bookmark called "appsupp", linked to
29 | # ~/Library/Application Support
30 | #
31 | # $ jump apsup m2/css
32 | #
33 | # would match:
34 | # ~/Library/Application Support/Marked 2/Custom CSS
35 | #
36 | # If the search arguments are all lowercase, the search is
37 | # case insensitive. If an argument contains uppercase
38 | # letters, matching becomes case sensitive. Use -i to force
39 | # case insensitive, or -c to force case sensitive.
40 | function jump -d 'Fish "jump" replacement with subdirectory matching'
41 | set -l options "I/case-sensitive" "i/case-insensitive" "h/help" "v/verbose" "c/command=" "nomenu" "multi"
42 | set -l case_sensitive
43 | set -l cmd
44 | set -l verbose
45 | set -l use_fzf ' -m'
46 | set -l multi ''
47 |
48 | argparse $options -- $argv
49 |
50 | if set -q _flag_nomenu
51 | set use_fzf ''
52 | end
53 |
54 | if set -q _flag_multi
55 | set multi ' --multi'
56 | end
57 |
58 | if set -q _flag_help || test (count $argv) -eq 0
59 | echo "Fuzzy jump with subdirectory matching"
60 | echo
61 | echo "Usage: jump [MARK] [sub directory search]"
62 | echo
63 | echo "- MARK fuzzy matches a link in $MARKPATH"
64 | echo "- following strings fuzzy match subdirectories"
65 | echo "- search folders can be separated by space or slash"
66 | echo "- folder matches can be up to 2 levels deeper than the preceding match"
67 | echo
68 | echo "Example:"
69 | echo " # where appsupp is an existing jump bookmark"
70 | echo " jump appsupp m2 css"
71 | echo " => ~/Library/Application Support/Marked 2/Custom CSS"
72 | echo
73 | echo "Options:"
74 | echo " -c, --command=CMD - run CMD instead of cd"
75 | echo " -I - force case sensitive subdirectory matching"
76 | echo " -i - force case insensitive subdirectory matching"
77 | echo " -h - display this help"
78 | return 0
79 | end
80 |
81 | if set -q _flag_I
82 | set case_sensitive " -c"
83 | else if set -q _flag_i
84 | set case_sensitive " -i"
85 | end
86 |
87 | if set -q _flag_c
88 | set cmd $_flag_c
89 | else
90 | set cmd "cd"
91 | end
92 |
93 | if set -q _flag_v
94 | if functions -q warn
95 | set verbose "warn"
96 | else
97 | set verbose "echo"
98 | end
99 | end
100 |
101 | set -l max_depth 2
102 | set -l regex
103 | set -l args
104 | set -l new_path
105 |
106 | # if first arg is '.', search from current directory
107 | if test "$argv[1]" = '.'
108 | set new_path (fcd_ffdir $use_fzf$multi$case_sensitive . $argv)
109 | if test -n "$new_path" -a -d "$new_path"
110 |
111 | eval $cmd \"$new_path\"
112 | else
113 | echo "No match found"
114 | return 1
115 | end
116 | # if first arg is an exact match for a bookmark
117 | else if test -d $MARKPATH/$argv[1] -a -L $MARKPATH/$argv[1]
118 | set new_path (readlink $MARKPATH/$argv[1])
119 | # we have more than one argument, search for subdirs
120 | if test (count $argv) -gt 1
121 | set args $argv[2..-1]
122 | set new_path (fcd_ffdir $use_fzf$multi$case_sensitive $new_path $args)
123 | end
124 | # if test -n verbose
125 | # eval $verbose $cmd \"$new_path\"
126 | # end
127 | eval $cmd \"$new_path\"
128 | # no match, fuzzy search bookmarks
129 | else
130 | set new_path (fcd_ffmark $case_sensitive $argv[1])
131 |
132 | if test -n "$new_path" -a -d "$new_path"
133 | # if we have more than one argument, search for
134 | # subdirs
135 | if test (count $argv) -gt 1
136 | # set args (string split / (string join "" $argv[2..-1]))
137 | set args $argv[2..-1]
138 | set new_path (fcd_ffdir $use_fzf$multi$case_sensitive $new_path $args)
139 | end
140 |
141 | eval $cmd \"$new_path\"
142 | # if first arg is an actual directory, open that
143 | else if test -d $argv[1]
144 | set new_path "$argv[1]"
145 | if test (count $argv) -gt 1
146 | set new_path (fcd_ffdir $use_fzf$multi$case_sensitive $new_path $argv[2..-1])
147 | end
148 |
149 | eval $cmd \"$new_path\"
150 | else
151 | echo "No such mark: $argv[1]"
152 | end
153 | end
154 | end
155 |
156 |
--------------------------------------------------------------------------------