├── functions ├── __f_insert_from_picker.fish ├── __f_clean.fish ├── __f_extract_paths.fish ├── __f_complete.fish ├── __f_add.fish └── __f.fish ├── conf.d ├── f_key_bindings.fish └── f.fish ├── LICENSE └── README.adoc /functions/__f_insert_from_picker.fish: -------------------------------------------------------------------------------- 1 | function __f_insert_from_picker -d "Launch f in picker mode and insert selection(s) to commandline" 2 | set -l paths (__f -ko 2> /dev/null) 3 | set -ga __f_temp_exclude_files $paths 4 | 5 | set paths (string escape -n $paths) 6 | 7 | commandline -i (printf '%s ' $paths) 8 | commandline -f repaint 9 | end 10 | -------------------------------------------------------------------------------- /functions/__f_clean.fish: -------------------------------------------------------------------------------- 1 | function __f_clean -d "Remove invalid files from data file" 2 | set -l tmpfile (mktemp $F_DATA.XXXXXX) 3 | 4 | if test -f $tmpfile 5 | 6 | while read -d '|' path remaining 7 | test -f $path; and printf '%s|%s\n' $path $remaining 8 | end <$F_DATA >$tmpfile 9 | 10 | command mv -f $tmpfile $F_DATA 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /conf.d/f_key_bindings.fish: -------------------------------------------------------------------------------- 1 | if not bind \ek 2>/dev/null 2 | bind \ek __f_insert_from_picker 3 | end 4 | 5 | if not bind -M insert \ek 2>/dev/null 6 | bind -M insert \ek __f_insert_from_picker 7 | end 8 | 9 | function f_key_bindings_uninstall --on-event f_key_bindings_uninstall 10 | bind \ek 2>/dev/null | string match -e __f_insert_from_picker 11 | and bind -e \ek 12 | 13 | bind -M insert \ek 2>/dev/null | string match -e __f_insert_from_picker 14 | and bind -M insert -e \ek 15 | end 16 | -------------------------------------------------------------------------------- /functions/__f_extract_paths.fish: -------------------------------------------------------------------------------- 1 | function __f_extract_paths -d 'Extract valid paths from string $argv[1]' 2 | 3 | if test (count $argv) -gt 1 4 | echo 'Can only take one argument; more given' > /dev/stderr 5 | return 1 6 | end 7 | 8 | printf '%s' $argv[1] | read -zat tokens 9 | for token in $tokens 10 | # Tilde expansion is done only on the commandline 11 | set -l token (string replace -r -- '^~/' "$HOME/" "$token") 12 | if test -f "$token" 13 | printf '%s\n' (builtin realpath -s "$token") 14 | end 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gokul Soumya 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 18 | THE 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 SOFTWARE. 21 | -------------------------------------------------------------------------------- /functions/__f_complete.fish: -------------------------------------------------------------------------------- 1 | function __f_complete -d "add completions" 2 | # string replace is faster than cut (on test with sample file) 3 | function __f_marks 4 | printf "%s\n" (string replace -r '\|.*' '' < $F_DATA) 5 | end 6 | 7 | complete -fc $F_CMD -a "(__f_marks)" 8 | 9 | complete -c $F_CMD -s k -l pick -d "Pick a file using fzf" 10 | complete -c $F_CMD -s K -l picker -d "Set picker program" -xa '(__fish_complete_command)' 11 | complete -c $F_CMD -s w -l with -d "Open file using alternate command" -xa '(__fish_complete_command)' 12 | complete -c $F_CMD -s d -l cd -d "cd into file's parent dir first" 13 | complete -c $F_CMD -s a -l app -d "Open with default app" 14 | complete -c $F_CMD -s c -l clean -d "Clean out $F_DATA" 15 | complete -c $F_CMD -s o -l echo -d "Print best match, do not open" 16 | complete -c $F_CMD -s l -l list -d "List matches, do not open" 17 | complete -c $F_CMD -s p -l purge -d "Purge $F_DATA" 18 | complete -c $F_CMD -s r -l rank -d "Search by rank and open" 19 | complete -c $F_CMD -s t -l recent -d "Search by recency and open" 20 | complete -c $F_CMD -s x -l delete -d "Remove file from database" 21 | complete -c $F_CMD -s h -l help -d "Print help" 22 | 23 | end 24 | -------------------------------------------------------------------------------- /conf.d/f.fish: -------------------------------------------------------------------------------- 1 | if test -z "$F_DATA" 2 | if test -n "$XDG_DATA_HOME" 3 | set -U F_DATA_DIR "$XDG_DATA_HOME/f" 4 | else 5 | set -U F_DATA_DIR "$HOME/.local/share/f" 6 | end 7 | set -U F_DATA "$F_DATA_DIR/data" 8 | end 9 | 10 | if test ! -e "$F_DATA" 11 | if test ! -e "$F_DATA_DIR" 12 | mkdir -p -m 700 "$F_DATA_DIR" 13 | end 14 | touch "$F_DATA" 15 | end 16 | 17 | if test -z "$F_CMD" 18 | set -U F_CMD "f" 19 | end 20 | 21 | if not set -q F_EXCLUDE 22 | set -U F_EXCLUDE '^/tmp/.+' 23 | end 24 | 25 | function $F_CMD -d "Open files fuzzily" 26 | __f $argv 27 | end 28 | 29 | function __f_on_event_postexec --on-event fish_postexec 30 | set -l paths (__f_extract_paths $argv) 31 | 32 | for path in $paths 33 | if not contains -- $path $__f_temp_exclude_files 34 | __f_add $path 35 | end 36 | end 37 | 38 | set -e __f_temp_exclude_files 39 | return 0 40 | end 41 | 42 | # Setup completions once first 43 | __f_complete 44 | 45 | function f_uninstall --on-event f_uninstall 46 | functions -e __f_on_event_postexec 47 | functions -e $F_CMD 48 | 49 | if test -n "$F_DATA" 50 | echo "To purge f's data, remove: $F_DATA" > /dev/stderr 51 | end 52 | 53 | set -e F_CMD 54 | set -e F_DATA 55 | set -e F_EXCLUDE 56 | end 57 | -------------------------------------------------------------------------------- /functions/__f_add.fish: -------------------------------------------------------------------------------- 1 | function __f_add -d "Add given path to f file" 2 | 3 | if test (count $argv) -gt 1 4 | echo 'Can only take one argument; more given' > /dev/stderr 5 | return 1 6 | end 7 | 8 | set -l filename $argv 9 | 10 | for pattern in $F_EXCLUDE 11 | if string match -rq -- $pattern $filename 12 | return 0 13 | end 14 | end 15 | 16 | set -l tmpfile (mktemp $F_DATA.XXXXXX) 17 | 18 | if test -f $tmpfile 19 | set -l filename (string replace --all \\ \\\\ $filename) 20 | command awk -v path=$filename -v now=(date +%s) -F "|" ' 21 | BEGIN { 22 | rank[path] = 1 23 | time[path] = now 24 | } 25 | $2 >= 1 { 26 | if( $1 == path ) { 27 | rank[$1] = $2 + 1 28 | time[$1] = now 29 | } 30 | else { 31 | rank[$1] = $2 32 | time[$1] = $3 33 | } 34 | count += $2 35 | } 36 | END { 37 | if( count > 1000 ) { 38 | for( i in rank ) print i "|" 0.9*rank[i] "|" time[i] # aging 39 | } 40 | else for( i in rank ) print i "|" rank[i] "|" time[i] 41 | } 42 | ' $F_DATA 2>/dev/null >$tmpfile 43 | 44 | if test -n "$F_OWNER" 45 | chown $F_OWNER:(id -ng $F_OWNER) $tmpfile 46 | end 47 | 48 | # Don't use redirection here as it can lead to a race condition where $F_DATA is clobbered. 49 | # Note: There is a still a possible race condition where an old version of $F_DATA is 50 | # read by one instance of Fish before another instance of Fish writes its copy. 51 | 52 | command mv $tmpfile $F_DATA 53 | or command rm $tmpfile 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = F image:https://img.shields.io/github/v/release/gokulsoumya/f?label=&style=flat-square[link="https://github.com/gokulsoumya/f/releases/latest"] image:https://img.shields.io/github/license/gokulsoumya/f?style=flat-square[link="LICENSE"] 2 | :toc: macro 3 | :experimental: 4 | 5 | ifdef::env-github[] 6 | :tip-caption: :bulb: 7 | :note-caption: :information_source: 8 | :important-caption: :heavy_exclamation_mark: 9 | :caution-caption: :fire: 10 | :warning-caption: :warning: 11 | endif::[] 12 | 13 | `f` lets you open files whose names you typed in the commandline some time before. 14 | The filenames will be recognized and stored by `f`, which 15 | you can then use in the future. It's like the famous https://www.github.com/rupa/z[rupa/z] 16 | plugin and uses the frecency concept, but for files. Similar to it, `f` also needs 17 | some time to learn the files you access the most and build a database. Usage is 18 | fairly similar, but with some added niceties. 19 | 20 | Most of the `f` source code has been modified from https://github.com/jethrokuan[jethrokuan's] 21 | https://github.com/jethrokuan/z[z] port from bash to pure fish. Many, many thanks to him and 22 | the original author of `z`, https://github.com/rupa[rupa]. 23 | 24 | toc::[] 25 | 26 | == Installation 27 | 28 | `f` has been tested only on fish version 3, specifically `3.1.2`. Other versions should work 29 | as the code is based on the fish port of `z` which works on fish `>=2.7.0`, though it is possible 30 | that there is some compatibility issue. Please open an issue if you have trouble installing. 31 | 32 | Recommended installation is through https://github.com/jorgebucaran/fisher[fisher]: 33 | 34 | fisher install gokulsoumya/f 35 | 36 | It is recommended to install https://github.com/junegunn/fzf[`fzf`] or any other fuzzy filter 37 | of your choice which can be used to fuzzily open (multiple) files, delete entries from the database, etc. 38 | 39 | == Usage 40 | 41 | The files are opened by default with `$VISUAL`. If not set, `$EDITOR` will be used. One of 42 | these variables has to be set. Use `--with` to open the files with another command. All files 43 | are stored and retrieved as absolute paths. 44 | 45 | ------- 46 | $ f --help 47 | 48 | Usage: f [-d] [-r|-t] [-w cmd|-a|-o] [-K cmd] [-k] [query] ... 49 | f [-r|-t] [-K cmd] [-k] -x [query] ... 50 | f [-r|-t] -l [query] ... 51 | f -c|-p|-h 52 | 53 | -k --pick Launch fzf for selection and then open with $EDITOR 54 | -K --picker cmd Command to be used as picker program; implies -k 55 | -w --with cmd Open the file with command cmd rather than $EDITOR 56 | -d --cd cd to the file directory after selection 57 | -a --app Open with default app, using xdg-open or open 58 | -c --clean Remove files that no longer exist from $F_DATA 59 | -o --echo Print match and return 60 | -l --list List matches and scores 61 | -p --purge Delete all entries from $F_DATA 62 | -r --rank Search by rank 63 | -t --recent Search by recency 64 | -x --delete Remove selected file from database 65 | -h --help Print this help 66 | 67 | ------- 68 | 69 | === Notable flags and examples 70 | 71 | Let's look at some interesting examples: 72 | 73 | [source,fish] 74 | ------ 75 | # Open the best matched file that has "con" and "fish" somewhere in it's name 76 | $ f con fish 77 | 78 | # Open the most recently mentioned file 79 | $ f --recent 80 | 81 | # Launch fzf to select a file from all files and open it 82 | $ f --pick 83 | 84 | # Launch fzf with "somestring" prefilled as query 85 | $ f --pick somestring 86 | 87 | # Interactlively delete an entry 88 | $ f --pick --delete 89 | 90 | # Pick using alternative fuzzy filter or command 91 | $ f --picker 'tail -n1' 92 | 93 | # Open with default app, uses xdg-open on Linux and open on OSX internally 94 | $ f --app --pick 95 | 96 | # Pick a file using fzf and view it using less 97 | $ f --with less --pick 98 | 99 | # Filenames are simply passed as arguments to the --with command 100 | $ f --with rm --pick 101 | 102 | # Pick a file using fzf, cd to it's parent directory and run ls -l on the file 103 | $ f --cd --with 'ls -l' --pick 104 | ------ 105 | 106 | NOTE: When using `--picker` or if <> is set, the arguments 107 | to `f` are discarded (the flags are still used). When using the default `fzf` 108 | picker, the arguments are used as a query to start `fzf` with. 109 | 110 | [[multiple-files]]Multiple files can be selected using the picker program; 111 | they should be printed to stdout with one file per line. With the default 112 | `fzf` picker, kbd:[Tab] can be used to select multiple files. 113 | 114 | TIP: Use `f --pick --delete` to interactively delete [multiple] files from the database. 115 | 116 | === Keybindings 117 | 118 | The whole reason I wrote this plugin was for this one feature -- insert filenames 119 | into the command line without hitting tab a thousand times or doing a token search 120 | with part of the filename. You can do this using the kbd:[Alt+K] keybinding. 121 | This opens fzf and the filename you choose will be inserted into the commandline. 122 | 123 | TIP: You can select and insert <> using the picker. 124 | 125 | If kbd:[Alt+K] is already bound to something, it will not be overridden. Instead 126 | you can bind `__f_insert_from_picker` to any key you wish: 127 | 128 | [source,fish] 129 | ----- 130 | $ bind \eu __f_insert_from_picker # bind Alt-u 131 | $ bind -M insert \eu __f_insert_from_picker # for vim insert mode 132 | ----- 133 | 134 | == Configuration 135 | 136 | * `set -U *F_CMD* "p"` + 137 | Change command from `f` to `p`. 138 | 139 | * `set -U *F_DATA* "$HOME/.foo"` + 140 | Set data file to `$HOME/.foo` instead of `$XDG_DATA_HOME/f/data`. 141 | 142 | // The backslash is needed only feore the first * here to render correctly 143 | * `set -U *F_EXCLUDE* ".\*password.*" ".*\.txt"` + 144 | List of regex patterns to use to exlude files from being added 145 | to `f` database. 146 | 147 | NOTE: If not set by the user, `F_EXCLUDE` is set to a list with 148 | `"^/tmp/.+"` as the only item. 149 | 150 | * `set -U *F_PICKER* "skim"` + 151 | Command to use as interactive filtering program. Must be a 152 | string, _not a list_. 153 | 154 | * `set -U *F_OWNER* "username"` + 155 | Ensure data file is owned by `username`. This prevents usage of `f` 156 | with `sudo` to cause file to be inaccessible in non-sudo sessions. 157 | 158 | == FAQ 159 | 160 | [qanda] 161 | 162 | Backgrounding with `&` is not working with `f --with cmd`:: 163 | https://github.com/fish-shell/fish-shell/issues/238[Functions cannot be backgrounded in fish], 164 | so something like `f --with gedit --pick &` won't work as expected. 165 | 166 | Filenames I haven't typed in are showing up in the list:: 167 | `f` works by scanning the command line and looking for valid filenames 168 | after each time a command is executed. If by some chance a file in a 169 | directory has the same name as some random token in the commandline, a 170 | false positive may be recorded. + 171 | Consider that a file called _status_ is present in the directory you're 172 | currently in. Now if you execute the `status` command, `f` will record 173 | the absolute path of the file _status_ in it's database. If you see this 174 | often with a particular file, you can make use of the `F_EXCLUDE` 175 | configuration variable. 176 | -------------------------------------------------------------------------------- /functions/__f.fish: -------------------------------------------------------------------------------- 1 | function __f -d "Open recent files entered on command line" 2 | 3 | function __print_help 4 | echo " 5 | Usage: $F_CMD [-d] [-r|-t] [-w cmd|-a|-o] [-K cmd] [-k] [query] ... 6 | $F_CMD [-r|-t] [-K cmd] [-k] -x [query] ... 7 | $F_CMD [-r|-t] -l [query] ... 8 | $F_CMD -c|-p|-h 9 | 10 | -k --pick Launch fzf for selection and then open with \$EDITOR 11 | -K --picker cmd Command to be used as picker; implies -k 12 | -w --with cmd Open the file with command cmd rather than \$EDITOR 13 | -d --cd cd to the file directory after selection 14 | -a --app Open with default app, using xdg-open or open 15 | -c --clean Remove files that no longer exist from $F_DATA 16 | -o --echo Print match and return 17 | -l --list List matches and scores 18 | -p --purge Delete all entries from $F_DATA 19 | -r --rank Search by rank 20 | -t --recent Search by recency 21 | -x --delete Remove selected file from database 22 | -h --help Print this help 23 | " | string replace -r '^ {12}' '' # remove unnecessary indent 24 | end 25 | 26 | set -l options "h/help" "c/clean" "o/echo" "l/list" "p/purge" "r/rank" "t/recent" \ 27 | "x/delete" "k/pick" "K/picker=" "w/with=" "d/cd" "a/app" 28 | 29 | argparse -n $F_CMD \ 30 | -x "a,w,o,l,x" -x "r,t" -x "p,c" -x "l,k" -x "l,K" -x "l,d,x" \ 31 | $options -- $argv 32 | or return 33 | 34 | if set -q _flag_help 35 | __print_help 36 | return 0 37 | else if set -q _flag_clean 38 | __f_clean 39 | echo "'$F_DATA' cleaned!" 40 | return 0 41 | else if set -q _flag_purge 42 | echo > $F_DATA 43 | echo "'$F_DATA' purged!" 44 | return 0 45 | end 46 | 47 | set -l typ 48 | 49 | if set -q _flag_rank 50 | set typ "rank" 51 | else if set -q _flag_recent 52 | set typ "recent" 53 | end 54 | 55 | set -l f_script ' 56 | function frecent(rank, time) { 57 | dx = t-time 58 | if( dx < 3600 ) return rank*4 59 | if( dx < 86400 ) return rank*2 60 | if( dx < 604800 ) return rank/2 61 | return rank/4 62 | } 63 | 64 | function output(matches, best_match, common) { 65 | # list or return the desired file 66 | if( list ) { 67 | cmd = "sort -nr" 68 | for( x in matches ) { 69 | if( matches[x] ) { 70 | printf "%-10s %s\n", matches[x], x | cmd 71 | } 72 | } 73 | if( common ) { 74 | printf "%-10s %s\n", "common:", common > "/dev/stderr" 75 | } 76 | } else { 77 | if( common ) best_match = common 78 | print best_match 79 | } 80 | } 81 | 82 | function common(matches) { 83 | # find the common root of a list of matches, if it exists 84 | for( x in matches ) { 85 | if( matches[x] && (!short || length(x) < length(short)) ) { 86 | short = x 87 | } 88 | } 89 | if( short == "/" ) return 90 | for( x in matches ) if( matches[x] && index(x, short) != 1 ) { 91 | return 92 | } 93 | return short 94 | } 95 | 96 | BEGIN { 97 | hi_rank = ihi_rank = -9999999999 98 | } 99 | { 100 | if( typ == "rank" ) { 101 | rank = $2 102 | } else if( typ == "recent" ) { 103 | rank = $3 - t 104 | } else rank = frecent($2, $3) 105 | if( $1 ~ q ) { 106 | matches[$1] = rank 107 | } else if( tolower($1) ~ tolower(q) ) imatches[$1] = rank 108 | if( matches[$1] && matches[$1] > hi_rank ) { 109 | best_match = $1 110 | hi_rank = matches[$1] 111 | } else if( imatches[$1] && imatches[$1] > ihi_rank ) { 112 | ibest_match = $1 113 | ihi_rank = imatches[$1] 114 | } 115 | } 116 | 117 | END { 118 | # prefer case sensitive 119 | if( best_match ) { 120 | output(matches, best_match, common(matches)) 121 | } else if( ibest_match ) { 122 | output(imatches, ibest_match, common(imatches)) 123 | } 124 | } 125 | ' 126 | 127 | set -l qs 128 | for arg in $argv 129 | set -l escaped $arg 130 | set escaped (string escape --style=regex $escaped) 131 | # Need to escape twice, see https://www.math.utah.edu/docs/info/gawk_5.html#SEC32 132 | set escaped (string replace --all \\ \\\\ $escaped) 133 | set qs $qs $escaped 134 | end 135 | set -l q (string join '.*' $qs) 136 | 137 | if set -q _flag_list 138 | # Handle list separately as it can print common path information to stderr 139 | # which cannot be captured from a subcommand. 140 | command awk -v t=(date +%s) -v list="list" -v typ="$typ" -v q="$q" -F "|" $f_script "$F_DATA" 141 | else 142 | set -l targets 143 | 144 | if set -q _flag_pick; or set -q _flag_picker 145 | 146 | if not set -q _flag_picker 147 | if set -q F_PICKER 148 | set _flag_picker $F_PICKER 149 | else 150 | set _flag_picker "fzf --multi --no-sort --tiebreak=end,length --query '$argv'" 151 | end 152 | end 153 | 154 | # pick command string should be tokenized first to be executed as command 155 | printf '%s' $_flag_picker | read -at _flag_picker 156 | 157 | set targets ( 158 | command awk -v t=(date +%s) -v list="list" -v typ="$typ" -v q=".*" -F "|" $f_script "$F_DATA" | 159 | string replace -r '^.{11}' '' | 160 | $_flag_picker 161 | ) 162 | else 163 | set targets (command awk -v t=(date +%s) -v typ="$typ" -v q="$q" -F "|" $f_script "$F_DATA") 164 | end 165 | 166 | if test "$status" -gt 0 167 | return 168 | end 169 | 170 | if test -z "$targets" 171 | echo "'$argv' did not match any results" 172 | return 1 173 | end 174 | 175 | if set -q _flag_delete 176 | for target in $targets 177 | sed -i -e "\:^$target|.*:d" $F_DATA 178 | echo "Deleted entry '$target'" 179 | end 180 | return 0 181 | end 182 | 183 | if set -q _flag_cd 184 | # cd into directory of [first] file 185 | pushd (string split -rm 1 '/' $targets[1])[1] 2> /dev/null 186 | if test $status -gt 0 187 | echo "Parent directory of '$targets[1]' does not exist" 188 | return 1 189 | end 190 | end 191 | 192 | for target in $targets 193 | __f_add $target 194 | end 195 | 196 | if set -q _flag_echo 197 | printf "%s\n" $targets 198 | return 0 199 | end 200 | 201 | set -l opencmd 202 | 203 | if set -q _flag_with 204 | set opencmd $_flag_with 205 | else if set -q _flag_app 206 | if test (uname) = Darwin 207 | set opencmd open 208 | else 209 | set opencmd xdg-open 210 | end 211 | else if set -q VISUAL 212 | set opencmd $VISUAL 213 | else if set -q EDITOR 214 | set opencmd $EDITOR 215 | else 216 | echo "\$EDITOR not set; cannot open file" > /dev/stderr 217 | return 1 218 | end 219 | 220 | # `$opencmd $targets` won't work if $opencmd is quoted; use source instead 221 | printf '%s ' $opencmd (string escape $targets) | source 222 | end 223 | end 224 | --------------------------------------------------------------------------------