pax_global_header00006660000000000000000000000064135761764750014537gustar00rootroot0000000000000052 comment=30577b0c17afff0293700ffa6b7ccb0c954e0b96 fzf-0.20.0/000077500000000000000000000000001357617647500124035ustar00rootroot00000000000000fzf-0.20.0/.github/000077500000000000000000000000001357617647500137435ustar00rootroot00000000000000fzf-0.20.0/.github/ISSUE_TEMPLATE.md000066400000000000000000000006471357617647500164570ustar00rootroot00000000000000 - [ ] I have read through the manual page (`man fzf`) - [ ] I have the latest version of fzf - [ ] I have searched through the existing issues ## Info - OS - [ ] Linux - [ ] Mac OS X - [ ] Windows - [ ] Etc. - Shell - [ ] bash - [ ] zsh - [ ] fish ## Problem / Steps to reproduce fzf-0.20.0/.gitignore000066400000000000000000000001231357617647500143670ustar00rootroot00000000000000bin/fzf bin/fzf.exe target pkg Gemfile.lock .DS_Store doc/tags vendor gopath *.zwc fzf-0.20.0/.travis.yml000066400000000000000000000007501357617647500145160ustar00rootroot00000000000000language: go dist: xenial addons: apt: sources: - sourceline: "ppa:pi-rho/dev" - sourceline: "ppa:fish-shell/release-2" packages: - tmux - zsh - fish env: - GO111MODULE=on jobs: include: - stage: unittest go: "1.13.x" script: make && make test - stage: cli go: "1.13.x" rvm: "2.5" script: | make install && ./install --all && tmux new "ruby test/test_go.rb > out && touch ok" && cat out && [ -e ok ] fzf-0.20.0/BUILD.md000066400000000000000000000022051357617647500135630ustar00rootroot00000000000000Building fzf ============ Build instructions ------------------ ### Prerequisites - Go 1.11 or above ### Using Makefile ```sh # Build fzf binary for your platform in target make # Build fzf binary and copy it to bin directory make install # Build 32-bit and 64-bit executables and tarballs in target make release # Make release archives for all supported platforms in target make release-all ``` ### Using `go get` Alternatively, you can build fzf directly with `go get` command without manually cloning the repository. ```sh go get -u github.com/junegunn/fzf ``` Third-party libraries used -------------------------- - [mattn/go-runewidth](https://github.com/mattn/go-runewidth) - Licensed under [MIT](http://mattn.mit-license.org) - [mattn/go-shellwords](https://github.com/mattn/go-shellwords) - Licensed under [MIT](http://mattn.mit-license.org) - [mattn/go-isatty](https://github.com/mattn/go-isatty) - Licensed under [MIT](http://mattn.mit-license.org) - [tcell](https://github.com/gdamore/tcell) - Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE) License ------- [MIT](LICENSE) fzf-0.20.0/CHANGELOG.md000066400000000000000000000624471357617647500142310ustar00rootroot00000000000000CHANGELOG ========= 0.20.0 ------ - Customizable preview window color (`preview-fg` and `preview-bg` for `--color`) ```sh fzf --preview 'cat {}' \ --color 'fg:#bbccdd,fg+:#ddeeff,bg:#334455,preview-bg:#223344,border:#778899' \ --border --height 20 --layout reverse --info inline ``` - Removed the immediate flicking of the screen on `reload` action. ```sh : | fzf --bind 'change:reload:seq {q}' --phony ``` - Added `clear-query` and `clear-selection` actions for `--bind` - It is now possible to split a composite bind action over multiple `--bind` expressions by prefixing the later ones with `+`. ```sh fzf --bind 'ctrl-a:up+up' # Can be now written as fzf --bind 'ctrl-a:up' --bind 'ctrl-a:+up' # This is useful when you need to write special execute/reload form (i.e. `execute:...`) # to avoid parse errors and add more actions to the same key fzf --multi --bind 'ctrl-l:select-all+execute:less {+f}' --bind 'ctrl-l:+deselect-all' ``` - Fixed parse error of `--bind` expression where concatenated execute/reload action contains `+` character. ```sh fzf --multi --bind 'ctrl-l:select-all+execute(less {+f})+deselect-all' ``` - Fixed bugs of reload action - Not triggered when there's no match even when the command doesn't have any placeholder expressions - Screen not properly cleared when `--header-lines` not filled on reload 0.19.0 ------ - Added `--phony` option which completely disables search functionality. Useful when you want to use fzf only as a selector interface. See below. - Added "reload" action for dynamically updating the input list without restarting fzf. See https://github.com/junegunn/fzf/issues/1750 to learn more about it. ```sh # Using fzf as the selector interface for ripgrep RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " INITIAL_QUERY="foo" FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY' || true" \ fzf --bind "change:reload:$RG_PREFIX {q} || true" \ --ansi --phony --query "$INITIAL_QUERY" ``` - `--multi` now takes an optional integer argument which indicates the maximum number of items that can be selected ```sh seq 100 | fzf --multi 3 --reverse --height 50% ``` - If a placeholder expression for `--preview` and `execute` action (and the new `reload` action) contains `f` flag, it is replaced to the path of a temporary file that holds the evaluated list. This is useful when you multi-select a large number of items and the length of the evaluated string may exceed [`ARG_MAX`][argmax]. ```sh # Press CTRL-A to select 100K items and see the sum of all the numbers seq 100000 | fzf --multi --bind ctrl-a:select-all \ --preview "awk '{sum+=\$1} END {print sum}' {+f}" ``` - `deselect-all` no longer deselects unmatched items. It is now consistent with `select-all` and `toggle-all` in that it only affects matched items. - Due to the limitation of bash, fuzzy completion is enabled by default for a fixed set of commands. A helper function for easily setting up fuzzy completion for any command is now provided. ```sh # usage: _fzf_setup_completion path|dir COMMANDS... _fzf_setup_completion path git kubectl ``` - Info line style can be changed by `--info=STYLE` - `--info=default` - `--info=inline` (same as old `--inline-info`) - `--info=hidden` - Preview window border can be disabled by adding `noborder` to `--preview-window`. - When you transform the input with `--with-nth`, the trailing white spaces are removed. - `ctrl-\`, `ctrl-]`, `ctrl-^`, and `ctrl-/` can now be used with `--bind` - See https://github.com/junegunn/fzf/milestone/15?closed=1 for more details [argmax]: https://unix.stackexchange.com/questions/120642/what-defines-the-maximum-size-for-a-command-single-argument 0.18.0 ------ - Added placeholder expression for zero-based item index: `{n}` and `{+n}` - `fzf --preview 'echo {n}: {}'` - Added color option for the gutter: `--color gutter:-1` - Added `--no-unicode` option for drawing borders in non-Unicode, ASCII characters - `FZF_PREVIEW_LINES` and `FZF_PREVIEW_COLUMNS` are exported to preview process - fzf still overrides `LINES` and `COLUMNS` as before, but they may be reset by the default shell. - Bug fixes and improvements - See https://github.com/junegunn/fzf/milestone/14?closed=1 - Built with Go 1.12.1 0.17.5 ------ - Bug fixes and improvements - See https://github.com/junegunn/fzf/milestone/13?closed=1 - Search query longer than the screen width is allowed (up to 300 chars) - Built with Go 1.11.1 0.17.4 ------ - Added `--layout` option with a new layout called `reverse-list`. - `--layout=reverse` is a synonym for `--reverse` - `--layout=default` is a synonym for `--no-reverse` - Preview window will be updated even when there is no match for the query if any of the placeholder expressions (e.g. `{q}`, `{+}`) evaluates to a non-empty string. - More keys for binding: `shift-{up,down}`, `alt-{up,down,left,right}` - fzf can now start even when `/dev/tty` is not available by making an educated guess. - Updated the default command for Windows. - Fixes and improvements on bash/zsh completion - install and uninstall scripts now supports generating files under `XDG_CONFIG_HOME` on `--xdg` flag. See https://github.com/junegunn/fzf/milestone/12?closed=1 for the full list of changes. 0.17.3 ------ - `$LINES` and `$COLUMNS` are exported to preview command so that the command knows the exact size of the preview window. - Better error messages when the default command or `$FZF_DEFAULT_COMMAND` fails. - Reverted #1061 to avoid having duplicate entries in the list when find command detected a file system loop (#1120). The default command now requires that find supports `-fstype` option. - fzf now distinguishes mouse left click and right click (#1130) - Right click is now bound to `toggle` action by default - `--bind` understands `left-click` and `right-click` - Added `replace-query` action (#1137) - Replaces query string with the current selection - Added `accept-non-empty` action (#1162) - Same as accept, except that it prevents fzf from exiting without any selection 0.17.1 ------ - Fixed custom background color of preview window (#1046) - Fixed background color issues of Windows binary - Fixed Windows binary to execute command using cmd.exe with no parsing and escaping (#1072) - Added support for `window` layout on Vim 8 using Vim 8 terminal (#1055) 0.17.0-2 -------- A maintenance release for auxiliary scripts. fzf binaries are not updated. - Experimental support for the builtin terminal of Vim 8 - fzf can now run inside GVim - Updated Vim plugin to better handle `&shell` issue on fish - Fixed a bug of fzf-tmux where invalid output is generated - Fixed fzf-tmux to work even when `tput` does not work 0.17.0 ------ - Performance optimization - One can match literal spaces in extended-search mode with a space prepended by a backslash. - `--expect` is now additive and can be specified multiple times. 0.16.11 ------- - Performance optimization - Fixed missing preview update 0.16.10 ------- - Fixed invalid handling of ANSI colors in preview window - Further improved `--ansi` performance 0.16.9 ------ - Memory and performance optimization - Around 20% performance improvement for general use cases - Up to 5x faster processing of `--ansi` - Up to 50% reduction of memory usage - Bug fixes and usability improvements - Fixed handling of bracketed paste mode - [ERROR] on info line when the default command failed - More efficient rendering of preview window - `--no-clear` updated for repetitive relaunching scenarios 0.16.8 ------ - New `change` event and `top` action for `--bind` - `fzf --bind change:top` - Move cursor to the top result whenever the query string is changed - `fzf --bind 'ctrl-w:unix-word-rubout+top,ctrl-u:unix-line-discard+top'` - `top` combined with `unix-word-rubout` and `unix-line-discard` - Fixed inconsistent tiebreak scores when `--nth` is used - Proper display of tab characters in `--prompt` - Fixed not to `--cycle` on page-up/page-down to prevent overshoot - Git revision in `--version` output - Basic support for Cygwin environment - Many fixes in Vim plugin on Windows/Cygwin (thanks to @janlazo) 0.16.7 ------ - Added support for `ctrl-alt-[a-z]` key chords - CTRL-Z (SIGSTOP) now works with fzf - fzf will export `$FZF_PREVIEW_WINDOW` so that the scripts can use it - Bug fixes and improvements in Vim plugin and shell extensions 0.16.6 ------ - Minor bug fixes and improvements - Added `--no-clear` option for scripting purposes 0.16.5 ------ - Minor bug fixes - Added `toggle-preview-wrap` action - Built with Go 1.8 0.16.4 ------ - Added `--border` option to draw border above and below the finder - Bug fixes and improvements 0.16.3 ------ - Fixed a bug where fzf incorrectly display the lines when straddling tab characters are trimmed - Placeholder expression used in `--preview` and `execute` action can optionally take `+` flag to be used with multiple selections - e.g. `git log --oneline | fzf --multi --preview 'git show {+1}'` - Added `execute-silent` action for executing a command silently without switching to the alternate screen. This is useful when the process is short-lived and you're not interested in its output. - e.g. `fzf --bind 'ctrl-y:execute!(echo -n {} | pbcopy)'` - `ctrl-space` is allowed in `--bind` 0.16.2 ------ - Dropped ncurses dependency - Binaries for freebsd, openbsd, arm5, arm6, arm7, and arm8 - Official 24-bit color support - Added support for composite actions in `--bind`. Multiple actions can be chained using `+` separator. - e.g. `fzf --bind 'ctrl-y:execute(echo -n {} | pbcopy)+abort'` - `--preview-window` with size 0 is allowed. This is used to make fzf execute preview command in the background without displaying the result. - Minor bug fixes and improvements 0.16.1 ------ - Fixed `--height` option to properly fill the window with the background color - Added `half-page-up` and `half-page-down` actions - Added `-L` flag to the default find command 0.16.0 ------ - *Added `--height HEIGHT[%]` option* - fzf can now display finder without occupying the full screen - Preview window will truncate long lines by default. Line wrap can be enabled by `:wrap` flag in `--preview-window`. - Latin script letters will be normalized before matching so that it's easier to match against accented letters. e.g. `sodanco` can match `Só Danço Samba`. - Normalization can be disabled via `--literal` - Added `--filepath-word` to make word-wise movements/actions (`alt-b`, `alt-f`, `alt-bs`, `alt-d`) respect path separators 0.15.9 ------ - Fixed rendering glitches introduced in 0.15.8 - The default escape delay is reduced to 50ms and is configurable via `$ESCDELAY` - Scroll indicator at the top-right corner of the preview window is always displayed when there's overflow - Can now be built with ncurses 6 or tcell to support extra features - *ncurses 6* - Supports more than 256 color pairs - Supports italics - *tcell* - 24-bit color support - See https://github.com/junegunn/fzf/blob/master/BUILD.md 0.15.8 ------ - Updated ANSI processor to handle more VT-100 escape sequences - Added `--no-bold` (and `--bold`) option - Improved escape sequence processing for WSL - Added support for `alt-[0-9]`, `f11`, and `f12` for `--bind` and `--expect` 0.15.7 ------ - Fixed panic when color is disabled and header lines contain ANSI colors 0.15.6 ------ - Windows binaries! (@kelleyma49) - Fixed the bug where header lines are cleared when preview window is toggled - Fixed not to display ^N and ^O on screen - Fixed cursor keys (or any key sequence that starts with ESC) on WSL by making fzf wait for additional keystrokes after ESC for up to 100ms 0.15.5 ------ - Setting foreground color will no longer set background color to black - e.g. `fzf --color fg:153` - `--tiebreak=end` will consider relative position instead of absolute distance - Updated `fzf#wrap` function to respect `g:fzf_colors` 0.15.4 ------ - Added support for range expression in preview and execute action - e.g. `ls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1` - `{q}` will be replaced to the single-quoted string of the current query - Fixed to properly handle unicode whitespace characters - Display scroll indicator in preview window - Inverse search term will use exact matcher by default - This is a breaking change, but I believe it makes much more sense. It is almost impossible to predict which entries will be filtered out due to a fuzzy inverse term. You can still perform inverse-fuzzy-match by prepending `!'` to the term. 0.15.3 ------ - Added support for more ANSI attributes: dim, underline, blink, and reverse - Fixed race condition in `toggle-preview` 0.15.2 ------ - Preview window is now scrollable - With mouse scroll or with bindable actions - `preview-up` - `preview-down` - `preview-page-up` - `preview-page-down` - Updated ANSI processor to support high intensity colors and ignore some VT100-related escape sequences 0.15.1 ------ - Fixed panic when the pattern occurs after 2^15-th column - Fixed rendering delay when displaying extremely long lines 0.15.0 ------ - Improved fuzzy search algorithm - Added `--algo=[v1|v2]` option so one can still choose the old algorithm which values the search performance over the quality of the result - Advanced scoring criteria - `--read0` to read input delimited by ASCII NUL character - `--print0` to print output delimited by ASCII NUL character 0.13.5 ------ - Memory and performance optimization - Up to 2x performance with half the amount of memory 0.13.4 ------ - Performance optimization - Memory footprint for ascii string is reduced by 60% - 15 to 20% improvement of query performance - Up to 45% better performance of `--nth` with non-regex delimiters - Fixed invalid handling of `hidden` property of `--preview-window` 0.13.3 ------ - Fixed duplicate rendering of the last line in preview window 0.13.2 ------ - Fixed race condition where preview window is not properly cleared 0.13.1 ------ - Fixed UI issue with large `--preview` output with many ANSI codes 0.13.0 ------ - Added preview feature - `--preview CMD` - `--preview-window POS[:SIZE][:hidden]` - `{}` in execute action is now replaced to the single-quoted (instead of double-quoted) string of the current line - Fixed to ignore control characters for bracketed paste mode 0.12.2 ------ - 256-color capability detection does not require `256` in `$TERM` - Added `print-query` action - More named keys for binding; F1 ~ F10, ALT-/, ALT-space, and ALT-enter - Added `jump` and `jump-accept` actions that implement [EasyMotion][em]-like movement ![][jump] [em]: https://github.com/easymotion/vim-easymotion [jump]: https://cloud.githubusercontent.com/assets/700826/15367574/b3999dc4-1d64-11e6-85da-28ceeb1a9bc2.png 0.12.1 ------ - Ranking algorithm introduced in 0.12.0 is now universally applied - Fixed invalid cache reference in exact mode - Fixes and improvements in Vim plugin and shell extensions 0.12.0 ------ - Enhanced ranking algorithm - Minor bug fixes 0.11.4 ------ - Added `--hscroll-off=COL` option (default: 10) (#513) - Some fixes in Vim plugin and shell extensions 0.11.3 ------ - Graceful exit on SIGTERM (#482) - `$SHELL` instead of `sh` for `execute` action and `$FZF_DEFAULT_COMMAND` (#481) - Changes in fuzzy completion API - [`_fzf_compgen_{path,dir}`](https://github.com/junegunn/fzf/commit/9617647) - [`_fzf_complete_COMMAND_post`](https://github.com/junegunn/fzf/commit/8206746) for post-processing 0.11.2 ------ - `--tiebreak` now accepts comma-separated list of sort criteria - Each criterion should appear only once in the list - `index` is only allowed at the end of the list - `index` is implicitly appended to the list when not specified - Default is `length` (or equivalently `length,index`) - `begin` criterion will ignore leading whitespaces when calculating the index - Added `toggle-in` and `toggle-out` actions - Switch direction depending on `--reverse`-ness - `export FZF_DEFAULT_OPTS="--bind tab:toggle-out,shift-tab:toggle-in"` - Reduced the initial delay when `--tac` is not given - fzf defers the initial rendering of the screen up to 100ms if the input stream is ongoing to prevent unnecessary redraw during the initial phase. However, 100ms delay is quite noticeable and might give the impression that fzf is not snappy enough. This commit reduces the maximum delay down to 20ms when `--tac` is not specified, in which case the input list quickly fills the entire screen. 0.11.1 ------ - Added `--tabstop=SPACES` option 0.11.0 ------ - Added OR operator for extended-search mode - Added `--execute-multi` action - Fixed incorrect cursor position when unicode wide characters are used in `--prompt` - Fixes and improvements in shell extensions 0.10.9 ------ - Extended-search mode is now enabled by default - `--extended-exact` is deprecated and instead we have `--exact` for orthogonally controlling "exactness" of search - Fixed not to display non-printable characters - Added `double-click` for `--bind` option - More robust handling of SIGWINCH 0.10.8 ------ - Fixed panic when trying to set colors after colors are disabled (#370) 0.10.7 ------ - Fixed unserialized interrupt handling during execute action which often caused invalid memory access and crash - Changed `--tiebreak=length` (default) to use trimmed length when `--nth` is used 0.10.6 ------ - Replaced `--header-file` with `--header` option - `--header` and `--header-lines` can be used together - Changed exit status - 0: Okay - 1: No match - 2: Error - 130: Interrupted - 64-bit linux binary is statically-linked with ncurses to avoid compatibility issues. 0.10.5 ------ - `'`-prefix to unquote the term in `--extended-exact` mode - Backward scan when `--tiebreak=end` is set 0.10.4 ------ - Fixed to remove ANSI code from output when `--with-nth` is set 0.10.3 ------ - Fixed slow performance of `--with-nth` when used with `--delimiter` - Regular expression engine of Golang as of now is very slow, so the fixed version will treat the given delimiter pattern as a plain string instead of a regular expression unless it contains special characters and is a valid regular expression. - Simpler regular expression for delimiter for better performance 0.10.2 ------ ### Fixes and improvements - Improvement in perceived response time of queries - Eager, efficient rune array conversion - Graceful exit when failed to initialize ncurses (invalid $TERM) - Improved ranking algorithm when `--nth` option is set - Changed the default command not to fail when there are files whose names start with dash 0.10.1 ------ ### New features - Added `--margin` option - Added options for sticky header - `--header-file` - `--header-lines` - Added `cancel` action which clears the input or closes the finder when the input is already empty - e.g. `export FZF_DEFAULT_OPTS="--bind esc:cancel"` - Added `delete-char/eof` action to differentiate `CTRL-D` and `DEL` ### Minor improvements/fixes - Fixed to allow binding colon and comma keys - Fixed ANSI processor to handle color regions spanning multiple lines 0.10.0 ------ ### New features - More actions for `--bind` - `select-all` - `deselect-all` - `toggle-all` - `ignore` - `execute(...)` action for running arbitrary command without leaving fzf - `fzf --bind "ctrl-m:execute(less {})"` - `fzf --bind "ctrl-t:execute(tmux new-window -d 'vim {}')"` - If the command contains parentheses, use any of the follows alternative notations to avoid parse errors - `execute[...]` - `execute~...~` - `execute!...!` - `execute@...@` - `execute#...#` - `execute$...$` - `execute%...%` - `execute^...^` - `execute&...&` - `execute*...*` - `execute;...;` - `execute/.../` - `execute|...|` - `execute:...` - This is the special form that frees you from parse errors as it does not expect the closing character - The catch is that it should be the last one in the comma-separated list - Added support for optional search history - `--history HISTORY_FILE` - When used, `CTRL-N` and `CTRL-P` are automatically remapped to `next-history` and `previous-history` - `--history-size MAX_ENTRIES` (default: 1000) - Cyclic scrolling can be enabled with `--cycle` - Fixed the bug where the spinner was not spinning on idle input stream - e.g. `sleep 100 | fzf` ### Minor improvements/fixes - Added synonyms for key names that can be specified for `--bind`, `--toggle-sort`, and `--expect` - Fixed the color of multi-select marker on the current line - Fixed to allow `^pattern$` in extended-search mode 0.9.13 ------ ### New features - Color customization with the extended `--color` option ### Bug fixes - Fixed premature termination of Reader in the presence of a long line which is longer than 64KB 0.9.12 ------ ### New features - Added `--bind` option for custom key bindings ### Bug fixes - Fixed to update "inline-info" immediately after terminal resize - Fixed ANSI code offset calculation 0.9.11 ------ ### New features - Added `--inline-info` option for saving screen estate (#202) - Useful inside Neovim - e.g. `let $FZF_DEFAULT_OPTS = $FZF_DEFAULT_OPTS.' --inline-info'` ### Bug fixes - Invalid mutation of input on case conversion (#209) - Smart-case for each term in extended-search mode (#208) - Fixed double-click result when scroll offset is positive 0.9.10 ------ ### Improvements - Performance optimization - Less aggressive memoization to limit memory usage ### New features - Added color scheme for light background: `--color=light` 0.9.9 ----- ### New features - Added `--tiebreak` option (#191) - Added `--no-hscroll` option (#193) - Visual indication of `--toggle-sort` (#194) 0.9.8 ----- ### Bug fixes - Fixed Unicode case handling (#186) - Fixed to terminate on RuneError (#185) 0.9.7 ----- ### New features - Added `--toggle-sort` option (#173) - `--toggle-sort=ctrl-r` is applied to `CTRL-R` shell extension ### Bug fixes - Fixed to print empty line if `--expect` is set and fzf is completed by `--select-1` or `--exit-0` (#172) - Fixed to allow comma character as an argument to `--expect` option 0.9.6 ----- ### New features #### Added `--expect` option (#163) If you provide a comma-separated list of keys with `--expect` option, fzf will allow you to select the match and complete the finder when any of the keys is pressed. Additionally, fzf will print the name of the key pressed as the first line of the output so that your script can decide what to do next based on the information. ```sh fzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@ ``` The updated vim plugin uses this option to implement [ctrlp](https://github.com/kien/ctrlp.vim)-compatible key bindings. ### Bug fixes - Fixed to ignore ANSI escape code `\e[K` (#162) 0.9.5 ----- ### New features #### Added `--ansi` option (#150) If you give `--ansi` option to fzf, fzf will interpret ANSI color codes from the input, display the item with the ANSI colors (true colors are not supported), and strips the codes from the output. This option is off by default as it entails some overhead. ### Improvements #### Reduced initial memory footprint (#151) By removing unnecessary copy of pointers, fzf will use significantly smaller amount of memory when it's started. The difference is hugely noticeable when the input is extremely large. (e.g. `locate / | fzf`) ### Bug fixes - Fixed panic on `--no-sort --filter ''` (#149) 0.9.4 ----- ### New features #### Added `--tac` option to reverse the order of the input. One might argue that this option is unnecessary since we can already put `tac` or `tail -r` in the command pipeline to achieve the same result. However, the advantage of `--tac` is that it does not block until the input is complete. ### *Backward incompatible changes* #### Changed behavior on `--no-sort` `--no-sort` option will no longer reverse the display order within finder. You may want to use the new `--tac` option with `--no-sort`. ``` history | fzf +s --tac ``` ### Improvements #### `--filter` will not block when sort is disabled When fzf works in filtering mode (`--filter`) and sort is disabled (`--no-sort`), there's no need to block until input is complete. The new version of fzf will print the matches on-the-fly when the following condition is met: --filter TERM --no-sort [--no-tac --no-sync] or simply: -f TERM +s This change removes unnecessary delay in the use cases like the following: fzf -f xxx +s | head -5 However, in this case, fzf processes the lines sequentially, so it cannot utilize multiple cores, and fzf will run slightly slower than the previous mode of execution where filtering is done in parallel after the entire input is loaded. If the user is concerned about this performance problem, one can add `--sync` option to re-enable buffering. 0.9.3 ----- ### New features - Added `--sync` option for multi-staged filtering ### Improvements - `--select-1` and `--exit-0` will start finder immediately when the condition cannot be met fzf-0.20.0/Dockerfile000066400000000000000000000007331357617647500144000ustar00rootroot00000000000000FROM archlinux/base:latest RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make RUN gem install --no-document minitest RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc RUN echo '. ~/.bashrc' >> ~/.bash_profile # Do not set default PS1 RUN rm -f /etc/bash.bashrc COPY . /fzf RUN cd /fzf && make install && ./install --all CMD tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ] fzf-0.20.0/LICENSE000066400000000000000000000020701357617647500134070ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 Junegunn Choi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. fzf-0.20.0/Makefile000066400000000000000000000104231357617647500140430ustar00rootroot00000000000000GO ?= go GOOS ?= $(word 1, $(subst /, " ", $(word 4, $(shell go version)))) MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST))) ROOT_DIR := $(shell dirname $(MAKEFILE)) SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(MAKEFILE) REVISION := $(shell git log -n 1 --pretty=format:%h -- $(SOURCES)) BUILD_FLAGS := -a -ldflags "-X main.revision=$(REVISION) -w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" BINARY32 := fzf-$(GOOS)_386 BINARY64 := fzf-$(GOOS)_amd64 BINARYARM5 := fzf-$(GOOS)_arm5 BINARYARM6 := fzf-$(GOOS)_arm6 BINARYARM7 := fzf-$(GOOS)_arm7 BINARYARM8 := fzf-$(GOOS)_arm8 BINARYPPC64LE := fzf-$(GOOS)_ppc64le VERSION := $(shell awk -F= '/version =/ {print $$2}' src/constants.go | tr -d "\" ") RELEASE32 := fzf-$(VERSION)-$(GOOS)_386 RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64 RELEASEARM5 := fzf-$(VERSION)-$(GOOS)_arm5 RELEASEARM6 := fzf-$(VERSION)-$(GOOS)_arm6 RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7 RELEASEARM8 := fzf-$(VERSION)-$(GOOS)_arm8 RELEASEPPC64LE := fzf-$(VERSION)-$(GOOS)_ppc64le # https://en.wikipedia.org/wiki/Uname UNAME_M := $(shell uname -m) ifeq ($(UNAME_M),x86_64) BINARY := $(BINARY64) else ifeq ($(UNAME_M),amd64) BINARY := $(BINARY64) else ifeq ($(UNAME_M),i686) BINARY := $(BINARY32) else ifeq ($(UNAME_M),i386) BINARY := $(BINARY32) else ifeq ($(UNAME_M),armv5l) BINARY := $(BINARYARM5) else ifeq ($(UNAME_M),armv6l) BINARY := $(BINARYARM6) else ifeq ($(UNAME_M),armv7l) BINARY := $(BINARYARM7) else ifeq ($(UNAME_M),armv8l) BINARY := $(BINARYARM8) else ifeq ($(UNAME_M),aarch64) BINARY := $(BINARYARM8) else ifeq ($(UNAME_M),ppc64le) BINARY := $(BINARYPPC64LE) else $(error "Build on $(UNAME_M) is not supported, yet.") endif all: target/$(BINARY) target: mkdir -p $@ ifeq ($(GOOS),windows) release: target/$(BINARY32) target/$(BINARY64) cd target && cp -f $(BINARY32) fzf.exe && zip $(RELEASE32).zip fzf.exe cd target && cp -f $(BINARY64) fzf.exe && zip $(RELEASE64).zip fzf.exe cd target && rm -f fzf.exe else ifeq ($(GOOS),linux) release: target/$(BINARY32) target/$(BINARY64) target/$(BINARYARM5) target/$(BINARYARM6) target/$(BINARYARM7) target/$(BINARYARM8) target/$(BINARYPPC64LE) cd target && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf cd target && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf cd target && cp -f $(BINARYARM5) fzf && tar -czf $(RELEASEARM5).tgz fzf cd target && cp -f $(BINARYARM6) fzf && tar -czf $(RELEASEARM6).tgz fzf cd target && cp -f $(BINARYARM7) fzf && tar -czf $(RELEASEARM7).tgz fzf cd target && cp -f $(BINARYARM8) fzf && tar -czf $(RELEASEARM8).tgz fzf cd target && cp -f $(BINARYPPC64LE) fzf && tar -czf $(RELEASEPPC64LE).tgz fzf cd target && rm -f fzf else release: target/$(BINARY32) target/$(BINARY64) cd target && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf cd target && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf cd target && rm -f fzf endif release-all: clean test GOOS=darwin make release GOOS=linux make release GOOS=freebsd make release GOOS=openbsd make release GOOS=windows make release test: $(SOURCES) SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \ github.com/junegunn/fzf/src \ github.com/junegunn/fzf/src/algo \ github.com/junegunn/fzf/src/tui \ github.com/junegunn/fzf/src/util install: bin/fzf clean: $(RM) -r target target/$(BINARY32): $(SOURCES) GOARCH=386 $(GO) build $(BUILD_FLAGS) -o $@ target/$(BINARY64): $(SOURCES) GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@ # https://github.com/golang/go/wiki/GoArm target/$(BINARYARM5): $(SOURCES) GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@ target/$(BINARYARM6): $(SOURCES) GOARCH=arm GOARM=6 $(GO) build $(BUILD_FLAGS) -o $@ target/$(BINARYARM7): $(SOURCES) GOARCH=arm GOARM=7 $(GO) build $(BUILD_FLAGS) -o $@ target/$(BINARYARM8): $(SOURCES) GOARCH=arm64 $(GO) build $(BUILD_FLAGS) -o $@ target/$(BINARYPPC64LE): $(SOURCES) GOARCH=ppc64le $(GO) build $(BUILD_FLAGS) -o $@ bin/fzf: target/$(BINARY) | bin cp -f target/$(BINARY) bin/fzf docker: docker build -t fzf-arch . docker run -it fzf-arch tmux docker-test: docker build -t fzf-arch . docker run -it fzf-arch update: $(GO) get -u $(GO) mod tidy .PHONY: all release release-all test install clean docker docker-test update fzf-0.20.0/README-VIM.md000066400000000000000000000260221357617647500143150ustar00rootroot00000000000000FZF Vim integration =================== Summary ------- The Vim plugin of fzf provides two core functions, and `:FZF` command which is the basic file selector command built on top of them. 1. **`fzf#run([spec dict])`** - Starts fzf inside Vim with the given spec - `:call fzf#run({'source': 'ls'})` 2. **`fzf#wrap([spec dict]) -> (dict)`** - Takes a spec for `fzf#run` and returns an extended version of it with additional options for addressing global preferences (`g:fzf_xxx`) - `:echo fzf#wrap({'source': 'ls'})` - We usually *wrap* a spec with `fzf#wrap` before passing it to `fzf#run` - `:call fzf#run(fzf#wrap({'source': 'ls'}))` 3. **`:FZF [fzf_options string] [path string]`** - Basic fuzzy file selector - A reference implementation for those who don't want to write VimScript to implement custom commands - If you're looking for more such commands, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project. The most important of all is `fzf#run`, but it would be easier to understand the whole if we start off with `:FZF` command. `:FZF[!]` --------- ```vim " Look for files under current directory :FZF " Look for files under your home directory :FZF ~ " With fzf command-line options :FZF --reverse --info=inline /tmp " Bang version starts fzf in fullscreen mode :FZF! ``` Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key, `CTRL-T`, `CTRL-X` or `CTRL-V` to open selected files in the current window, in new tabs, in horizontal splits, or in vertical splits respectively. Note that the environment variables `FZF_DEFAULT_COMMAND` and `FZF_DEFAULT_OPTS` also apply here. ### Configuration - `g:fzf_action` - Customizable extra key bindings for opening selected files in different ways - `g:fzf_layout` - Determines the size and position of fzf window - `g:fzf_colors` - Customizes fzf colors to match the current color scheme - `g:fzf_history_dir` - Enables history feature #### Examples ```vim " This is the default extra key bindings let g:fzf_action = { \ 'ctrl-t': 'tab split', \ 'ctrl-x': 'split', \ 'ctrl-v': 'vsplit' } " An action can be a reference to a function that processes selected lines function! s:build_quickfix_list(lines) call setqflist(map(copy(a:lines), '{ "filename": v:val }')) copen cc endfunction let g:fzf_action = { \ 'ctrl-q': function('s:build_quickfix_list'), \ 'ctrl-t': 'tab split', \ 'ctrl-x': 'split', \ 'ctrl-v': 'vsplit' } " Default fzf layout " - down / up / left / right let g:fzf_layout = { 'down': '~40%' } " You can set up fzf window using a Vim command (Neovim or latest Vim 8 required) let g:fzf_layout = { 'window': 'enew' } let g:fzf_layout = { 'window': '-tabnew' } let g:fzf_layout = { 'window': '10new' } " Customize fzf colors to match your color scheme " - fzf#wrap translates this to a set of `--color` options let g:fzf_colors = \ { 'fg': ['fg', 'Normal'], \ 'bg': ['bg', 'Normal'], \ 'hl': ['fg', 'Comment'], \ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'], \ 'bg+': ['bg', 'CursorLine', 'CursorColumn'], \ 'hl+': ['fg', 'Statement'], \ 'info': ['fg', 'PreProc'], \ 'border': ['fg', 'Ignore'], \ 'prompt': ['fg', 'Conditional'], \ 'pointer': ['fg', 'Exception'], \ 'marker': ['fg', 'Keyword'], \ 'spinner': ['fg', 'Label'], \ 'header': ['fg', 'Comment'] } " Enable per-command history " - History files will be stored in the specified directory " - When set, CTRL-N and CTRL-P will be bound to 'next-history' and " 'previous-history' instead of 'down' and 'up'. let g:fzf_history_dir = '~/.local/share/fzf-history' ``` `fzf#run` --------- `fzf#run()` function is the core of Vim integration. It takes a single dictionary argument, *a spec*, and starts fzf process accordingly. At the very least, specify `sink` option to tell what it should do with the selected entry. ```vim call fzf#run({'sink': 'e'}) ``` We haven't specified the `source`, so this is equivalent to starting fzf on command line without standard input pipe; fzf will use find command (or `$FZF_DEFAULT_COMMAND` if defined) to list the files under the current directory. When you select one, it will open it with the sink, `:e` command. If you want to open it in a new tab, you can pass `:tabedit` command instead as the sink. ```vim call fzf#run({'sink': 'tabedit'}) ``` Instead of using the default find command, you can use any shell command as the source. The following example will list the files managed by git. It's equivalent to running `git ls-files | fzf` on shell. ```vim call fzf#run({'source': 'git ls-files', 'sink': 'e'}) ``` fzf options can be specified as `options` entry in spec dictionary. ```vim call fzf#run({'sink': 'tabedit', 'options': '--multi --reverse'}) ``` You can also pass a layout option if you don't want fzf window to take up the entire screen. ```vim " up / down / left / right / window are allowed call fzf#run({'source': 'git ls-files', 'sink': 'e', 'left': '40%'}) call fzf#run({'source': 'git ls-files', 'sink': 'e', 'window': '30vnew'}) ``` `source` doesn't have to be an external shell command, you can pass a Vim array as the source. In the next example, we pass the names of color schemes as the source to implement a color scheme selector. ```vim call fzf#run({'source': map(split(globpath(&rtp, 'colors/*.vim')), \ 'fnamemodify(v:val, ":t:r")'), \ 'sink': 'colo', 'left': '25%'}) ``` The following table summarizes the available options. | Option name | Type | Description | | -------------------------- | ------------- | ---------------------------------------------------------------- | | `source` | string | External command to generate input to fzf (e.g. `find .`) | | `source` | list | Vim list as input to fzf | | `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) | | `sink` | funcref | Reference to function to process each selected item | | `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once | | `options` | string/list | Options to fzf | | `dir` | string | Working directory | | `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) | | `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) | `options` entry can be either a string or a list. For simple cases, string should suffice, but prefer to use list type to avoid escaping issues. ```vim call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'}) call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']}) ``` `fzf#wrap` ---------- We have seen that several aspects of `:FZF` command can be configured with a set of global option variables; different ways to open files (`g:fzf_action`), window position and size (`g:fzf_layout`), color palette (`g:fzf_colors`), etc. So how can we make our custom `fzf#run` calls also respect those variables? Simply by *"wrapping"* the spec dictionary with `fzf#wrap` before passing it to `fzf#run`. - **`fzf#wrap([name string], [spec dict], [fullscreen bool]) -> (dict)`** - All arguments are optional. Usually we only need to pass a spec dictionary. - `name` is for managing history files. It is ignored if `g:fzf_history_dir` is not defined. - `fullscreen` can be either `0` or `1` (default: 0). `fzf#wrap` takes a spec and returns an extended version of it (also a dictionary) with additional options for addressing global preferences. You can examine the return value of it like so: ```vim echo fzf#wrap({'source': 'ls'}) ``` After we *"wrap"* our spec, we pass it to `fzf#run`. ```vim call fzf#run(fzf#wrap({'source': 'ls'})) ``` Now it supports `CTRL-T`, `CTRL-V`, and `CTRL-X` key bindings and it opens fzf window according to `g:fzf_layout` setting. To make it easier to use, let's define `LS` command. ```vim command! LS call fzf#run(fzf#wrap({'source': 'ls'})) ``` Type `:LS` and see how it works. We would like to make `:LS!` (bang version) open fzf in fullscreen, just like `:FZF!`. Add `-bang` to command definition, and use `` value to set the last `fullscreen` argument of `fzf#wrap` (see `:help `). ```vim " On :LS!, evaluates to '!', and '!0' becomes 1 command! -bang LS call fzf#run(fzf#wrap({'source': 'ls'}, 0)) ``` Our `:LS` command will be much more useful if we can pass a directory argument to it, so that something like `:LS /tmp` is possible. ```vim command! -bang -complete=dir -nargs=* LS \ call fzf#run(fzf#wrap({'source': 'ls', 'dir': }, 0)) ``` Lastly, if you have enabled `g:fzf_history_dir`, you might want to assign a unique name to our command and pass it as the first argument to `fzf#wrap`. ```vim " The query history for this command will be stored as 'ls' inside g:fzf_history_dir. " The name is ignored if g:fzf_history_dir is not defined. command! -bang -complete=dir -nargs=* LS \ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': }, 0)) ``` Tips ---- ### fzf inside terminal buffer The latest versions of Vim and Neovim include builtin terminal emulator (`:terminal`) and fzf will start in a terminal buffer in the following cases: - On Neovim - On GVim - On Terminal Vim with a non-default layout - `call fzf#run({'left': '30%'})` or `let g:fzf_layout = {'left': '30%'}` #### Starting fzf in Neovim floating window ```vim " Using floating windows of Neovim to start fzf if has('nvim') let $FZF_DEFAULT_OPTS .= ' --border --margin=0,2' function! FloatingFZF() let width = float2nr(&columns * 0.9) let height = float2nr(&lines * 0.6) let opts = { 'relative': 'editor', \ 'row': (&lines - height) / 2, \ 'col': (&columns - width) / 2, \ 'width': width, \ 'height': height } let win = nvim_open_win(nvim_create_buf(v:false, v:true), v:true, opts) call setwinvar(win, '&winhighlight', 'NormalFloat:Normal') endfunction let g:fzf_layout = { 'window': 'call FloatingFZF()' } endif ``` #### Hide statusline When fzf starts in a terminal buffer, the file type of the buffer is set to `fzf`. So you can set up `FileType fzf` autocmd to customize the settings of the window. For example, if you use the default layout (`{'down': '~40%'}`) on Neovim, you might want to temporarily disable the statusline for a cleaner look. ```vim if has('nvim') && !exists('g:fzf_layout') autocmd! FileType fzf autocmd FileType fzf set laststatus=0 noshowmode noruler \| autocmd BufLeave set laststatus=2 showmode ruler endif ``` [License](LICENSE) ------------------ The MIT License (MIT) Copyright (c) 2019 Junegunn Choi fzf-0.20.0/README.md000066400000000000000000000436701357617647500136740ustar00rootroot00000000000000fzf - a command-line fuzzy finder [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf) === fzf is a general-purpose command-line fuzzy finder. It's an interactive Unix filter for command-line that can be used with any list; files, command history, processes, hostnames, bookmarks, git commits, etc. Pros ---- - Portable, no dependencies - Blazingly fast - The most comprehensive feature set - Flexible layout - Batteries included - Vim/Neovim plugin, key bindings and fuzzy auto-completion Table of Contents ----------------- * [Installation](#installation) * [Using Homebrew or Linuxbrew](#using-homebrew-or-linuxbrew) * [Using git](#using-git) * [Using Linux package managers](#using-linux-package-managers) * [Windows](#windows) * [As Vim plugin](#as-vim-plugin) * [Upgrading fzf](#upgrading-fzf) * [Building fzf](#building-fzf) * [Usage](#usage) * [Using the finder](#using-the-finder) * [Layout](#layout) * [Search syntax](#search-syntax) * [Environment variables](#environment-variables) * [Options](#options) * [Demo](#demo) * [Examples](#examples) * [fzf-tmux script](#fzf-tmux-script) * [Key bindings for command line](#key-bindings-for-command-line) * [Fuzzy completion for bash and zsh](#fuzzy-completion-for-bash-and-zsh) * [Files and directories](#files-and-directories) * [Process IDs](#process-ids) * [Host names](#host-names) * [Environment variables / Aliases](#environment-variables--aliases) * [Settings](#settings) * [Supported commands](#supported-commands) * [Vim plugin](#vim-plugin) * [Advanced topics](#advanced-topics) * [Performance](#performance) * [Executing external programs](#executing-external-programs) * [Preview window](#preview-window) * [Tips](#tips) * [Respecting .gitignore](#respecting-gitignore) * [Fish shell](#fish-shell) * [Related projects](#related-projects) * [License](#license) Installation ------------ fzf project consists of the following components: - `fzf` executable - `fzf-tmux` script for launching fzf in a tmux pane - Shell extensions - Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish) - Fuzzy auto-completion (bash, zsh) - Vim/Neovim plugin You can [download fzf executable][bin] alone if you don't need the extra stuff. [bin]: https://github.com/junegunn/fzf-bin/releases ### Using Homebrew or Linuxbrew You can use [Homebrew](http://brew.sh/) or [Linuxbrew](http://linuxbrew.sh/) to install fzf. ```sh brew install fzf # To install useful key bindings and fuzzy completion: $(brew --prefix)/opt/fzf/install ``` fzf is also available [via MacPorts][portfile]: `sudo port install fzf` [portfile]: https://github.com/macports/macports-ports/blob/master/sysutils/fzf/Portfile ### Using git Alternatively, you can "git clone" this repository to any directory and run [install](https://github.com/junegunn/fzf/blob/master/install) script. ```sh git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf ~/.fzf/install ``` ### Using Linux package managers | Distro | Command | | --- | --- | | Alpine Linux | `sudo apk add fzf` | | Arch Linux | `sudo pacman -S fzf` | | Debian | `sudo apt-get install fzf` | | Fedora | `sudo dnf install fzf` | | FreeBSD | `pkg install fzf` | | NixOS | `nix-env -iA nixpkgs.fzf` | | openSUSE | `sudo zypper install fzf` | Shell extensions (key bindings and fuzzy auto-completion) and Vim/Neovim plugin may or may not be enabled by default depending on the package manager. Refer to the package documentation for more information. ### Windows Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also available via [Chocolatey][choco] and [Scoop][scoop]: | Package manager | Command | | --- | --- | | Chocolatey | `choco install fzf` | | Scoop | `scoop install fzf` | [choco]: https://chocolatey.org/packages/fzf [scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/fzf.json Known issues and limitations on Windows can be found on [the wiki page][windows-wiki]. [windows-wiki]: https://github.com/junegunn/fzf/wiki/Windows ### As Vim plugin Once you have fzf installed, you can enable it inside Vim simply by adding the directory to `&runtimepath` in your Vim configuration file. The path may differ depending on the package manager. ```vim " If installed using Homebrew set rtp+=/usr/local/opt/fzf " If installed using git set rtp+=~/.fzf ``` If you use [vim-plug](https://github.com/junegunn/vim-plug), the same can be written as: ```vim " If installed using Homebrew Plug '/usr/local/opt/fzf' " If installed using git Plug '~/.fzf' ``` But instead of separately installing fzf on your system (using Homebrew or "git clone") and enabling it on Vim (adding it to `&runtimepath`), you can use vim-plug to do both. ```vim " PlugInstall and PlugUpdate will clone fzf in ~/.fzf and run the install script Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' } " Both options are optional. You don't have to install fzf in ~/.fzf " and you don't have to run the install script if you use fzf only in Vim. ``` Upgrading fzf ------------- fzf is being actively developed and you might want to upgrade it once in a while. Please follow the instruction below depending on the installation method used. - git: `cd ~/.fzf && git pull && ./install` - brew: `brew update; brew reinstall fzf` - chocolatey: `choco upgrade fzf` - vim-plug: `:PlugUpdate fzf` Building fzf ------------ See [BUILD.md](BUILD.md). Usage ----- fzf will launch interactive finder, read the list from STDIN, and write the selected item to STDOUT. ```sh find * -type f | fzf > selected ``` Without STDIN pipe, fzf will use find command to fetch the list of files excluding hidden ones. (You can override the default command with `FZF_DEFAULT_COMMAND`) ```sh vim $(fzf) ``` #### Using the finder - `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P`) to move cursor up and down - `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit - On multi-select mode (`-m`), `TAB` and `Shift-TAB` to mark multiple items - Emacs style key bindings - Mouse: scroll, click, double-click; shift-click and shift-scroll on multi-select mode #### Layout fzf by default starts in fullscreen mode, but you can make it start below the cursor with `--height` option. ```sh vim $(fzf --height 40%) ``` Also check out `--reverse` and `--layout` options if you prefer "top-down" layout instead of the default "bottom-up" layout. ```sh vim $(fzf --height 40% --reverse) ``` You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by default. For example, ```sh export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border' ``` #### Search syntax Unless otherwise specified, fzf starts in "extended-search mode" where you can type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt !fire` | Token | Match type | Description | | --------- | -------------------------- | ------------------------------------ | | `sbtrkt` | fuzzy-match | Items that match `sbtrkt` | | `'wild` | exact-match (quoted) | Items that include `wild` | | `^music` | prefix-exact-match | Items that start with `music` | | `.mp3$` | suffix-exact-match | Items that end with `.mp3` | | `!fire` | inverse-exact-match | Items that do not include `fire` | | `!^music` | inverse-prefix-exact-match | Items that do not start with `music` | | `!.mp3$` | inverse-suffix-exact-match | Items that do not end with `.mp3` | If you don't prefer fuzzy matching and do not wish to "quote" every word, start fzf with `-e` or `--exact` option. Note that when `--exact` is set, `'`-prefix "unquotes" the term. A single bar character term acts as an OR operator. For example, the following query matches entries that start with `core` and end with either `go`, `rb`, or `py`. ``` ^core go$ | rb$ | py$ ``` #### Environment variables - `FZF_DEFAULT_COMMAND` - Default command to use when input is tty - e.g. `export FZF_DEFAULT_COMMAND='fd --type f'` - `FZF_DEFAULT_OPTS` - Default options - e.g. `export FZF_DEFAULT_OPTS="--layout=reverse --inline-info"` #### Options See the man page (`man fzf`) for the full list of options. #### Demo If you learn by watching videos, check out this screencast by [@samoshkin](https://github.com/samoshkin) to explore `fzf` features. Examples -------- Many useful examples can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/examples). Feel free to add your own as well. `fzf-tmux` script ----------------- [fzf-tmux](bin/fzf-tmux) is a bash script that opens fzf in a tmux pane. ```sh # usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] # (-[udlr]: up/down/left/right) # select git branches in horizontal split below (15 lines) git branch | fzf-tmux -d 15 # select multiple words in vertical split on the left (20% of screen width) cat /usr/share/dict/words | fzf-tmux -l 20% --multi --reverse ``` It will still work even when you're not on tmux, silently ignoring `-[udlr]` options, so you can invariably use `fzf-tmux` in your scripts. Alternatively, you can use `--height HEIGHT[%]` option not to start fzf in fullscreen mode. ```sh fzf --height 40% ``` Key bindings for command-line ----------------------------- The install script will setup the following key bindings for bash, zsh, and fish. - `CTRL-T` - Paste the selected files and directories onto the command-line - Set `FZF_CTRL_T_COMMAND` to override the default command - Set `FZF_CTRL_T_OPTS` to pass additional options - `CTRL-R` - Paste the selected command from history onto the command-line - If you want to see the commands in chronological order, press `CTRL-R` again which toggles sorting by relevance - Set `FZF_CTRL_R_OPTS` to pass additional options - `ALT-C` - cd into the selected directory - Set `FZF_ALT_C_COMMAND` to override the default command - Set `FZF_ALT_C_OPTS` to pass additional options If you're on a tmux session, you can start fzf in a split pane by setting `FZF_TMUX` to 1, and change the height of the pane with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`). If you use vi mode on bash, you need to add `set -o vi` *before* `source ~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi mode. More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Configuring-shell-key-bindings). Fuzzy completion for bash and zsh --------------------------------- #### Files and directories Fuzzy completion for files and directories can be triggered if the word before the cursor ends with the trigger sequence which is by default `**`. - `COMMAND [DIRECTORY/][FUZZY_PATTERN]**` ```sh # Files under current directory # - You can select multiple items with TAB key vim ** # Files under parent directory vim ../** # Files under parent directory that match `fzf` vim ../fzf** # Files under your home directory vim ~/** # Directories under current directory (single-selection) cd ** # Directories under ~/github that match `fzf` cd ~/github/fzf** ``` #### Process IDs Fuzzy completion for PIDs is provided for kill command. In this case, there is no trigger sequence, just press tab key after kill command. ```sh # Can select multiple processes with or keys kill -9 ``` #### Host names For ssh and telnet commands, fuzzy completion for host names is provided. The names are extracted from /etc/hosts and ~/.ssh/config. ```sh ssh ** telnet ** ``` #### Environment variables / Aliases ```sh unset ** export ** unalias ** ``` #### Settings ```sh # Use ~~ as the trigger sequence instead of the default ** export FZF_COMPLETION_TRIGGER='~~' # Options to fzf command export FZF_COMPLETION_OPTS='+c -x' # Use fd (https://github.com/sharkdp/fd) instead of the default find # command for listing path candidates. # - The first argument to the function ($1) is the base path to start traversal # - See the source code (completion.{bash,zsh}) for the details. _fzf_compgen_path() { fd --hidden --follow --exclude ".git" . "$1" } # Use fd to generate the list for directory completion _fzf_compgen_dir() { fd --type d --hidden --follow --exclude ".git" . "$1" } ``` #### Supported commands On bash, fuzzy completion is enabled only for a predefined set of commands (`complete | grep _fzf` to see the list). But you can enable it for other commands as well by using `_fzf_setup_completion` helper function. ```sh # usage: _fzf_setup_completion path|dir COMMANDS... _fzf_setup_completion path ag git kubectl _fzf_setup_completion dir tree ``` Vim plugin ---------- See [README-VIM.md](README-VIM.md). Advanced topics --------------- ### Performance fzf is fast and is [getting even faster][perf]. Performance should not be a problem in most use cases. However, you might want to be aware of the options that affect the performance. - `--ansi` tells fzf to extract and parse ANSI color codes in the input and it makes the initial scanning slower. So it's not recommended that you add it to your `$FZF_DEFAULT_OPTS`. - `--nth` makes fzf slower as fzf has to tokenize each line. - `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each line. - If you absolutely need better performance, you can consider using `--algo=v1` (the default being `v2`) to make fzf use a faster greedy algorithm. However, this algorithm is not guaranteed to find the optimal ordering of the matches and is not recommended. [perf]: https://junegunn.kr/images/fzf-0.17.0.png ### Executing external programs You can set up key bindings for starting external processes without leaving fzf (`execute`, `execute-silent`). ```bash # Press F1 to open the file with less without leaving fzf # Press CTRL-Y to copy the line to clipboard and aborts fzf (requires pbcopy) fzf --bind 'f1:execute(less -f {}),ctrl-y:execute-silent(echo {} | pbcopy)+abort' ``` See *KEY BINDINGS* section of the man page for details. ### Preview window When `--preview` option is set, fzf automatically starts an external process with the current line as the argument and shows the result in the split window. ```bash # {} is replaced to the single-quoted string of the focused line fzf --preview 'cat {}' ``` Since the preview window is updated only after the process is complete, it's important that the command finishes quickly. ```bash # Use head instead of cat so that the command doesn't take too long to finish fzf --preview 'head -100 {}' ``` Preview window supports ANSI colors, so you can use any program that syntax-highlights the content of a file. - Bat: https://github.com/sharkdp/bat - Highlight: http://www.andre-simon.de/doku/highlight/en/highlight.php ```bash fzf --preview 'bat --style=numbers --color=always {} | head -500' ``` You can customize the size, position, and border of the preview window using `--preview-window` option, and the foreground and background color of it with `--color` option. For example, ```bash fzf --height 40% --layout reverse --info inline --border \ --preview 'file {}' --preview-window down:1:noborder \ --color 'fg:#bbccdd,fg+:#ddeeff,bg:#334455,preview-bg:#223344,border:#778899' ``` See the man page (`man fzf`) for the full list of options. For more advanced examples, see [Key bindings for git with fzf][fzf-git] ([code](https://gist.github.com/junegunn/8b572b8d4b5eddd8b85e5f4d40f17236)). [fzf-git]: https://junegunn.kr/2016/07/fzf-git/ ---- Since fzf is a general-purpose text filter rather than a file finder, **it is not a good idea to add `--preview` option to your `$FZF_DEFAULT_OPTS`**. ```sh # ********************* # ** DO NOT DO THIS! ** # ********************* export FZF_DEFAULT_OPTS='--preview "bat --style=numbers --color=always {} | head -500"' # bat doesn't work with any input other than the list of files ps -ef | fzf seq 100 | fzf history | fzf ``` Tips ---- #### Respecting `.gitignore` You can use [fd](https://github.com/sharkdp/fd), [ripgrep](https://github.com/BurntSushi/ripgrep), or [the silver searcher](https://github.com/ggreer/the_silver_searcher) instead of the default find command to traverse the file system while respecting `.gitignore`. ```sh # Feed the output of fd into fzf fd --type f | fzf # Setting fd as the default source for fzf export FZF_DEFAULT_COMMAND='fd --type f' # Now fzf (w/o pipe) will use fd instead of find fzf # To apply the command to CTRL-T as well export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" ``` If you want the command to follow symbolic links, and don't want it to exclude hidden files, use the following command: ```sh export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git' ``` #### Fish shell `CTRL-T` key binding of fish, unlike those of bash and zsh, will use the last token on the command-line as the root directory for the recursive search. For instance, hitting `CTRL-T` at the end of the following command-line ```sh ls /var/ ``` will list all files and directories under `/var/`. When using a custom `FZF_CTRL_T_COMMAND`, use the unexpanded `$dir` variable to make use of this feature. `$dir` defaults to `.` when the last token is not a valid directory. Example: ```sh set -g FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'" ``` Related projects ---------------- https://github.com/junegunn/fzf/wiki/Related-projects [License](LICENSE) ------------------ The MIT License (MIT) Copyright (c) 2019 Junegunn Choi fzf-0.20.0/bin/000077500000000000000000000000001357617647500131535ustar00rootroot00000000000000fzf-0.20.0/bin/fzf-tmux000077500000000000000000000123111357617647500146570ustar00rootroot00000000000000#!/usr/bin/env bash # fzf-tmux: starts fzf in a tmux pane # usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] fail() { >&2 echo "$1" exit 2 } fzf="$(command -v fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" [[ -x "$fzf" ]] || fail 'fzf executable not found' args=() opt="" skip="" swap="" close="" term="" [[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}") [[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}") help() { >&2 echo 'usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] Layout -u [HEIGHT[%]] Split above (up) -d [HEIGHT[%]] Split below (down) -l [WIDTH[%]] Split left -r [WIDTH[%]] Split right (default: -d 50%) ' exit } while [[ $# -gt 0 ]]; do arg="$1" shift [[ -z "$skip" ]] && case "$arg" in -) term=1 ;; --help) help ;; --version) echo "fzf-tmux (with fzf $("$fzf" --version))" exit ;; -w*|-h*|-d*|-u*|-r*|-l*) if [[ "$arg" =~ ^.[lrw] ]]; then opt="-h" if [[ "$arg" =~ ^.l ]]; then opt="$opt -d" swap="; swap-pane -D ; select-pane -L" close="; tmux swap-pane -D" fi else opt="" if [[ "$arg" =~ ^.u ]]; then opt="$opt -d" swap="; swap-pane -D ; select-pane -U" close="; tmux swap-pane -D" fi fi if [[ ${#arg} -gt 2 ]]; then size="${arg:2}" else if [[ "$1" =~ ^[0-9]+%?$ ]]; then size="$1" shift else continue fi fi if [[ "$size" =~ %$ ]]; then size=${size:0:((${#size}-1))} if [[ -n "$swap" ]]; then opt="$opt -p $(( 100 - size ))" else opt="$opt -p $size" fi else if [[ -n "$swap" ]]; then if [[ "$arg" =~ ^.l ]]; then max=$columns else max=$lines fi size=$(( max - size )) [[ $size -lt 0 ]] && size=0 opt="$opt -l $size" else opt="$opt -l $size" fi fi ;; --) # "--" can be used to separate fzf-tmux options from fzf options to # avoid conflicts skip=1 continue ;; *) args+=("$arg") ;; esac [[ -n "$skip" ]] && args+=("$arg") done if [[ -z "$TMUX" || "$opt" =~ ^-h && "$columns" -le 40 || ! "$opt" =~ ^-h && "$lines" -le 15 ]]; then "$fzf" "${args[@]}" exit $? fi # --height option is not allowed args+=("--no-height") # Handle zoomed tmux pane by moving it to a temp window if tmux list-panes -F '#F' | grep -q Z; then zoomed=1 original_window=$(tmux display-message -p "#{window_id}") tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'") tmux swap-pane -t $tmp_window \; select-window -t $tmp_window fi set -e # Clean up named pipes on exit id=$RANDOM argsf="${TMPDIR:-/tmp}/fzf-args-$id" fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id" fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id" fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id" cleanup() { \rm -f $argsf $fifo1 $fifo2 $fifo3 # Restore tmux window options if [[ "${#tmux_win_opts[@]}" -gt 0 ]]; then eval "tmux ${tmux_win_opts[@]}" fi # Remove temp window if we were zoomed if [[ -n "$zoomed" ]]; then tmux display-message -p "#{window_id}" > /dev/null tmux swap-pane -t $original_window \; \ select-window -t $original_window \; \ kill-window -t $tmp_window \; \ resize-pane -Z fi if [ $# -gt 0 ]; then trap - EXIT exit 130 fi } trap 'cleanup 1' SIGUSR1 trap 'cleanup' EXIT envs="env TERM=$TERM " [[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" [[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" mkfifo -m o+w $fifo2 mkfifo -m o+w $fifo3 # Build arguments to fzf opts="" for arg in "${args[@]}"; do arg="${arg//\\/\\\\}" arg="${arg//\"/\\\"}" arg="${arg//\`/\\\`}" arg="${arg//$/\\$}" opts="$opts \"$arg\"" done pppid=$$ echo -n "trap 'kill -SIGUSR1 -$pppid' EXIT SIGINT SIGTERM;" > $argsf close="; trap - EXIT SIGINT SIGTERM $close" tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') ) if [[ -n "$term" ]] || [[ -t 0 ]]; then cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ split-window $opt "$envs bash -c 'cd $(printf %q "$PWD"); exec -a fzf bash $argsf'" $swap \ > /dev/null 2>&1 else mkfifo $fifo1 cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" >> $argsf TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ split-window $opt "$envs bash -c 'exec -a fzf bash $argsf'" $swap \ > /dev/null 2>&1 cat <&0 > $fifo1 & fi cat $fifo2 exit "$(cat $fifo3)" fzf-0.20.0/doc/000077500000000000000000000000001357617647500131505ustar00rootroot00000000000000fzf-0.20.0/doc/fzf.txt000066400000000000000000000327511357617647500145060ustar00rootroot00000000000000fzf.txt fzf Last change: November 23 2019 FZF - TABLE OF CONTENTS *fzf* *fzf-toc* ============================================================================== FZF Vim integration Summary :FZF[!] Configuration Examples fzf#run fzf#wrap Tips fzf inside terminal buffer Starting fzf in Neovim floating window Hide statusline License FZF VIM INTEGRATION *fzf-vim-integration* ============================================================================== SUMMARY *fzf-summary* ============================================================================== The Vim plugin of fzf provides two core functions, and `:FZF` command which is the basic file selector command built on top of them. 1. `fzf#run([spec dict])` - Starts fzf inside Vim with the given spec - `:call fzf#run({'source': 'ls'})` 2. `fzf#wrap([spec dict]) -> (dict)` - Takes a spec for `fzf#run` and returns an extended version of it with additional options for addressing global preferences (`g:fzf_xxx`) - `:echo fzf#wrap({'source': 'ls'})` - We usually wrap a spec with `fzf#wrap` before passing it to `fzf#run` - `:call fzf#run(fzf#wrap({'source': 'ls'}))` 3. `:FZF [fzf_options string] [path string]` - Basic fuzzy file selector - A reference implementation for those who don't want to write VimScript to implement custom commands - If you're looking for more such commands, check out {fzf.vim}{1} project. The most important of all is `fzf#run`, but it would be easier to understand the whole if we start off with `:FZF` command. {1} https://github.com/junegunn/fzf.vim :FZF[!] ============================================================================== *:FZF* > " Look for files under current directory :FZF " Look for files under your home directory :FZF ~ " With fzf command-line options :FZF --reverse --info=inline /tmp " Bang version starts fzf in fullscreen mode :FZF! < Similarly to {ctrlp.vim}{2}, use enter key, CTRL-T, CTRL-X or CTRL-V to open selected files in the current window, in new tabs, in horizontal splits, or in vertical splits respectively. Note that the environment variables `FZF_DEFAULT_COMMAND` and `FZF_DEFAULT_OPTS` also apply here. {2} https://github.com/kien/ctrlp.vim < Configuration >_____________________________________________________________~ *fzf-configuration* *g:fzf_action* *g:fzf_layout* *g:fzf_colors* *g:fzf_history_dir* - `g:fzf_action` - Customizable extra key bindings for opening selected files in different ways - `g:fzf_layout` - Determines the size and position of fzf window - `g:fzf_colors` - Customizes fzf colors to match the current color scheme - `g:fzf_history_dir` - Enables history feature Examples~ *fzf-examples* > " This is the default extra key bindings let g:fzf_action = { \ 'ctrl-t': 'tab split', \ 'ctrl-x': 'split', \ 'ctrl-v': 'vsplit' } " An action can be a reference to a function that processes selected lines function! s:build_quickfix_list(lines) call setqflist(map(copy(a:lines), '{ "filename": v:val }')) copen cc endfunction let g:fzf_action = { \ 'ctrl-q': function('s:build_quickfix_list'), \ 'ctrl-t': 'tab split', \ 'ctrl-x': 'split', \ 'ctrl-v': 'vsplit' } " Default fzf layout " - down / up / left / right let g:fzf_layout = { 'down': '~40%' } " You can set up fzf window using a Vim command (Neovim or latest Vim 8 required) let g:fzf_layout = { 'window': 'enew' } let g:fzf_layout = { 'window': '-tabnew' } let g:fzf_layout = { 'window': '10new' } " Customize fzf colors to match your color scheme " - fzf#wrap translates this to a set of `--color` options let g:fzf_colors = \ { 'fg': ['fg', 'Normal'], \ 'bg': ['bg', 'Normal'], \ 'hl': ['fg', 'Comment'], \ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'], \ 'bg+': ['bg', 'CursorLine', 'CursorColumn'], \ 'hl+': ['fg', 'Statement'], \ 'info': ['fg', 'PreProc'], \ 'border': ['fg', 'Ignore'], \ 'prompt': ['fg', 'Conditional'], \ 'pointer': ['fg', 'Exception'], \ 'marker': ['fg', 'Keyword'], \ 'spinner': ['fg', 'Label'], \ 'header': ['fg', 'Comment'] } " Enable per-command history " - History files will be stored in the specified directory " - When set, CTRL-N and CTRL-P will be bound to 'next-history' and " 'previous-history' instead of 'down' and 'up'. let g:fzf_history_dir = '~/.local/share/fzf-history' < FZF#RUN ============================================================================== *fzf#run* `fzf#run()` function is the core of Vim integration. It takes a single dictionary argument, a spec, and starts fzf process accordingly. At the very least, specify `sink` option to tell what it should do with the selected entry. > call fzf#run({'sink': 'e'}) < We haven't specified the `source`, so this is equivalent to starting fzf on command line without standard input pipe; fzf will use find command (or `$FZF_DEFAULT_COMMAND` if defined) to list the files under the current directory. When you select one, it will open it with the sink, `:e` command. If you want to open it in a new tab, you can pass `:tabedit` command instead as the sink. > call fzf#run({'sink': 'tabedit'}) < Instead of using the default find command, you can use any shell command as the source. The following example will list the files managed by git. It's equivalent to running `git ls-files | fzf` on shell. > call fzf#run({'source': 'git ls-files', 'sink': 'e'}) < fzf options can be specified as `options` entry in spec dictionary. > call fzf#run({'sink': 'tabedit', 'options': '--multi --reverse'}) < You can also pass a layout option if you don't want fzf window to take up the entire screen. > " up / down / left / right / window are allowed call fzf#run({'source': 'git ls-files', 'sink': 'e', 'left': '40%'}) call fzf#run({'source': 'git ls-files', 'sink': 'e', 'window': '30vnew'}) < `source` doesn't have to be an external shell command, you can pass a Vim array as the source. In the next example, we pass the names of color schemes as the source to implement a color scheme selector. > call fzf#run({'source': map(split(globpath(&rtp, 'colors/*.vim')), \ 'fnamemodify(v:val, ":t:r")'), \ 'sink': 'colo', 'left': '25%'}) < The following table summarizes the available options. ---------------------------+---------------+---------------------------------------------------------------------- Option name | Type | Description ~ ---------------------------+---------------+---------------------------------------------------------------------- `source` | string | External command to generate input to fzf (e.g. `find .` ) `source` | list | Vim list as input to fzf `sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` ) `sink` | funcref | Reference to function to process each selected item `sink*` | funcref | Similar to `sink` , but takes the list of output lines at once `options` | string/list | Options to fzf `dir` | string | Working directory `up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` ) `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` ) ---------------------------+---------------+---------------------------------------------------------------------- `options` entry can be either a string or a list. For simple cases, string should suffice, but prefer to use list type to avoid escaping issues. > call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'}) call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']}) < FZF#WRAP ============================================================================== *fzf#wrap* We have seen that several aspects of `:FZF` command can be configured with a set of global option variables; different ways to open files (`g:fzf_action`), window position and size (`g:fzf_layout`), color palette (`g:fzf_colors`), etc. So how can we make our custom `fzf#run` calls also respect those variables? Simply by "wrapping" the spec dictionary with `fzf#wrap` before passing it to `fzf#run`. - `fzf#wrap([name string], [spec dict], [fullscreen bool]) -> (dict)` - All arguments are optional. Usually we only need to pass a spec dictionary. - `name` is for managing history files. It is ignored if `g:fzf_history_dir` is not defined. - `fullscreen` can be either `0` or `1` (default: 0). `fzf#wrap` takes a spec and returns an extended version of it (also a dictionary) with additional options for addressing global preferences. You can examine the return value of it like so: > echo fzf#wrap({'source': 'ls'}) < After we "wrap" our spec, we pass it to `fzf#run`. > call fzf#run(fzf#wrap({'source': 'ls'})) < Now it supports CTRL-T, CTRL-V, and CTRL-X key bindings and it opens fzf window according to `g:fzf_layout` setting. To make it easier to use, let's define `LS` command. > command! LS call fzf#run(fzf#wrap({'source': 'ls'})) < Type `:LS` and see how it works. We would like to make `:LS!` (bang version) open fzf in fullscreen, just like `:FZF!`. Add `-bang` to command definition, and use value to set the last `fullscreen` argument of `fzf#wrap` (see :help ). > " On :LS!, evaluates to '!', and '!0' becomes 1 command! -bang LS call fzf#run(fzf#wrap({'source': 'ls'}, 0)) < Our `:LS` command will be much more useful if we can pass a directory argument to it, so that something like `:LS /tmp` is possible. > command! -bang -complete=dir -nargs=* LS \ call fzf#run(fzf#wrap({'source': 'ls', 'dir': }, 0)) < Lastly, if you have enabled `g:fzf_history_dir`, you might want to assign a unique name to our command and pass it as the first argument to `fzf#wrap`. > " The query history for this command will be stored as 'ls' inside g:fzf_history_dir. " The name is ignored if g:fzf_history_dir is not defined. command! -bang -complete=dir -nargs=* LS \ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': }, 0)) < TIPS *fzf-tips* ============================================================================== < fzf inside terminal buffer >________________________________________________~ *fzf-inside-terminal-buffer* The latest versions of Vim and Neovim include builtin terminal emulator (`:terminal`) and fzf will start in a terminal buffer in the following cases: - On Neovim - On GVim - On Terminal Vim with a non-default layout - `call fzf#run({'left': '30%'})` or `let g:fzf_layout = {'left': '30%'}` Starting fzf in Neovim floating window~ *fzf-starting-fzf-in-neovim-floating-window* > " Using floating windows of Neovim to start fzf if has('nvim') let $FZF_DEFAULT_OPTS .= ' --border --margin=0,2' function! FloatingFZF() let width = float2nr(&columns * 0.9) let height = float2nr(&lines * 0.6) let opts = { 'relative': 'editor', \ 'row': (&lines - height) / 2, \ 'col': (&columns - width) / 2, \ 'width': width, \ 'height': height } let win = nvim_open_win(nvim_create_buf(v:false, v:true), v:true, opts) call setwinvar(win, '&winhighlight', 'NormalFloat:Normal') endfunction let g:fzf_layout = { 'window': 'call FloatingFZF()' } endif < Hide statusline~ *fzf-hide-statusline* When fzf starts in a terminal buffer, the file type of the buffer is set to `fzf`. So you can set up `FileType fzf` autocmd to customize the settings of the window. For example, if you use the default layout (`{'down': '~40%'}`) on Neovim, you might want to temporarily disable the statusline for a cleaner look. > if has('nvim') && !exists('g:fzf_layout') autocmd! FileType fzf autocmd FileType fzf set laststatus=0 noshowmode noruler \| autocmd BufLeave set laststatus=2 showmode ruler endif < LICENSE *fzf-license* ============================================================================== The MIT License (MIT) Copyright (c) 2019 Junegunn Choi ============================================================================== vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap: fzf-0.20.0/go.mod000066400000000000000000000016361357617647500135170ustar00rootroot00000000000000module github.com/junegunn/fzf require ( github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635 // indirect github.com/gdamore/tcell v0.0.0-20170915061752-0a0db94084df github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/lucasb-eyer/go-colorful v0.0.0-20170223221042-c900de9dbbc7 // indirect github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c github.com/mattn/go-shellwords v1.0.3 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect golang.org/x/crypto v0.0.0-20170728183002-558b6879de74 golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 // indirect golang.org/x/text v0.0.0-20170530162606-4ee4af566555 // indirect ) go 1.13 fzf-0.20.0/go.sum000066400000000000000000000054631357617647500135460ustar00rootroot00000000000000github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635 h1:hheUEMzaOie/wKeIc1WPa7CDVuIO5hqQxjS+dwTQEnI= github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635/go.mod h1:yrQYJKKDTrHmbYxI7CYi+/hbdiDT2m4Hj+t0ikCjsrQ= github.com/gdamore/tcell v0.0.0-20170915061752-0a0db94084df h1:tLS1QD2puA1USLvkEnGfOt+Zp2ijtNIK3z2YFaIZZR4= github.com/gdamore/tcell v0.0.0-20170915061752-0a0db94084df/go.mod h1:tqyG50u7+Ctv1w5VX67kLzKcj9YXR/JSBZQq/+mLl1A= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/lucasb-eyer/go-colorful v0.0.0-20170223221042-c900de9dbbc7 h1:G52I+Gk/wPD4HKvKT0Vxxp9OUPxqKs3OK6rffSPtNkA= github.com/lucasb-eyer/go-colorful v0.0.0-20170223221042-c900de9dbbc7/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4= github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c h1:3nKFouDdpgGUV/uerJcYWH45ZbJzX0SiVWfTgmUeTzc= github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c h1:eFzthqtg3W6Pihj3DMTXLAF4f+ge5r5Ie5g6HLIZAF0= github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-shellwords v1.0.3 h1:K/VxK7SZ+cvuPgFSLKi5QPI9Vr/ipOf4C1gN+ntueUk= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= golang.org/x/crypto v0.0.0-20170728183002-558b6879de74 h1:/0jf0Cx3u07Ma4EzUA6NIGuvk9hb3Br6i9V8atthkwk= golang.org/x/crypto v0.0.0-20170728183002-558b6879de74/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug= golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170530162606-4ee4af566555 h1:pjwO9HxObpgZBurDvTLFbSinaf3+cKpTAjVfiGaHwrI= golang.org/x/text v0.0.0-20170530162606-4ee4af566555/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= fzf-0.20.0/install000077500000000000000000000250401357617647500140000ustar00rootroot00000000000000#!/usr/bin/env bash set -u version=0.20.0 auto_completion= key_bindings= update_config=2 binary_arch= shells="bash zsh fish" prefix='~/.fzf' prefix_expand=~/.fzf fish_dir=${XDG_CONFIG_HOME:-$HOME/.config}/fish help() { cat << EOF usage: $0 [OPTIONS] --help Show this message --bin Download fzf binary only; Do not generate ~/.fzf.{bash,zsh} --all Download fzf binary and update configuration files to enable key bindings and fuzzy completion --xdg Generate files under \$XDG_CONFIG_HOME/fzf --[no-]key-bindings Enable/disable key bindings (CTRL-T, CTRL-R, ALT-C) --[no-]completion Enable/disable fuzzy completion (bash & zsh) --[no-]update-rc Whether or not to update shell configuration files --no-bash Do not set up bash configuration --no-zsh Do not set up zsh configuration --no-fish Do not set up fish configuration --32 Download 32-bit binary --64 Download 64-bit binary EOF } for opt in "$@"; do case $opt in --help) help exit 0 ;; --all) auto_completion=1 key_bindings=1 update_config=1 ;; --xdg) prefix='"${XDG_CONFIG_HOME:-$HOME/.config}"/fzf/fzf' prefix_expand=${XDG_CONFIG_HOME:-$HOME/.config}/fzf/fzf mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/fzf" ;; --key-bindings) key_bindings=1 ;; --no-key-bindings) key_bindings=0 ;; --completion) auto_completion=1 ;; --no-completion) auto_completion=0 ;; --update-rc) update_config=1 ;; --no-update-rc) update_config=0 ;; --32) binary_arch=386 ;; --64) binary_arch=amd64 ;; --bin) ;; --no-bash) shells=${shells/bash/} ;; --no-zsh) shells=${shells/zsh/} ;; --no-fish) shells=${shells/fish/} ;; *) echo "unknown option: $opt" help exit 1 ;; esac done cd "$(dirname "${BASH_SOURCE[0]}")" fzf_base=$(pwd) fzf_base_esc=$(printf %q "$fzf_base") ask() { while true; do read -p "$1 ([y]/n) " -r REPLY=${REPLY:-"y"} if [[ $REPLY =~ ^[Yy]$ ]]; then return 1 elif [[ $REPLY =~ ^[Nn]$ ]]; then return 0 fi done } check_binary() { echo -n " - Checking fzf executable ... " local output output=$("$fzf_base"/bin/fzf --version 2>&1) if [ $? -ne 0 ]; then echo "Error: $output" binary_error="Invalid binary" else output=${output/ */} if [ "$version" != "$output" ]; then echo "$output != $version" binary_error="Invalid version" else echo "$output" binary_error="" return 0 fi fi rm -f "$fzf_base"/bin/fzf return 1 } link_fzf_in_path() { if which_fzf="$(command -v fzf)"; then echo " - Found in \$PATH" echo " - Creating symlink: bin/fzf -> $which_fzf" (cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf) check_binary && return fi return 1 } try_curl() { command -v curl > /dev/null && if [[ $1 =~ tgz$ ]]; then curl -fL $1 | tar -xzf - else local temp=${TMPDIR:-/tmp}/fzf.zip curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp" fi } try_wget() { command -v wget > /dev/null && if [[ $1 =~ tgz$ ]]; then wget -O - $1 | tar -xzf - else local temp=${TMPDIR:-/tmp}/fzf.zip wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp" fi } download() { echo "Downloading bin/fzf ..." if [[ ! "$version" =~ alpha ]]; then if [ -x "$fzf_base"/bin/fzf ]; then echo " - Already exists" check_binary && return fi link_fzf_in_path && return fi mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin if [ $? -ne 0 ]; then binary_error="Failed to create bin directory" return fi local url [[ "$version" =~ alpha ]] && url=https://github.com/junegunn/fzf-bin/releases/download/alpha/${1} || url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1} set -o pipefail if ! (try_curl $url || try_wget $url); then set +o pipefail binary_error="Failed to download with curl and wget" return fi set +o pipefail if [ ! -f fzf ]; then binary_error="Failed to download ${1}" return fi chmod +x fzf && check_binary } # Try to download binary executable archi=$(uname -sm) binary_available=1 binary_error="" case "$archi" in Darwin\ *64) download fzf-$version-darwin_${binary_arch:-amd64}.tgz ;; Darwin\ *86) download fzf-$version-darwin_${binary_arch:-386}.tgz ;; Linux\ armv5*) download fzf-$version-linux_${binary_arch:-arm5}.tgz ;; Linux\ armv6*) download fzf-$version-linux_${binary_arch:-arm6}.tgz ;; Linux\ armv7*) download fzf-$version-linux_${binary_arch:-arm7}.tgz ;; Linux\ armv8*) download fzf-$version-linux_${binary_arch:-arm8}.tgz ;; Linux\ aarch64*) download fzf-$version-linux_${binary_arch:-arm8}.tgz ;; Linux\ *64) download fzf-$version-linux_${binary_arch:-amd64}.tgz ;; Linux\ *86) download fzf-$version-linux_${binary_arch:-386}.tgz ;; FreeBSD\ *64) download fzf-$version-freebsd_${binary_arch:-amd64}.tgz ;; FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386}.tgz ;; OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64}.tgz ;; OpenBSD\ *86) download fzf-$version-openbsd_${binary_arch:-386}.tgz ;; CYGWIN*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;; MINGW*\ *86) download fzf-$version-windows_${binary_arch:-386}.zip ;; MINGW*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;; MSYS*\ *86) download fzf-$version-windows_${binary_arch:-386}.zip ;; MSYS*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;; Windows*\ *86) download fzf-$version-windows_${binary_arch:-386}.zip ;; Windows*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;; *) binary_available=0 binary_error=1 ;; esac cd "$fzf_base" if [ -n "$binary_error" ]; then if [ $binary_available -eq 0 ]; then echo "No prebuilt binary for $archi ..." else echo " - $binary_error !!!" fi if command -v go > /dev/null; then echo -n "Building binary (go get -u github.com/junegunn/fzf) ... " if [ -z "${GOPATH-}" ]; then export GOPATH="${TMPDIR:-/tmp}/fzf-gopath" mkdir -p "$GOPATH" fi if go get -u github.com/junegunn/fzf; then echo "OK" cp "$GOPATH/bin/fzf" "$fzf_base/bin/" else echo "Failed to build binary. Installation failed." exit 1 fi else echo "go executable not found. Installation failed." exit 1 fi fi [[ "$*" =~ "--bin" ]] && exit 0 for s in $shells; do if ! command -v "$s" > /dev/null; then shells=${shells/$s/} fi done if [[ ${#shells} -lt 3 ]]; then echo "No shell configuration to be updated." exit 0 fi # Auto-completion if [ -z "$auto_completion" ]; then ask "Do you want to enable fuzzy auto-completion?" auto_completion=$? fi # Key-bindings if [ -z "$key_bindings" ]; then ask "Do you want to enable key bindings?" key_bindings=$? fi echo for shell in $shells; do [[ "$shell" = fish ]] && continue src=${prefix_expand}.${shell} echo -n "Generate $src ... " fzf_completion="[[ \$- == *i* ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null" if [ $auto_completion -eq 0 ]; then fzf_completion="# $fzf_completion" fi fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\"" if [ $key_bindings -eq 0 ]; then fzf_key_bindings="# $fzf_key_bindings" fi cat > "$src" << EOF # Setup fzf # --------- if [[ ! "\$PATH" == *$fzf_base_esc/bin* ]]; then export PATH="\${PATH:+\${PATH}:}$fzf_base/bin" fi # Auto-completion # --------------- $fzf_completion # Key bindings # ------------ $fzf_key_bindings EOF echo "OK" done # fish if [[ "$shells" =~ fish ]]; then echo -n "Update fish_user_paths ... " fish << EOF echo \$fish_user_paths | \grep "$fzf_base"/bin > /dev/null or set --universal fish_user_paths \$fish_user_paths "$fzf_base"/bin EOF [ $? -eq 0 ] && echo "OK" || echo "Failed" mkdir -p "${fish_dir}/functions" if [ -e "${fish_dir}/functions/fzf.fish" ]; then echo -n "Remove unnecessary ${fish_dir}/functions/fzf.fish ... " rm -f "${fish_dir}/functions/fzf.fish" && echo "OK" || echo "Failed" fi fish_binding="${fish_dir}/functions/fzf_key_bindings.fish" if [ $key_bindings -ne 0 ]; then echo -n "Symlink $fish_binding ... " ln -sf "$fzf_base/shell/key-bindings.fish" \ "$fish_binding" && echo "OK" || echo "Failed" else echo -n "Removing $fish_binding ... " rm -f "$fish_binding" echo "OK" fi fi append_line() { set -e local update line file pat lno update="$1" line="$2" file="$3" pat="${4:-}" lno="" echo "Update $file:" echo " - $line" if [ -f "$file" ]; then if [ $# -lt 4 ]; then lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ') else lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ') fi fi if [ -n "$lno" ]; then echo " - Already exists: line #$lno" else if [ $update -eq 1 ]; then [ -f "$file" ] && echo >> "$file" echo "$line" >> "$file" echo " + Added" else echo " ~ Skipped" fi fi echo set +e } create_file() { local file="$1" shift echo "Create $file:" for line in "$@"; do echo " $line" echo "$line" >> "$file" done echo } if [ $update_config -eq 2 ]; then echo ask "Do you want to update your shell configuration files?" update_config=$? fi echo for shell in $shells; do [[ "$shell" = fish ]] && continue [ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc append_line $update_config "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" "$dest" "${prefix}.${shell}" done if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then bind_file="${fish_dir}/functions/fish_user_key_bindings.fish" if [ ! -e "$bind_file" ]; then create_file "$bind_file" \ 'function fish_user_key_bindings' \ ' fzf_key_bindings' \ 'end' else append_line $update_config "fzf_key_bindings" "$bind_file" fi fi if [ $update_config -eq 1 ]; then echo 'Finished. Restart your shell or reload config file.' [[ "$shells" =~ bash ]] && echo ' source ~/.bashrc # bash' [[ "$shells" =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh" [[ "$shells" =~ fish ]] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish' echo echo 'Use uninstall script to remove fzf.' echo fi echo 'For more information, see: https://github.com/junegunn/fzf' fzf-0.20.0/main.go000066400000000000000000000002001357617647500136460ustar00rootroot00000000000000package main import "github.com/junegunn/fzf/src" var revision string func main() { fzf.Run(fzf.ParseOptions(), revision) } fzf-0.20.0/man/000077500000000000000000000000001357617647500131565ustar00rootroot00000000000000fzf-0.20.0/man/man1/000077500000000000000000000000001357617647500140125ustar00rootroot00000000000000fzf-0.20.0/man/man1/fzf-tmux.1000066400000000000000000000034211357617647500156540ustar00rootroot00000000000000.ig The MIT License (MIT) Copyright (c) 2019 Junegunn Choi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. .TH fzf-tmux 1 "Dec 2019" "fzf 0.20.0" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane .SH SYNOPSIS .B fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] .SH DESCRIPTION fzf-tmux is a wrapper script for fzf that opens fzf in a tmux split pane. It is designed to work just like fzf except that it does not take up the whole screen. You can safely use fzf-tmux instead of fzf in your scripts as the extra options will be silently ignored if you're not on tmux. .SH OPTIONS .SS Layout (default: \fB-d 50%\fR) .TP .B "-u [height[%]]" Split above (up) .TP .B "-d [height[%]]" Split below (down) .TP .B "-l [width[%]]" Split left .TP .B "-r [width[%]]" Split right fzf-0.20.0/man/man1/fzf.1000066400000000000000000000554421357617647500146730ustar00rootroot00000000000000.ig The MIT License (MIT) Copyright (c) 2019 Junegunn Choi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. .TH fzf 1 "Dec 2019" "fzf 0.20.0" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder .SH SYNOPSIS fzf [options] .SH DESCRIPTION fzf is a general-purpose command-line fuzzy finder. .SH OPTIONS .SS Search mode .TP .B "-x, --extended" Extended-search mode. Since 0.10.9, this is enabled by default. You can disable it with \fB+x\fR or \fB--no-extended\fR. .TP .B "-e, --exact" Enable exact-match .TP .B "-i" Case-insensitive match (default: smart-case match) .TP .B "+i" Case-sensitive match .TP .B "--literal" Do not normalize latin script letters for matching. .TP .BI "--algo=" TYPE Fuzzy matching algorithm (default: v2) .br .BR v2 " Optimal scoring algorithm (quality)" .br .BR v1 " Faster but not guaranteed to find the optimal result (performance)" .br .TP .BI "-n, --nth=" "N[,..]" Comma-separated list of field index expressions for limiting search scope. See \fBFIELD INDEX EXPRESSION\fR for the details. .TP .BI "--with-nth=" "N[,..]" Transform the presentation of each line using field index expressions .TP .BI "-d, --delimiter=" "STR" Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style) .TP .BI "--phony" Do not perform search. With this option, fzf becomes a simple selector interface rather than a "fuzzy finder". .SS Search result .TP .B "+s, --no-sort" Do not sort the result .TP .B "--tac" Reverse the order of the input .RS e.g. \fBhistory | fzf --tac --no-sort\fR .RE .TP .BI "--tiebreak=" "CRI[,..]" Comma-separated list of sort criteria to apply when the scores are tied. .br .br .BR length " Prefers line with shorter length" .br .BR begin " Prefers line with matched substring closer to the beginning" .br .BR end " Prefers line with matched substring closer to the end" .br .BR index " Prefers line that appeared earlier in the input stream" .br .br - Each criterion should appear only once in the list .br - \fBindex\fR is only allowed at the end of the list .br - \fBindex\fR is implicitly appended to the list when not specified .br - Default is \fBlength\fR (or equivalently \fBlength\fR,index) .br - If \fBend\fR is found in the list, fzf will scan each line backwards .SS Interface .TP .B "-m, --multi" Enable multi-select with tab/shift-tab. It optionally takes an integer argument which denotes the maximum number of items that can be selected. .TP .B "+m, --no-multi" Disable multi-select .TP .B "--no-mouse" Disable mouse .TP .BI "--bind=" "KEYBINDS" Comma-separated list of custom key bindings. See \fBKEY/EVENT BINDINGS\fR for the details. .TP .B "--cycle" Enable cyclic scroll .TP .B "--no-hscroll" Disable horizontal scroll .TP .BI "--hscroll-off=" "COL" Number of screen columns to keep to the right of the highlighted substring (default: 10). Setting it to a large value will cause the text to be positioned on the center of the screen. .TP .B "--filepath-word" Make word-wise movements and actions respect path separators. The following actions are affected: \fBbackward-kill-word\fR .br \fBbackward-word\fR .br \fBforward-word\fR .br \fBkill-word\fR .TP .BI "--jump-labels=" "CHARS" Label characters for \fBjump\fR and \fBjump-accept\fR .SS Layout .TP .BI "--height=" "HEIGHT[%]" Display fzf window below the cursor with the given height instead of using the full screen. .TP .BI "--min-height=" "HEIGHT" Minimum height when \fB--height\fR is given in percent (default: 10). Ignored when \fB--height\fR is not specified. .TP .BI "--layout=" "LAYOUT" Choose the layout (default: default) .br .BR default " Display from the bottom of the screen" .br .BR reverse " Display from the top of the screen" .br .BR reverse-list " Display from the top of the screen, prompt at the bottom" .br .TP .B "--reverse" A synonym for \fB--layout=reverse\fB .TP .B "--border" Draw border above and below the finder .TP .B "--no-unicode" Use ASCII characters instead of Unicode box drawing characters to draw border .TP .BI "--margin=" MARGIN Comma-separated expression for margins around the finder. .br .br .RS .BR TRBL " Same margin for top, right, bottom, and left" .br .BR TB,RL " Vertical, horizontal margin" .br .BR T,RL,B " Top, horizontal, bottom margin" .br .BR T,R,B,L " Top, right, bottom, left margin" .br .br Each part can be given in absolute number or in percentage relative to the terminal size with \fB%\fR suffix. .br .br e.g. \fBfzf --margin 10% fzf --margin 1,5%\fR .RE .TP .BI "--info=" "STYLE" Determines the display style of finder info. .br .BR default " Display on the next line to the prompt" .br .BR inline " Display on the same line" .br .BR hidden " Do not display finder info" .br .TP .B "--no-info" A synonym for \fB--info=hidden\fB .TP .BI "--prompt=" "STR" Input prompt (default: '> ') .TP .BI "--header=" "STR" The given string will be printed as the sticky header. The lines are displayed in the given order from top to bottom regardless of \fB--layout\fR option, and are not affected by \fB--with-nth\fR. ANSI color codes are processed even when \fB--ansi\fR is not set. .TP .BI "--header-lines=" "N" The first N lines of the input are treated as the sticky header. When \fB--with-nth\fR is set, the lines are transformed just like the other lines that follow. .SS Display .TP .B "--ansi" Enable processing of ANSI color codes .TP .BI "--tabstop=" SPACES Number of spaces for a tab character (default: 8) .TP .BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]" Color configuration. The name of the base color scheme is followed by custom color mappings. Ansi color code of -1 denotes terminal default foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR format. .RS .B BASE SCHEME: (default: dark on 256-color terminal, otherwise 16) \fBdark \fRColor scheme for dark 256-color terminal \fBlight \fRColor scheme for light 256-color terminal \fB16 \fRColor scheme for 16-color terminal \fBbw \fRNo colors (equivalent to \fB--no-color\fR) .B COLOR: \fBfg \fRText \fBbg \fRBackground \fBpreview-fg \fRPreview window text \fBpreview-bg \fRPreview window background \fBhl \fRHighlighted substrings \fBfg+ \fRText (current line) \fBbg+ \fRBackground (current line) \fBgutter \fRGutter on the left (defaults to \fBbg+\fR) \fBhl+ \fRHighlighted substrings (current line) \fBinfo \fRInfo \fBborder \fRBorder of the preview window and horizontal separators (\fB--border\fR) \fBprompt \fRPrompt \fBpointer \fRPointer to the current line \fBmarker \fRMulti-select marker \fBspinner \fRStreaming input indicator \fBheader \fRHeader .B EXAMPLES: \fB# Seoul256 theme with 8-bit colors # (https://github.com/junegunn/seoul256.vim) fzf --color='bg:237,bg+:236,info:143,border:240,spinner:108' \\ --color='hl:65,fg:252,header:65,fg+:252' \\ --color='pointer:161,marker:168,prompt:110,hl+:108' # Seoul256 theme with 24-bit colors fzf --color='bg:#4B4B4B,bg+:#3F3F3F,info:#BDBB72,border:#6B6B6B,spinner:#98BC99' \\ --color='hl:#719872,fg:#D9D9D9,header:#719872,fg+:#D9D9D9' \\ --color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR .RE .TP .B "--no-bold" Do not use bold text .TP .B "--black" Use black background .SS History .TP .BI "--history=" "HISTORY_FILE" Load search history from the specified file and update the file on completion. When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to \fBnext-history\fR and \fBprevious-history\fR. .TP .BI "--history-size=" "N" Maximum number of entries in the history file (default: 1000). The file is automatically truncated when the number of the lines exceeds the value. .SS Preview .TP .BI "--preview=" "COMMAND" Execute the given command for the current line and display the result on the preview window. \fB{}\fR in the command is the placeholder that is replaced to the single-quoted string of the current line. To transform the replacement string, specify field index expressions between the braces (See \fBFIELD INDEX EXPRESSION\fR for the details). .RS e.g. \fBfzf --preview='head -$LINES {}' ls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR fzf exports \fB$FZF_PREVIEW_LINES\fR and \fB$FZF_PREVIEW_COLUMNS\fR so that they represent the exact size of the preview window. (It also overrides \fB$LINES\fR and \fB$COLUMNS\fR with the same values but they can be reset by the default shell, so prefer to refer to the ones with \fBFZF_PREVIEW_\fR prefix.) A placeholder expression starting with \fB+\fR flag will be replaced to the space-separated list of the selected lines (or the current line if no selection was made) individually quoted. e.g. \fBfzf --multi --preview='head -10 {+}' git log --oneline | fzf --multi --preview 'git show {+1}'\fR When using a field index expression, leading and trailing whitespace is stripped from the replacement string. To preserve the whitespace, use the \fBs\fR flag. Also, \fB{q}\fR is replaced to the current query string, and \fB{n}\fR is replaced to zero-based ordinal index of the line. Use \fB{+n}\fR if you want all index numbers when multiple lines are selected. A placeholder expression with \fBf\fR flag is replaced to the path of a temporary file that holds the evaluated list. This is useful when you multi-select a large number of items and the length of the evaluated string may exceed \fBARG_MAX\fR. e.g. \fB# Press CTRL-A to select 100K items and see the sum of all the numbers. # This won't work properly without 'f' flag due to ARG_MAX limit. seq 100000 | fzf --multi --bind ctrl-a:select-all \\ --preview "awk '{sum+=\$1} END {print sum}' {+f}"\fR Note that you can escape a placeholder pattern by prepending a backslash. Preview window will be updated even when there is no match for the current query if any of the placeholder expressions evaluates to a non-empty string. .RE .TP .BI "--preview-window=" "[POSITION][:SIZE[%]][:noborder][:wrap][:hidden]" Determines the layout of the preview window. If the argument contains \fB:hidden\fR, the preview window will be hidden by default until \fBtoggle-preview\fR action is triggered. Long lines are truncated by default. Line wrap can be enabled with \fB:wrap\fR flag. If size is given as 0, preview window will not be visible, but fzf will still execute the command in the background. .RS .B POSITION: (default: right) \fBup \fBdown \fBleft \fBright .RE .RS e.g. \fBfzf --preview="head {}" --preview-window=up:30% fzf --preview="file {}" --preview-window=down:1\fR .RE .SS Scripting .TP .BI "-q, --query=" "STR" Start the finder with the given query .TP .B "-1, --select-1" Automatically select the only match .TP .B "-0, --exit-0" Exit immediately when there's no match .TP .BI "-f, --filter=" "STR" Filter mode. Do not start interactive finder. When used with \fB--no-sort\fR, fzf becomes a fuzzy-version of grep. .TP .B "--print-query" Print query as the first line .TP .BI "--expect=" "KEY[,..]" Comma-separated list of keys that can be used to complete fzf in addition to the default enter key. When this option is set, fzf will print the name of the key pressed as the first line of its output (or as the second line if \fB--print-query\fR is also used). The line will be empty if fzf is completed with the default enter key. If \fB--expect\fR option is specified multiple times, fzf will expect the union of the keys. \fB--no-expect\fR will clear the list. .RS e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR .RE .TP .B "--read0" Read input delimited by ASCII NUL characters instead of newline characters .TP .B "--print0" Print output delimited by ASCII NUL characters instead of newline characters .TP .B "--no-clear" Do not clear finder interface on exit. If fzf was started in full screen mode, it will not switch back to the original screen, so you'll have to manually run \fBtput rmcup\fR to return. This option can be used to avoid flickering of the screen when your application needs to start fzf multiple times in order. .TP .B "--sync" Synchronous search for multi-staged filtering. If specified, fzf will launch ncurses finder only after the input stream is complete. .RS e.g. \fBfzf --multi | fzf --sync\fR .RE .TP .B "--version" Display version information and exit .TP Note that most options have the opposite versions with \fB--no-\fR prefix. .SH ENVIRONMENT VARIABLES .TP .B FZF_DEFAULT_COMMAND Default command to use when input is tty. On *nix systems, fzf runs the command with \fBsh -c\fR, so make sure that it's POSIX-compliant. .TP .B FZF_DEFAULT_OPTS Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR .SH EXIT STATUS .BR 0 " Normal exit" .br .BR 1 " No match" .br .BR 2 " Error" .br .BR 130 " Interrupted with \fBCTRL-C\fR or \fBESC\fR" .SH FIELD INDEX EXPRESSION A field index expression can be a non-zero integer or a range expression ([BEGIN]..[END]). \fB--nth\fR and \fB--with-nth\fR take a comma-separated list of field index expressions. .SS Examples .BR 1 " The 1st field" .br .BR 2 " The 2nd field" .br .BR -1 " The last field" .br .BR -2 " The 2nd to last field" .br .BR 3..5 " From the 3rd field to the 5th field" .br .BR 2.. " From the 2nd field to the last field" .br .BR ..-3 " From the 1st field to the 3rd to the last field" .br .BR .. " All the fields" .br .SH EXTENDED SEARCH MODE Unless specified otherwise, fzf will start in "extended-search mode". In this mode, you can specify multiple patterns delimited by spaces, such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR You can prepend a backslash to a space (\fB\\ \fR) to match a literal space character. .SS Exact-match (quoted) A term that is prefixed by a single-quote character (\fB'\fR) is interpreted as an "exact-match" (or "non-fuzzy") term. fzf will search for the exact occurrences of the string. .SS Anchored-match A term can be prefixed by \fB^\fR, or suffixed by \fB$\fR to become an anchored-match term. Then fzf will search for the lines that start with or end with the given string. An anchored-match term is also an exact-match term. .SS Negation If a term is prefixed by \fB!\fR, fzf will exclude the lines that satisfy the term from the result. In this case, fzf performs exact match by default. .SS Exact-match by default If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with \fB'\fR) every word, start fzf with \fB-e\fR or \fB--exact\fR option. Note that when \fB--exact\fR is set, \fB'\fR-prefix "unquotes" the term. .SS OR operator A single bar character term acts as an OR operator. For example, the following query matches entries that start with \fBcore\fR and end with either \fBgo\fR, \fBrb\fR, or \fBpy\fR. e.g. \fB^core go$ | rb$ | py$\fR .SH KEY/EVENT BINDINGS \fB--bind\fR option allows you to bind \fBa key\fR or \fBan event\fR to one or more \fBactions\fR. You can use it to customize key bindings or implement dynamic behaviors. \fB--bind\fR takes a comma-separated list of binding expressions. Each binding expression is \fBKEY:ACTION\fR or \fBEVENT:ACTION\fR. e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR .SS AVAILABLE KEYS: (SYNONYMS) \fIctrl-[a-z]\fR .br \fIctrl-space\fR .br \fIctrl-\\\fR .br \fIctrl-]\fR .br \fIctrl-^\fR (\fIctrl-6\fR) .br \fIctrl-/\fR (\fIctrl-_\fR) .br \fIctrl-alt-[a-z]\fR .br \fIalt-[a-z]\fR .br \fIalt-[0-9]\fR .br \fIf[1-12]\fR .br \fIenter\fR (\fIreturn\fR \fIctrl-m\fR) .br \fIspace\fR .br \fIbspace\fR (\fIbs\fR) .br \fIalt-up\fR .br \fIalt-down\fR .br \fIalt-left\fR .br \fIalt-right\fR .br \fIalt-enter\fR .br \fIalt-space\fR .br \fIalt-bspace\fR (\fIalt-bs\fR) .br \fIalt-/\fR .br \fItab\fR .br \fIbtab\fR (\fIshift-tab\fR) .br \fIesc\fR .br \fIdel\fR .br \fIup\fR .br \fIdown\fR .br \fIleft\fR .br \fIright\fR .br \fIhome\fR .br \fIend\fR .br \fIpgup\fR (\fIpage-up\fR) .br \fIpgdn\fR (\fIpage-down\fR) .br \fIshift-up\fR .br \fIshift-down\fR .br \fIshift-left\fR .br \fIshift-right\fR .br \fIleft-click\fR .br \fIright-click\fR .br \fIdouble-click\fR .br or any single character .SS AVAILABLE EVENTS: \fIchange\fR (triggered whenever the query string is changed) .br e.g. \fB# Moves cursor to the top (or bottom depending on --layout) whenever the query is changed fzf --bind change:top\fR .SS AVAILABLE ACTIONS: A key or an event can be bound to one or more of the following actions. \fBACTION: DEFAULT BINDINGS (NOTES): \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR \fBaccept\fR \fIenter double-click\fR \fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection) \fBbackward-char\fR \fIctrl-b left\fR \fBbackward-delete-char\fR \fIctrl-h bspace\fR \fBbackward-kill-word\fR \fIalt-bs\fR \fBbackward-word\fR \fIalt-b shift-left\fR \fBbeginning-of-line\fR \fIctrl-a home\fR \fBcancel\fR (clear query string if not empty, abort fzf otherwise) \fBclear-screen\fR \fIctrl-l\fR \fBclear-selection\fR (clear multi-selection) \fBclear-query\fR (clear query string) \fBdelete-char\fR \fIdel\fR \fBdelete-char/eof\fR \fIctrl-d\fR \fBdeselect-all\fR (deselect all matches) \fBdown\fR \fIctrl-j ctrl-n down\fR \fBend-of-line\fR \fIctrl-e end\fR \fBexecute(...)\fR (see below for the details) \fBexecute-silent(...)\fR (see below for the details) \fRexecute-multi(...)\fR (deprecated in favor of \fB{+}\fR expression) \fBforward-char\fR \fIctrl-f right\fR \fBforward-word\fR \fIalt-f shift-right\fR \fBignore\fR \fBjump\fR (EasyMotion-like 2-keystroke movement) \fBjump-accept\fR (jump and accept) \fBkill-line\fR \fBkill-word\fR \fIalt-d\fR \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) \fBpage-down\fR \fIpgdn\fR \fBpage-up\fR \fIpgup\fR \fBhalf-page-down\fR \fBhalf-page-up\fR \fBpreview-down\fR \fIshift-down\fR \fBpreview-up\fR \fIshift-up\fR \fBpreview-page-down\fR \fBpreview-page-up\fR \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR) \fBprint-query\fR (print query and exit) \fBreload(...)\fR (see below for the details) \fBreplace-query\fR (replace query string with the current selection) \fBselect-all\fR (select all matches) \fBtoggle\fR (\fIright-click\fR) \fBtoggle-all\fR (toggle all matches) \fBtoggle+down\fR \fIctrl-i (tab)\fR \fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR) \fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR) \fBtoggle-preview\fR \fBtoggle-preview-wrap\fR \fBtoggle-sort\fR \fBtoggle+up\fR \fIbtab (shift-tab)\fR \fBtop\fR (move to the top result) \fBunix-line-discard\fR \fIctrl-u\fR \fBunix-word-rubout\fR \fIctrl-w\fR \fBup\fR \fIctrl-k ctrl-p up\fR \fByank\fR \fIctrl-y\fR .SS ACTION COMPOSITION Multiple actions can be chained using \fB+\fR separator. e.g. \fBfzf --bind 'ctrl-a:select-all+accept'\fR .SS COMMAND EXECUTION With \fBexecute(...)\fR action, you can execute arbitrary commands without leaving fzf. For example, you can turn fzf into a simple file browser by binding \fBenter\fR key to \fBless\fR command like follows. \fBfzf --bind "enter:execute(less {})"\fR You can use the same placeholder expressions as in \fB--preview\fR. If the command contains parentheses, fzf may fail to parse the expression. In that case, you can use any of the following alternative notations to avoid parse errors. \fBexecute[...]\fR \fBexecute~...~\fR \fBexecute!...!\fR \fBexecute@...@\fR \fBexecute#...#\fR \fBexecute$...$\fR \fBexecute%...%\fR \fBexecute^...^\fR \fBexecute&...&\fR \fBexecute*...*\fR \fBexecute;...;\fR \fBexecute/.../\fR \fBexecute|...|\fR \fBexecute:...\fR .RS The last one is the special form that frees you from parse errors as it does not expect the closing character. The catch is that it should be the last one in the comma-separated list of key-action pairs. .RE fzf switches to the alternate screen when executing a command. However, if the command is expected to complete quickly, and you are not interested in its output, you might want to use \fBexecute-silent\fR instead, which silently executes the command without the switching. Note that fzf will not be responsive until the command is complete. For asynchronous execution, start your command as a background process (i.e. appending \fB&\fR). .SS RELOAD INPUT \fBreload(...)\fR action is used to dynamically update the input list without restarting fzf. It takes the same command template with placeholder expressions as \fBexecute(...)\fR. See \fIhttps://github.com/junegunn/fzf/issues/1750\fR for more info. e.g. \fB# Update the list of processes by pressing CTRL-R ps -ef | fzf --bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \\ --header-lines=1 --layout=reverse # Integration with ripgrep RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " INITIAL_QUERY="foobar" FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \\ fzf --bind "change:reload:$RG_PREFIX {q} || true" \\ --ansi --phony --query "$INITIAL_QUERY"\fR .SH AUTHOR Junegunn Choi (\fIjunegunn.c@gmail.com\fR) .SH SEE ALSO .B Project homepage: .RS .I https://github.com/junegunn/fzf .RE .br .br .B Extra Vim plugin: .RS .I https://github.com/junegunn/fzf.vim .RE .SH LICENSE MIT fzf-0.20.0/plugin/000077500000000000000000000000001357617647500137015ustar00rootroot00000000000000fzf-0.20.0/plugin/fzf.vim000066400000000000000000000576611357617647500152220ustar00rootroot00000000000000" Copyright (c) 2017 Junegunn Choi " " MIT License " " Permission is hereby granted, free of charge, to any person obtaining " a copy of this software and associated documentation files (the " "Software"), to deal in the Software without restriction, including " without limitation the rights to use, copy, modify, merge, publish, " distribute, sublicense, and/or sell copies of the Software, and to " permit persons to whom the Software is furnished to do so, subject to " the following conditions: " " The above copyright notice and this permission notice shall be " included in all copies or substantial portions of the Software. " " THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, " EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF " MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND " NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE " LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION " OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION " WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. if exists('g:loaded_fzf') finish endif let g:loaded_fzf = 1 let s:is_win = has('win32') || has('win64') if s:is_win && &shellslash set noshellslash let s:base_dir = expand(':h:h') set shellslash else let s:base_dir = expand(':h:h') endif if s:is_win let s:term_marker = '&::FZF' function! s:fzf_call(fn, ...) let shellslash = &shellslash try set noshellslash return call(a:fn, a:000) finally let &shellslash = shellslash endtry endfunction " Use utf-8 for fzf.vim commands " Return array of shell commands for cmd.exe let s:codepage = libcallnr('kernel32.dll', 'GetACP', 0) function! s:enc_to_cp(str) return iconv(a:str, &encoding, 'cp'.s:codepage) endfunction function! s:wrap_cmds(cmds) return map([ \ '@echo off', \ 'setlocal enabledelayedexpansion'] \ + (has('gui_running') ? ['set TERM= > nul'] : []) \ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds]) \ + ['endlocal'], \ 'enc_to_cp(v:val."\r")') endfunction else let s:term_marker = ";#FZF" function! s:fzf_call(fn, ...) return call(a:fn, a:000) endfunction function! s:wrap_cmds(cmds) return a:cmds endfunction function! s:enc_to_cp(str) return a:str endfunction endif function! s:shellesc_cmd(arg) let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g') let escaped = substitute(escaped, '%', '%%', 'g') let escaped = substitute(escaped, '"', '\\^&', 'g') let escaped = substitute(escaped, '\(\\\+\)\(\\^\)', '\1\1\2', 'g') return '^"'.substitute(escaped, '\(\\\+\)$', '\1\1', '').'^"' endfunction function! fzf#shellescape(arg, ...) let shell = get(a:000, 0, s:is_win ? 'cmd.exe' : 'sh') if shell =~# 'cmd.exe$' return s:shellesc_cmd(a:arg) endif return s:fzf_call('shellescape', a:arg) endfunction function! s:fzf_getcwd() return s:fzf_call('getcwd') endfunction function! s:fzf_fnamemodify(fname, mods) return s:fzf_call('fnamemodify', a:fname, a:mods) endfunction function! s:fzf_expand(fmt) return s:fzf_call('expand', a:fmt, 1) endfunction function! s:fzf_tempname() return s:fzf_call('tempname') endfunction let s:default_layout = { 'down': '~40%' } let s:layout_keys = ['window', 'up', 'down', 'left', 'right'] let s:fzf_go = s:base_dir.'/bin/fzf' let s:fzf_tmux = s:base_dir.'/bin/fzf-tmux' let s:install = s:base_dir.'/install' let s:installed = 0 let s:cpo_save = &cpo set cpo&vim function! s:fzf_exec() if !exists('s:exec') if executable(s:fzf_go) let s:exec = s:fzf_go elseif executable('fzf') let s:exec = 'fzf' elseif s:is_win && !has('win32unix') call s:warn('fzf executable not found.') call s:warn('Download fzf binary for Windows from https://github.com/junegunn/fzf-bin/releases/') call s:warn('and place it as '.s:base_dir.'\bin\fzf.exe') throw 'fzf executable not found' elseif !s:installed && executable(s:install) && \ input('fzf executable not found. Download binary? (y/n) ') =~? '^y' redraw echo call s:warn('Downloading fzf binary. Please wait ...') let s:installed = 1 call system(s:install.' --bin') return s:fzf_exec() else redraw throw 'fzf executable not found' endif endif return fzf#shellescape(s:exec) endfunction function! s:tmux_enabled() if has('gui_running') return 0 endif if exists('s:tmux') return s:tmux endif let s:tmux = 0 if exists('$TMUX') && executable(s:fzf_tmux) let output = system('tmux -V') let s:tmux = !v:shell_error && output >= 'tmux 1.7' endif return s:tmux endfunction function! s:escape(path) let path = fnameescape(a:path) return s:is_win ? escape(path, '$') : path endfunction " Upgrade legacy options function! s:upgrade(dict) let copy = copy(a:dict) if has_key(copy, 'tmux') let copy.down = remove(copy, 'tmux') endif if has_key(copy, 'tmux_height') let copy.down = remove(copy, 'tmux_height') endif if has_key(copy, 'tmux_width') let copy.right = remove(copy, 'tmux_width') endif return copy endfunction function! s:error(msg) echohl ErrorMsg echom a:msg echohl None endfunction function! s:warn(msg) echohl WarningMsg echom a:msg echohl None endfunction function! s:has_any(dict, keys) for key in a:keys if has_key(a:dict, key) return 1 endif endfor return 0 endfunction function! s:open(cmd, target) if stridx('edit', a:cmd) == 0 && s:fzf_fnamemodify(a:target, ':p') ==# s:fzf_expand('%:p') return endif execute a:cmd s:escape(a:target) endfunction function! s:common_sink(action, lines) abort if len(a:lines) < 2 return endif let key = remove(a:lines, 0) let Cmd = get(a:action, key, 'e') if type(Cmd) == type(function('call')) return Cmd(a:lines) endif if len(a:lines) > 1 augroup fzf_swap autocmd SwapExists * let v:swapchoice='o' \| call s:warn('fzf: E325: swap file exists: '.s:fzf_expand('')) augroup END endif try let empty = empty(s:fzf_expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified let autochdir = &autochdir set noautochdir for item in a:lines if empty execute 'e' s:escape(item) let empty = 0 else call s:open(Cmd, item) endif if !has('patch-8.0.0177') && !has('nvim-0.2') && exists('#BufEnter') \ && isdirectory(item) doautocmd BufEnter endif endfor catch /^Vim:Interrupt$/ finally let &autochdir = autochdir silent! autocmd! fzf_swap endtry endfunction function! s:get_color(attr, ...) let gui = !s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors let fam = gui ? 'gui' : 'cterm' let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$' for group in a:000 let code = synIDattr(synIDtrans(hlID(group)), a:attr, fam) if code =~? pat return code endif endfor return '' endfunction function! s:defaults() let rules = copy(get(g:, 'fzf_colors', {})) let colors = join(map(items(filter(map(rules, 'call("s:get_color", v:val)'), '!empty(v:val)')), 'join(v:val, ":")'), ',') return empty(colors) ? '' : fzf#shellescape('--color='.colors) endfunction function! s:validate_layout(layout) for key in keys(a:layout) if index(s:layout_keys, key) < 0 throw printf('Invalid entry in g:fzf_layout: %s (allowed: %s)%s', \ key, join(s:layout_keys, ', '), key == 'options' ? '. Use $FZF_DEFAULT_OPTS.' : '') endif endfor return a:layout endfunction function! s:evaluate_opts(options) return type(a:options) == type([]) ? \ join(map(copy(a:options), 'fzf#shellescape(v:val)')) : a:options endfunction " [name string,] [opts dict,] [fullscreen boolean] function! fzf#wrap(...) let args = ['', {}, 0] let expects = map(copy(args), 'type(v:val)') let tidx = 0 for arg in copy(a:000) let tidx = index(expects, type(arg), tidx) if tidx < 0 throw 'Invalid arguments (expected: [name string] [opts dict] [fullscreen boolean])' endif let args[tidx] = arg let tidx += 1 unlet arg endfor let [name, opts, bang] = args if len(name) let opts.name = name end " Layout: g:fzf_layout (and deprecated g:fzf_height) if bang for key in s:layout_keys if has_key(opts, key) call remove(opts, key) endif endfor elseif !s:has_any(opts, s:layout_keys) if !exists('g:fzf_layout') && exists('g:fzf_height') let opts.down = g:fzf_height else let opts = extend(opts, s:validate_layout(get(g:, 'fzf_layout', s:default_layout))) endif endif " Colors: g:fzf_colors let opts.options = s:defaults() .' '. s:evaluate_opts(get(opts, 'options', '')) " History: g:fzf_history_dir if len(name) && len(get(g:, 'fzf_history_dir', '')) let dir = s:fzf_expand(g:fzf_history_dir) if !isdirectory(dir) call mkdir(dir, 'p') endif let history = fzf#shellescape(dir.'/'.name) let opts.options = join(['--history', history, opts.options]) endif " Action: g:fzf_action if !s:has_any(opts, ['sink', 'sink*']) let opts._action = get(g:, 'fzf_action', s:default_action) let opts.options .= ' --expect='.join(keys(opts._action), ',') function! opts.sink(lines) abort return s:common_sink(self._action, a:lines) endfunction let opts['sink*'] = remove(opts, 'sink') endif return opts endfunction function! s:use_sh() let [shell, shellslash, shellcmdflag, shellxquote] = [&shell, &shellslash, &shellcmdflag, &shellxquote] if s:is_win set shell=cmd.exe set noshellslash let &shellcmdflag = has('nvim') ? '/s /c' : '/c' let &shellxquote = has('nvim') ? '"' : '(' else set shell=sh endif return [shell, shellslash, shellcmdflag, shellxquote] endfunction function! fzf#run(...) abort try let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh() let dict = exists('a:1') ? s:upgrade(a:1) : {} let temps = { 'result': s:fzf_tempname() } let optstr = s:evaluate_opts(get(dict, 'options', '')) try let fzf_exec = s:fzf_exec() catch throw v:exception endtry if !has_key(dict, 'dir') let dict.dir = s:fzf_getcwd() endif if has('win32unix') && has_key(dict, 'dir') let dict.dir = fnamemodify(dict.dir, ':p') endif if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) && !s:is_win let temps.source = s:fzf_tempname() call writefile(s:wrap_cmds(split($FZF_DEFAULT_COMMAND, "\n")), temps.source) let dict.source = (empty($SHELL) ? &shell : $SHELL).' '.fzf#shellescape(temps.source) endif if has_key(dict, 'source') let source = dict.source let type = type(source) if type == 1 let prefix = '( '.source.' )|' elseif type == 3 let temps.input = s:fzf_tempname() call writefile(map(source, 'enc_to_cp(v:val)'), temps.input) let prefix = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input).'|' else throw 'Invalid source type' endif else let prefix = '' endif let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) let use_height = has_key(dict, 'down') && !has('gui_running') && \ !(has('nvim') || s:is_win || has('win32unix') || s:present(dict, 'up', 'left', 'right', 'window')) && \ executable('tput') && filereadable('/dev/tty') let has_vim8_term = has('terminal') && has('patch-8.0.995') let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win let use_term = has_nvim_term || \ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || !use_height && s:present(dict, 'down', 'up', 'left', 'right', 'window')) let use_tmux = (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:tmux_enabled() && s:splittable(dict) if prefer_tmux && use_tmux let use_height = 0 let use_term = 0 endif if use_height let height = s:calc_size(&lines, dict.down, dict) let optstr .= ' --height='.height elseif use_term let optstr .= ' --no-height' endif let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result if use_term return s:execute_term(dict, command, temps) endif let lines = use_tmux ? s:execute_tmux(dict, command, temps) \ : s:execute(dict, command, use_height, temps) call s:callback(dict, lines) return lines finally let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote] endtry endfunction function! s:present(dict, ...) for key in a:000 if !empty(get(a:dict, key, '')) return 1 endif endfor return 0 endfunction function! s:fzf_tmux(dict) let size = '' for o in ['up', 'down', 'left', 'right'] if s:present(a:dict, o) let spec = a:dict[o] if (o == 'up' || o == 'down') && spec[0] == '~' let size = '-'.o[0].s:calc_size(&lines, spec, a:dict) else " Legacy boolean option let size = '-'.o[0].(spec == 1 ? '' : substitute(spec, '^\~', '', '')) endif break endif endfor return printf('LINES=%d COLUMNS=%d %s %s %s --', \ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-')) endfunction function! s:splittable(dict) return s:present(a:dict, 'up', 'down') && &lines > 15 || \ s:present(a:dict, 'left', 'right') && &columns > 40 endfunction function! s:pushd(dict) if s:present(a:dict, 'dir') let cwd = s:fzf_getcwd() let w:fzf_pushd = { \ 'command': haslocaldir() ? 'lcd' : (exists(':tcd') && haslocaldir(-1) ? 'tcd' : 'cd'), \ 'origin': cwd, \ 'bufname': bufname('') \ } execute 'lcd' s:escape(a:dict.dir) let cwd = s:fzf_getcwd() let w:fzf_pushd.dir = cwd let a:dict.pushd = w:fzf_pushd return cwd endif return '' endfunction augroup fzf_popd autocmd! autocmd WinEnter * call s:dopopd() augroup END function! s:dopopd() if !exists('w:fzf_pushd') return endif " FIXME: We temporarily change the working directory to 'dir' entry " of options dictionary (set to the current working directory if not given) " before running fzf. " " e.g. call fzf#run({'dir': '/tmp', 'source': 'ls', 'sink': 'e'}) " " After processing the sink function, we have to restore the current working " directory. But doing so may not be desirable if the function changed the " working directory on purpose. " " So how can we tell if we should do it or not? A simple heuristic we use " here is that we change directory only if the current working directory " matches 'dir' entry. However, it is possible that the sink function did " change the directory to 'dir'. In that case, the user will have an " unexpected result. if s:fzf_getcwd() ==# w:fzf_pushd.dir && (!&autochdir || w:fzf_pushd.bufname ==# bufname('')) execute w:fzf_pushd.command s:escape(w:fzf_pushd.origin) endif unlet w:fzf_pushd endfunction function! s:xterm_launcher() let fmt = 'xterm -T "[fzf]" -bg "%s" -fg "%s" -geometry %dx%d+%d+%d -e bash -ic %%s' if has('gui_macvim') let fmt .= '&& osascript -e "tell application \"MacVim\" to activate"' endif return printf(fmt, \ escape(synIDattr(hlID("Normal"), "bg"), '#'), escape(synIDattr(hlID("Normal"), "fg"), '#'), \ &columns, &lines/2, getwinposx(), getwinposy()) endfunction unlet! s:launcher if s:is_win || has('win32unix') let s:launcher = '%s' else let s:launcher = function('s:xterm_launcher') endif function! s:exit_handler(code, command, ...) if a:code == 130 return 0 elseif has('nvim') && a:code == 129 " When deleting the terminal buffer while fzf is still running, " Nvim sends SIGHUP. return 0 elseif a:code > 1 call s:error('Error running ' . a:command) if !empty(a:000) sleep endif return 0 endif return 1 endfunction function! s:execute(dict, command, use_height, temps) abort call s:pushd(a:dict) if has('unix') && !a:use_height silent! !clear 2> /dev/null endif let escaped = (a:use_height || s:is_win) ? a:command : escape(substitute(a:command, '\n', '\\n', 'g'), '%#!') if has('gui_running') let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher))) let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher if has('unix') let escaped = "'".substitute(escaped, "'", "'\"'\"'", 'g')."'" endif let command = printf(fmt, escaped) else let command = escaped endif if s:is_win let batchfile = s:fzf_tempname().'.bat' call writefile(s:wrap_cmds(command), batchfile) let command = batchfile let a:temps.batchfile = batchfile if has('nvim') let fzf = {} let fzf.dict = a:dict let fzf.temps = a:temps function! fzf.on_exit(job_id, exit_status, event) dict call s:pushd(self.dict) let lines = s:collect(self.temps) call s:callback(self.dict, lines) endfunction let cmd = 'start /wait cmd /c '.command call jobstart(cmd, fzf) return [] endif elseif has('win32unix') && $TERM !=# 'cygwin' let shellscript = s:fzf_tempname() call writefile([command], shellscript) let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript) let a:temps.shellscript = shellscript endif if a:use_height let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty' call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin)) else execute 'silent !'.command endif let exit_status = v:shell_error redraw! return s:exit_handler(exit_status, command) ? s:collect(a:temps) : [] endfunction function! s:execute_tmux(dict, command, temps) abort let command = a:command let cwd = s:pushd(a:dict) if len(cwd) " -c '#{pane_current_path}' is only available on tmux 1.9 or above let command = join(['cd', fzf#shellescape(cwd), '&&', command]) endif call system(command) let exit_status = v:shell_error redraw! return s:exit_handler(exit_status, command) ? s:collect(a:temps) : [] endfunction function! s:calc_size(max, val, dict) let val = substitute(a:val, '^\~', '', '') if val =~ '%$' let size = a:max * str2nr(val[:-2]) / 100 else let size = min([a:max, str2nr(val)]) endif let srcsz = -1 if type(get(a:dict, 'source', 0)) == type([]) let srcsz = len(a:dict.source) endif let opts = $FZF_DEFAULT_OPTS.' '.s:evaluate_opts(get(a:dict, 'options', '')) let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2 let margin += stridx(opts, '--border') > stridx(opts, '--no-border') ? 2 : 0 let margin += stridx(opts, '--header') > stridx(opts, '--no-header') return srcsz >= 0 ? min([srcsz + margin, size]) : size endfunction function! s:getpos() return {'tab': tabpagenr(), 'win': winnr(), 'cnt': winnr('$'), 'tcnt': tabpagenr('$')} endfunction function! s:split(dict) let directions = { \ 'up': ['topleft', 'resize', &lines], \ 'down': ['botright', 'resize', &lines], \ 'left': ['vertical topleft', 'vertical resize', &columns], \ 'right': ['vertical botright', 'vertical resize', &columns] } let ppos = s:getpos() try if s:present(a:dict, 'window') execute 'keepalt' a:dict.window elseif !s:splittable(a:dict) execute (tabpagenr()-1).'tabnew' else for [dir, triple] in items(directions) let val = get(a:dict, dir, '') if !empty(val) let [cmd, resz, max] = triple if (dir == 'up' || dir == 'down') && val[0] == '~' let sz = s:calc_size(max, val, a:dict) else let sz = s:calc_size(max, val, {}) endif execute cmd sz.'new' execute resz sz return [ppos, {}] endif endfor endif return [ppos, { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh }] finally setlocal winfixwidth winfixheight endtry endfunction function! s:execute_term(dict, command, temps) abort let winrest = winrestcmd() let pbuf = bufnr('') let [ppos, winopts] = s:split(a:dict) call s:use_sh() let b:fzf = a:dict let fzf = { 'buf': bufnr(''), 'pbuf': pbuf, 'ppos': ppos, 'dict': a:dict, 'temps': a:temps, \ 'winopts': winopts, 'winrest': winrest, 'lines': &lines, \ 'columns': &columns, 'command': a:command } function! fzf.switch_back(inplace) if a:inplace && bufnr('') == self.buf if bufexists(self.pbuf) execute 'keepalt b' self.pbuf endif " No other listed buffer if bufnr('') == self.buf enew endif endif endfunction function! fzf.on_exit(id, code, ...) if s:getpos() == self.ppos " {'window': 'enew'} for [opt, val] in items(self.winopts) execute 'let' opt '=' val endfor call self.switch_back(1) else if bufnr('') == self.buf " We use close instead of bd! since Vim does not close the split when " there's no other listed buffer (nvim +'set nobuflisted') close endif execute 'tabnext' self.ppos.tab execute self.ppos.win.'wincmd w' endif if bufexists(self.buf) execute 'bd!' self.buf endif if &lines == self.lines && &columns == self.columns && s:getpos() == self.ppos execute self.winrest endif if !s:exit_handler(a:code, self.command, 1) return endif call s:pushd(self.dict) let lines = s:collect(self.temps) call s:callback(self.dict, lines) call self.switch_back(s:getpos() == self.ppos) endfunction try call s:pushd(a:dict) if s:is_win let fzf.temps.batchfile = s:fzf_tempname().'.bat' call writefile(s:wrap_cmds(a:command), fzf.temps.batchfile) let command = fzf.temps.batchfile else let command = a:command endif let command .= s:term_marker if has('nvim') call termopen(command, fzf) else let fzf.buf = term_start([&shell, &shellcmdflag, command], {'curwin': 1, 'exit_cb': function(fzf.on_exit)}) if !has('patch-8.0.1261') && !has('nvim') && !s:is_win call term_wait(fzf.buf, 20) endif endif finally call s:dopopd() endtry setlocal nospell bufhidden=wipe nobuflisted nonumber setf fzf startinsert return [] endfunction function! s:collect(temps) abort try return filereadable(a:temps.result) ? readfile(a:temps.result) : [] finally for tf in values(a:temps) silent! call delete(tf) endfor endtry endfunction function! s:callback(dict, lines) abort let popd = has_key(a:dict, 'pushd') if popd let w:fzf_pushd = a:dict.pushd endif try if has_key(a:dict, 'sink') for line in a:lines if type(a:dict.sink) == 2 call a:dict.sink(line) else execute a:dict.sink s:escape(line) endif endfor endif if has_key(a:dict, 'sink*') call a:dict['sink*'](a:lines) endif catch if stridx(v:exception, ':E325:') < 0 echoerr v:exception endif endtry " We may have opened a new window or tab if popd let w:fzf_pushd = a:dict.pushd call s:dopopd() endif endfunction let s:default_action = { \ 'ctrl-t': 'tab split', \ 'ctrl-x': 'split', \ 'ctrl-v': 'vsplit' } function! s:shortpath() let short = fnamemodify(getcwd(), ':~:.') if !has('win32unix') let short = pathshorten(short) endif let slash = (s:is_win && !&shellslash) ? '\' : '/' return empty(short) ? '~'.slash : short . (short =~ escape(slash, '\').'$' ? '' : slash) endfunction function! s:cmd(bang, ...) abort let args = copy(a:000) let opts = { 'options': ['--multi'] } if len(args) && isdirectory(expand(args[-1])) let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '') if s:is_win && !&shellslash let opts.dir = substitute(opts.dir, '/', '\\', 'g') endif let prompt = opts.dir else let prompt = s:shortpath() endif let prompt = strwidth(prompt) < &columns - 20 ? prompt : '> ' call extend(opts.options, ['--prompt', prompt]) call extend(opts.options, args) call fzf#run(fzf#wrap('FZF', opts, a:bang)) endfunction command! -nargs=* -complete=dir -bang FZF call s:cmd(0, ) let &cpo = s:cpo_save unlet s:cpo_save fzf-0.20.0/shell/000077500000000000000000000000001357617647500135125ustar00rootroot00000000000000fzf-0.20.0/shell/completion.bash000066400000000000000000000230411357617647500165220ustar00rootroot00000000000000# ____ ____ # / __/___ / __/ # / /_/_ / / /_ # / __/ / /_/ __/ # /_/ /___/_/-completion.bash # # - $FZF_TMUX (default: 0) # - $FZF_TMUX_HEIGHT (default: '40%') # - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_OPTS (default: empty) if [[ $- =~ i ]]; then # To use custom commands instead of find, override _fzf_compgen_{path,dir} if ! declare -f _fzf_compgen_path > /dev/null; then _fzf_compgen_path() { echo "$1" command find -L "$1" \ -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' } fi if ! declare -f _fzf_compgen_dir > /dev/null; then _fzf_compgen_dir() { command find -L "$1" \ -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \ -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' } fi ########################################################### # To redraw line after fzf closes (printf '\e[5n') bind '"\e[0n": redraw-current-line' __fzfcmd_complete() { [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" } __fzf_orig_completion_filter() { sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2"; [[ "\1" = *" -o nospace "* ]] \&\& [[ ! "$__fzf_nospace_commands" = *" \3 "* ]] \&\& __fzf_nospace_commands="$__fzf_nospace_commands \3 ";/' | awk -F= '{OFS = FS} {gsub(/[^A-Za-z0-9_= ;]/, "_", $1);}1' } _fzf_opts_completion() { local cur prev opts COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" opts=" -x --extended -e --exact --algo -i +i -n --nth --with-nth -d --delimiter +s --no-sort --tac --tiebreak -m --multi --no-mouse --bind --cycle --no-hscroll --jump-labels --height --literal --reverse --margin --inline-info --prompt --header --header-lines --ansi --tabstop --color --no-bold --history --history-size --preview --preview-window -q --query -1 --select-1 -0 --exit-0 -f --filter --print-query --expect --sync" case "${prev}" in --tiebreak) COMPREPLY=( $(compgen -W "length begin end index" -- "$cur") ) return 0 ;; --color) COMPREPLY=( $(compgen -W "dark light 16 bw" -- "$cur") ) return 0 ;; --history) COMPREPLY=() return 0 ;; esac if [[ "$cur" =~ ^-|\+ ]]; then COMPREPLY=( $(compgen -W "${opts}" -- "$cur") ) return 0 fi return 0 } _fzf_handle_dynamic_completion() { local cmd orig_var orig ret orig_cmd orig_complete cmd="$1" shift orig_cmd="$1" orig_var="_fzf_orig_completion_$cmd" orig="${!orig_var##*#}" if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then $orig "$@" elif [ -n "$_fzf_completion_loader" ]; then orig_complete=$(complete -p "$orig_cmd" 2> /dev/null) _completion_loader "$@" ret=$? # _completion_loader may not have updated completion for the command if [ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]; then eval "$(complete | command grep " -F.* $orig_cmd$" | __fzf_orig_completion_filter)" if [[ "$__fzf_nospace_commands" = *" $orig_cmd "* ]]; then eval "${orig_complete/ -F / -o nospace -F }" else eval "$orig_complete" fi fi return $ret fi } __fzf_generic_path_completion() { local cur base dir leftover matches trigger cmd fzf fzf="$(__fzfcmd_complete)" cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}" COMPREPLY=() trigger=${FZF_COMPLETION_TRIGGER-'**'} cur="${COMP_WORDS[COMP_CWORD]}" if [[ "$cur" == *"$trigger" ]]; then base=${cur:0:${#cur}-${#trigger}} eval "base=$base" [[ $base = *"/"* ]] && dir="$base" while true; do if [ -z "$dir" ] || [ -d "$dir" ]; then leftover=${base/#"$dir"} leftover=${leftover/#\/} [ -z "$dir" ] && dir='.' [ "$dir" != "/" ] && dir="${dir/%\//}" matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $2 -q "$leftover" | while read -r item; do printf "%q$3 " "$item" done) matches=${matches% } [[ -z "$3" ]] && [[ "$__fzf_nospace_commands" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches " if [ -n "$matches" ]; then COMPREPLY=( "$matches" ) else COMPREPLY=( "$cur" ) fi printf '\e[5n' return 0 fi dir=$(dirname "$dir") [[ "$dir" =~ /$ ]] || dir="$dir"/ done else shift shift shift _fzf_handle_dynamic_completion "$cmd" "$@" fi } _fzf_complete() { local cur selected trigger cmd fzf post post="$(caller 0 | awk '{print $2}')_post" type -t "$post" > /dev/null 2>&1 || post=cat fzf="$(__fzfcmd_complete)" cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}" trigger=${FZF_COMPLETION_TRIGGER-'**'} cur="${COMP_WORDS[COMP_CWORD]}" if [[ "$cur" == *"$trigger" ]]; then cur=${cur:0:${#cur}-${#trigger}} selected=$(cat | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $1 -q "$cur" | $post | tr '\n' ' ') selected=${selected% } # Strip trailing space not to repeat "-o nospace" if [ -n "$selected" ]; then COMPREPLY=("$selected") else COMPREPLY=("$cur") fi printf '\e[5n' return 0 else shift _fzf_handle_dynamic_completion "$cmd" "$@" fi } _fzf_path_completion() { __fzf_generic_path_completion _fzf_compgen_path "-m" "" "$@" } # Deprecated. No file only completion. _fzf_file_completion() { _fzf_path_completion "$@" } _fzf_dir_completion() { __fzf_generic_path_completion _fzf_compgen_dir "" "/" "$@" } _fzf_complete_kill() { [ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1 local selected fzf fzf="$(__fzfcmd_complete)" selected=$(command ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ') printf '\e[5n' if [ -n "$selected" ]; then COMPREPLY=( "$selected" ) return 0 fi } _fzf_complete_telnet() { _fzf_complete '+m' "$@" < <( command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u ) } _fzf_complete_ssh() { _fzf_complete '+m' "$@" < <( cat <(cat ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \ <(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \ <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u ) } _fzf_complete_unset() { _fzf_complete '-m' "$@" < <( declare -xp | sed 's/=.*//' | sed 's/.* //' ) } _fzf_complete_export() { _fzf_complete '-m' "$@" < <( declare -xp | sed 's/=.*//' | sed 's/.* //' ) } _fzf_complete_unalias() { _fzf_complete '-m' "$@" < <( alias | sed 's/=.*//' | sed 's/.* //' ) } # fzf options complete -o default -F _fzf_opts_completion fzf d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}" a_cmds=" awk cat diff diff3 emacs emacsclient ex file ftp g++ gcc gvim head hg java javac ld less more mvim nvim patch perl python ruby sed sftp sort source tail tee uniq vi view vim wc xdg-open basename bunzip2 bzip2 chmod chown curl cp dirname du find git grep gunzip gzip hg jar ln ls mv open rm rsync scp svn tar unzip zip" x_cmds="kill ssh telnet unset unalias export" # Preserve existing completion eval "$(complete | sed -E '/-F/!d; / _fzf/d; '"/ ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g; s/+/\\+/g'))$/"'!d' | __fzf_orig_completion_filter)" if type _completion_loader > /dev/null 2>&1; then _fzf_completion_loader=1 fi __fzf_defc() { local cmd func opts orig_var orig def cmd="$1" func="$2" opts="$3" orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" orig="${!orig_var}" if [ -n "$orig" ]; then printf -v def "$orig" "$func" eval "$def" else complete -F "$func" $opts "$cmd" fi } # Anything for cmd in $a_cmds; do __fzf_defc "$cmd" _fzf_path_completion "-o default -o bashdefault" done # Directory for cmd in $d_cmds; do __fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames" done # Kill completion complete -F _fzf_complete_kill -o nospace -o default -o bashdefault kill # Host completion complete -F _fzf_complete_ssh -o default -o bashdefault ssh complete -F _fzf_complete_telnet -o default -o bashdefault telnet # Environment variables / Aliases complete -F _fzf_complete_unset -o default -o bashdefault unset complete -F _fzf_complete_export -o default -o bashdefault export complete -F _fzf_complete_unalias -o default -o bashdefault unalias unset cmd d_cmds a_cmds x_cmds _fzf_setup_completion() { local kind fn cmd kind=$1 fn=_fzf_${1}_completion if [[ $# -lt 2 ]] || ! type -t "$fn" > /dev/null; then echo "usage: ${FUNCNAME[0]} path|dir COMMANDS..." return 1 fi shift for cmd in "$@"; do eval "$(complete -p "$cmd" 2> /dev/null | grep -v "$fn" | __fzf_orig_completion_filter)" case "$kind" in dir) __fzf_defc "$cmd" "$fn" "-o nospace -o dirnames" ;; *) __fzf_defc "$cmd" "$fn" "-o default -o bashdefault" ;; esac done } fi fzf-0.20.0/shell/completion.zsh000066400000000000000000000141711357617647500164150ustar00rootroot00000000000000# ____ ____ # / __/___ / __/ # / /_/_ / / /_ # / __/ / /_/ __/ # /_/ /___/_/-completion.zsh # # - $FZF_TMUX (default: 0) # - $FZF_TMUX_HEIGHT (default: '40%') # - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_OPTS (default: empty) if [[ $- =~ i ]]; then # To use custom commands instead of find, override _fzf_compgen_{path,dir} if ! declare -f _fzf_compgen_path > /dev/null; then _fzf_compgen_path() { echo "$1" command find -L "$1" \ -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' } fi if ! declare -f _fzf_compgen_dir > /dev/null; then _fzf_compgen_dir() { command find -L "$1" \ -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \ -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' } fi ########################################################### __fzfcmd_complete() { [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" } __fzf_generic_path_completion() { local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches base=$1 lbuf=$2 compgen=$3 fzf_opts=$4 suffix=$5 tail=$6 fzf="$(__fzfcmd_complete)" setopt localoptions nonomatch eval "base=$base" [[ $base = *"/"* ]] && dir="$base" while [ 1 ]; do if [[ -z "$dir" || -d ${dir} ]]; then leftover=${base/#"$dir"} leftover=${leftover/#\/} [ -z "$dir" ] && dir='.' [ "$dir" != "/" ] && dir="${dir/%\//}" matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "$leftover" | while read item; do echo -n "${(q)item}$suffix " done) matches=${matches% } if [ -n "$matches" ]; then LBUFFER="$lbuf$matches$tail" fi zle reset-prompt break fi dir=$(dirname "$dir") dir=${dir%/}/ done } _fzf_path_completion() { __fzf_generic_path_completion "$1" "$2" _fzf_compgen_path \ "-m" "" " " } _fzf_dir_completion() { __fzf_generic_path_completion "$1" "$2" _fzf_compgen_dir \ "" "/" "" } _fzf_feed_fifo() ( command rm -f "$1" mkfifo "$1" cat <&0 > "$1" & ) _fzf_complete() { local fifo fzf_opts lbuf fzf matches post fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$" fzf_opts=$1 lbuf=$2 post="${funcstack[2]}_post" type $post > /dev/null 2>&1 || post=cat fzf="$(__fzfcmd_complete)" _fzf_feed_fifo "$fifo" matches=$(cat "$fifo" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$lbuf$matches" fi zle reset-prompt command rm -f "$fifo" } _fzf_complete_telnet() { _fzf_complete '+m' "$@" < <( command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u ) } _fzf_complete_ssh() { _fzf_complete '+m' "$@" < <( setopt localoptions nonomatch command cat <(cat ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \ <(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \ <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u ) } _fzf_complete_export() { _fzf_complete '-m' "$@" < <( declare -xp | sed 's/=.*//' | sed 's/.* //' ) } _fzf_complete_unset() { _fzf_complete '-m' "$@" < <( declare -xp | sed 's/=.*//' | sed 's/.* //' ) } _fzf_complete_unalias() { _fzf_complete '+m' "$@" < <( alias | sed 's/=.*//' ) } fzf-completion() { local tokens cmd prefix trigger tail fzf matches lbuf d_cmds setopt localoptions noshwordsplit noksh_arrays noposixbuiltins # http://zsh.sourceforge.net/FAQ/zshfaq03.html # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags tokens=(${(z)LBUFFER}) if [ ${#tokens} -lt 1 ]; then zle ${fzf_default_completion:-expand-or-complete} return fi cmd=${tokens[1]} # Explicitly allow for empty trigger. trigger=${FZF_COMPLETION_TRIGGER-'**'} [ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("") # When the trigger starts with ';', it becomes a separate token if [[ ${LBUFFER} = *"${tokens[-2]}${tokens[-1]}" ]]; then tokens[-2]="${tokens[-2]}${tokens[-1]}" tokens=(${tokens[0,-2]}) fi tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))} # Kill completion (do not require trigger sequence) if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then fzf="$(__fzfcmd_complete)" matches=$(command ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$LBUFFER$matches" fi zle reset-prompt # Trigger sequence given elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}) [ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}} [ -z "${tokens[-1]}" ] && lbuf=$LBUFFER || lbuf=${LBUFFER:0:-${#tokens[-1]}} if eval "type _fzf_complete_${cmd} > /dev/null"; then eval "prefix=\"$prefix\" _fzf_complete_${cmd} \"$lbuf\"" elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then _fzf_dir_completion "$prefix" "$lbuf" else _fzf_path_completion "$prefix" "$lbuf" fi # Fall back to default completion else zle ${fzf_default_completion:-expand-or-complete} fi } [ -z "$fzf_default_completion" ] && { binding=$(bindkey '^I') [[ $binding =~ 'undefined-key' ]] || fzf_default_completion=$binding[(s: :w)2] unset binding } zle -N fzf-completion bindkey '^I' fzf-completion fi fzf-0.20.0/shell/key-bindings.bash000066400000000000000000000110551357617647500167360ustar00rootroot00000000000000# Key bindings # ------------ __fzf_select__() { local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | cut -b3-"}" eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" fzf -m "$@" | while read -r item; do printf '%q ' "$item" done echo } if [[ $- =~ i ]]; then __fzf_use_tmux__() { [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] } __fzfcmd() { __fzf_use_tmux__ && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" } __fzf_select_tmux__() { local height height=${FZF_TMUX_HEIGHT:-40%} if [[ $height =~ %$ ]]; then height="-p ${height%\%}" else height="-l $height" fi tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; RESULT=\"\$(__fzf_select__ --no-height)\"; tmux setb -b fzf \"\$RESULT\" \\; pasteb -b fzf -t $TMUX_PANE \\; deleteb -b fzf || tmux send-keys -t $TMUX_PANE \"\$RESULT\"'" } fzf-file-widget() { if __fzf_use_tmux__; then __fzf_select_tmux__ else local selected="$(__fzf_select__)" READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}" READLINE_POINT=$(( READLINE_POINT + ${#selected} )) fi } __fzf_cd__() { local cmd dir cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | cut -b3-"}" dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir" } __fzf_history__() ( local line shopt -u nocaseglob nocasematch line=$( HISTTIMEFORMAT= builtin history | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac --sync -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" $(__fzfcmd) | command grep '^ *[0-9]') && if [[ $- =~ H ]]; then sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" else sed 's/^ *\([0-9]*\)\** *//' <<< "$line" fi ) if [[ ! -o vi ]]; then # Required to refresh the prompt after fzf bind '"\er": redraw-current-line' bind '"\e^": history-expand-line' # CTRL-T - Paste the selected file path into the command line if [ $BASH_VERSINFO -gt 3 ]; then bind -x '"\C-t": "fzf-file-widget"' elif __fzf_use_tmux__; then bind '"\C-t": " \C-u \C-a\C-k`__fzf_select_tmux__`\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"' else bind '"\C-t": " \C-u \C-a\C-k`__fzf_select__`\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"' fi # CTRL-R - Paste the selected command from history into the command line bind '"\C-r": " \C-e\C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er\e^"' # ALT-C - cd into the selected directory bind '"\ec": " \C-e\C-u`__fzf_cd__`\e\C-e\er\C-m"' else # We'd usually use "\e" to enter vi-movement-mode so we can do our magic, # but this incurs a very noticeable delay of a half second or so, # because many other commands start with "\e". # Instead, we bind an unused key, "\C-x\C-a", # to also enter vi-movement-mode, # and then use that thereafter. # (We imagine that "\C-x\C-a" is relatively unlikely to be in use.) bind '"\C-x\C-a": vi-movement-mode' bind '"\C-x\C-e": shell-expand-line' bind '"\C-x\C-r": redraw-current-line' bind '"\C-x^": history-expand-line' # CTRL-T - Paste the selected file path into the command line # - FIXME: Selected items are attached to the end regardless of cursor position if [ $BASH_VERSINFO -gt 3 ]; then bind -x '"\C-t": "fzf-file-widget"' elif __fzf_use_tmux__; then bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select_tmux__`\C-x\C-e\C-x\C-a0P$xa"' else bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select__`\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "' fi bind -m vi-command '"\C-t": "i\C-t"' # CTRL-R - Paste the selected command from history into the command line bind '"\C-r": "\C-x\C-addi`__fzf_history__`\C-x\C-e\C-x\C-r\C-x^\C-x\C-a$a"' bind -m vi-command '"\C-r": "i\C-r"' # ALT-C - cd into the selected directory bind '"\ec": "\C-x\C-addi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"' bind -m vi-command '"\ec": "ddi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"' fi fi fzf-0.20.0/shell/key-bindings.fish000066400000000000000000000115021357617647500167470ustar00rootroot00000000000000# Key bindings # ------------ function fzf_key_bindings # Store current token in $dir as root for the 'find' command function fzf-file-widget -d "List files and folders" set -l commandline (__fzf_parse_commandline) set -l dir $commandline[1] set -l fzf_query $commandline[2] # "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not # $dir itself, even if hidden. set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND " command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | sed 's@^\./@@'" set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% begin set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end end if [ -z "$result" ] commandline -f repaint return else # Remove last token from commandline. commandline -t "" end for i in $result commandline -it -- (string escape $i) commandline -it -- ' ' end commandline -f repaint end function fzf-history-widget -d "Show command history" set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% begin set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" set -l FISH_MAJOR (echo $version | cut -f1 -d.) set -l FISH_MINOR (echo $version | cut -f2 -d.) # history's -z flag is needed for multi-line support. # history's -z flag was added in fish 2.4.0, so don't use it for versions # before 2.4.0. if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ]; history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result and commandline -- $result else history | eval (__fzfcmd) -q '(commandline)' | read -l result and commandline -- $result end end commandline -f repaint end function fzf-cd-widget -d "Change directory" set -l commandline (__fzf_parse_commandline) set -l dir $commandline[1] set -l fzf_query $commandline[2] set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND " command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ -o -type d -print 2> /dev/null | sed 's@^\./@@'" set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% begin set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result if [ -n "$result" ] cd $result # Remove last token from commandline. commandline -t "" end end commandline -f repaint end function __fzfcmd set -q FZF_TMUX; or set FZF_TMUX 0 set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% if [ $FZF_TMUX -eq 1 ] echo "fzf-tmux -d$FZF_TMUX_HEIGHT" else echo "fzf" end end bind \ct fzf-file-widget bind \cr fzf-history-widget bind \ec fzf-cd-widget if bind -M insert > /dev/null 2>&1 bind -M insert \ct fzf-file-widget bind -M insert \cr fzf-history-widget bind -M insert \ec fzf-cd-widget end function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath and rest of token' # eval is used to do shell expansion on paths set -l commandline (eval "printf '%s' "(commandline -t)) if [ -z $commandline ] # Default to current directory with no --query set dir '.' set fzf_query '' else set dir (__fzf_get_dir $commandline) if [ "$dir" = "." -a (string sub -l 1 $commandline) != '.' ] # if $dir is "." but commandline is not a relative path, this means no file path found set fzf_query $commandline else # Also remove trailing slash after dir, to "split" input properly set fzf_query (string replace -r "^$dir/?" '' "$commandline") end end echo $dir echo $fzf_query end function __fzf_get_dir -d 'Find the longest existing filepath from input string' set dir $argv # Strip all trailing slashes. Ignore if $dir is root dir (/) if [ (string length $dir) -gt 1 ] set dir (string replace -r '/*$' '' $dir) end # Iteratively check if dir exists and strip tail end of path while [ ! -d "$dir" ] # If path is absolute, this can keep going until ends up at / # If path is relative, this can keep going until entire input is consumed, dirname returns "." set dir (dirname "$dir") end echo $dir end end fzf-0.20.0/shell/key-bindings.zsh000066400000000000000000000050671357617647500166330ustar00rootroot00000000000000# Key bindings # ------------ if [[ $- == *i* ]]; then # CTRL-T - Paste the selected file path(s) into the command line __fsel() { local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | cut -b3-"}" setopt localoptions pipefail no_aliases 2> /dev/null eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do echo -n "${(q)item} " done local ret=$? echo return $ret } __fzf_use_tmux__() { [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] } __fzfcmd() { __fzf_use_tmux__ && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" } fzf-file-widget() { LBUFFER="${LBUFFER}$(__fsel)" local ret=$? zle reset-prompt return $ret } zle -N fzf-file-widget bindkey '^T' fzf-file-widget # Ensure precmds are run after cd fzf-redraw-prompt() { local precmd for precmd in $precmd_functions; do $precmd done zle reset-prompt } zle -N fzf-redraw-prompt # ALT-C - cd into the selected directory fzf-cd-widget() { local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | cut -b3-"}" setopt localoptions pipefail no_aliases 2> /dev/null local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)" if [[ -z "$dir" ]]; then zle redisplay return 0 fi cd "$dir" unset dir # ensure this doesn't end up appearing in prompt expansion local ret=$? zle fzf-redraw-prompt return $ret } zle -N fzf-cd-widget bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { local selected num setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null selected=( $(fc -rl 1 | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) ) local ret=$? if [ -n "$selected" ]; then num=$selected[1] if [ -n "$num" ]; then zle vi-fetch-history -n $num fi fi zle reset-prompt return $ret } zle -N fzf-history-widget bindkey '^R' fzf-history-widget fi fzf-0.20.0/src/000077500000000000000000000000001357617647500131725ustar00rootroot00000000000000fzf-0.20.0/src/LICENSE000066400000000000000000000020701357617647500141760ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2017 Junegunn Choi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. fzf-0.20.0/src/algo/000077500000000000000000000000001357617647500141145ustar00rootroot00000000000000fzf-0.20.0/src/algo/algo.go000066400000000000000000000555001357617647500153720ustar00rootroot00000000000000package algo /* Algorithm --------- FuzzyMatchV1 finds the first "fuzzy" occurrence of the pattern within the given text in O(n) time where n is the length of the text. Once the position of the last character is located, it traverses backwards to see if there's a shorter substring that matches the pattern. a_____b___abc__ To find "abc" *-----*-----*> 1. Forward scan <*** 2. Backward scan The algorithm is simple and fast, but as it only sees the first occurrence, it is not guaranteed to find the occurrence with the highest score. a_____b__c__abc *-----*--* *** FuzzyMatchV2 implements a modified version of Smith-Waterman algorithm to find the optimal solution (highest score) according to the scoring criteria. Unlike the original algorithm, omission or mismatch of a character in the pattern is not allowed. Performance ----------- The new V2 algorithm is slower than V1 as it examines all occurrences of the pattern instead of stopping immediately after finding the first one. The time complexity of the algorithm is O(nm) if a match is found and O(n) otherwise where n is the length of the item and m is the length of the pattern. Thus, the performance overhead may not be noticeable for a query with high selectivity. However, if the performance is more important than the quality of the result, you can still choose v1 algorithm with --algo=v1. Scoring criteria ---------------- - We prefer matches at special positions, such as the start of a word, or uppercase character in camelCase words. - That is, we prefer an occurrence of the pattern with more characters matching at special positions, even if the total match length is longer. e.g. "fuzzyfinder" vs. "fuzzy-finder" on "ff" ```````````` - Also, if the first character in the pattern appears at one of the special positions, the bonus point for the position is multiplied by a constant as it is extremely likely that the first character in the typed pattern has more significance than the rest. e.g. "fo-bar" vs. "foob-r" on "br" `````` - But since fzf is still a fuzzy finder, not an acronym finder, we should also consider the total length of the matched substring. This is why we have the gap penalty. The gap penalty increases as the length of the gap (distance between the matching characters) increases, so the effect of the bonus is eventually cancelled at some point. e.g. "fuzzyfinder" vs. "fuzzy-blurry-finder" on "ff" ``````````` - Consequently, it is crucial to find the right balance between the bonus and the gap penalty. The parameters were chosen that the bonus is cancelled when the gap size increases beyond 8 characters. - The bonus mechanism can have the undesirable side effect where consecutive matches are ranked lower than the ones with gaps. e.g. "foobar" vs. "foo-bar" on "foob" ``````` - To correct this anomaly, we also give extra bonus point to each character in a consecutive matching chunk. e.g. "foobar" vs. "foo-bar" on "foob" `````` - The amount of consecutive bonus is primarily determined by the bonus of the first character in the chunk. e.g. "foobar" vs. "out-of-bound" on "oob" ```````````` */ import ( "bytes" "fmt" "strings" "unicode" "unicode/utf8" "github.com/junegunn/fzf/src/util" ) var DEBUG bool func indexAt(index int, max int, forward bool) int { if forward { return index } return max - index - 1 } // Result contains the results of running a match function. type Result struct { // TODO int32 should suffice Start int End int Score int } const ( scoreMatch = 16 scoreGapStart = -3 scoreGapExtention = -1 // We prefer matches at the beginning of a word, but the bonus should not be // too great to prevent the longer acronym matches from always winning over // shorter fuzzy matches. The bonus point here was specifically chosen that // the bonus is cancelled when the gap between the acronyms grows over // 8 characters, which is approximately the average length of the words found // in web2 dictionary and my file system. bonusBoundary = scoreMatch / 2 // Although bonus point for non-word characters is non-contextual, we need it // for computing bonus points for consecutive chunks starting with a non-word // character. bonusNonWord = scoreMatch / 2 // Edge-triggered bonus for matches in camelCase words. // Compared to word-boundary case, they don't accompany single-character gaps // (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly. bonusCamel123 = bonusBoundary + scoreGapExtention // Minimum bonus point given to characters in consecutive chunks. // Note that bonus points for consecutive matches shouldn't have needed if we // used fixed match score as in the original algorithm. bonusConsecutive = -(scoreGapStart + scoreGapExtention) // The first character in the typed pattern usually has more significance // than the rest so it's important that it appears at special positions where // bonus points are given. e.g. "to-go" vs. "ongoing" on "og" or on "ogo". // The amount of the extra bonus should be limited so that the gap penalty is // still respected. bonusFirstCharMultiplier = 2 ) type charClass int const ( charNonWord charClass = iota charLower charUpper charLetter charNumber ) func posArray(withPos bool, len int) *[]int { if withPos { pos := make([]int, 0, len) return &pos } return nil } func alloc16(offset int, slab *util.Slab, size int) (int, []int16) { if slab != nil && cap(slab.I16) > offset+size { slice := slab.I16[offset : offset+size] return offset + size, slice } return offset, make([]int16, size) } func alloc32(offset int, slab *util.Slab, size int) (int, []int32) { if slab != nil && cap(slab.I32) > offset+size { slice := slab.I32[offset : offset+size] return offset + size, slice } return offset, make([]int32, size) } func charClassOfAscii(char rune) charClass { if char >= 'a' && char <= 'z' { return charLower } else if char >= 'A' && char <= 'Z' { return charUpper } else if char >= '0' && char <= '9' { return charNumber } return charNonWord } func charClassOfNonAscii(char rune) charClass { if unicode.IsLower(char) { return charLower } else if unicode.IsUpper(char) { return charUpper } else if unicode.IsNumber(char) { return charNumber } else if unicode.IsLetter(char) { return charLetter } return charNonWord } func charClassOf(char rune) charClass { if char <= unicode.MaxASCII { return charClassOfAscii(char) } return charClassOfNonAscii(char) } func bonusFor(prevClass charClass, class charClass) int16 { if prevClass == charNonWord && class != charNonWord { // Word boundary return bonusBoundary } else if prevClass == charLower && class == charUpper || prevClass != charNumber && class == charNumber { // camelCase letter123 return bonusCamel123 } else if class == charNonWord { return bonusNonWord } return 0 } func bonusAt(input *util.Chars, idx int) int16 { if idx == 0 { return bonusBoundary } return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx))) } func normalizeRune(r rune) rune { if r < 0x00C0 || r > 0x2184 { return r } n := normalized[r] if n > 0 { return n } return r } // Algo functions make two assumptions // 1. "pattern" is given in lowercase if "caseSensitive" is false // 2. "pattern" is already normalized if "normalize" is true type Algo func(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) func trySkip(input *util.Chars, caseSensitive bool, b byte, from int) int { byteArray := input.Bytes()[from:] idx := bytes.IndexByte(byteArray, b) if idx == 0 { // Can't skip any further return from } // We may need to search for the uppercase letter again. We don't have to // consider normalization as we can be sure that this is an ASCII string. if !caseSensitive && b >= 'a' && b <= 'z' { if idx > 0 { byteArray = byteArray[:idx] } uidx := bytes.IndexByte(byteArray, b-32) if uidx >= 0 { idx = uidx } } if idx < 0 { return -1 } return from + idx } func isAscii(runes []rune) bool { for _, r := range runes { if r >= utf8.RuneSelf { return false } } return true } func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) int { // Can't determine if !input.IsBytes() { return 0 } // Not possible if !isAscii(pattern) { return -1 } firstIdx, idx := 0, 0 for pidx := 0; pidx < len(pattern); pidx++ { idx = trySkip(input, caseSensitive, byte(pattern[pidx]), idx) if idx < 0 { return -1 } if pidx == 0 && idx > 0 { // Step back to find the right bonus point firstIdx = idx - 1 } idx++ } return firstIdx } func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []int16) { width := lastIdx - int(F[0]) + 1 for i, f := range F { I := i * width if i == 0 { fmt.Print(" ") for j := int(f); j <= lastIdx; j++ { fmt.Printf(" " + string(T[j]) + " ") } fmt.Println() } fmt.Print(string(pattern[i]) + " ") for idx := int(F[0]); idx < int(f); idx++ { fmt.Print(" 0 ") } for idx := int(f); idx <= lastIdx; idx++ { fmt.Printf("%2d ", H[i*width+idx-int(F[0])]) } fmt.Println() fmt.Print(" ") for idx, p := range C[I : I+width] { if idx+int(F[0]) < int(F[i]) { p = 0 } if p > 0 { fmt.Printf("%2d ", p) } else { fmt.Print(" ") } } fmt.Println() } } func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { // Assume that pattern is given in lowercase if case-insensitive. // First check if there's a match and calculate bonus for each position. // If the input string is too long, consider finding the matching chars in // this phase as well (non-optimal alignment). M := len(pattern) if M == 0 { return Result{0, 0, 0}, posArray(withPos, M) } N := input.Length() // Since O(nm) algorithm can be prohibitively expensive for large input, // we fall back to the greedy algorithm. if slab != nil && N*M > cap(slab.I16) { return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab) } // Phase 1. Optimized search for ASCII string idx := asciiFuzzyIndex(input, pattern, caseSensitive) if idx < 0 { return Result{-1, -1, 0}, nil } // Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages offset16 := 0 offset32 := 0 offset16, H0 := alloc16(offset16, slab, N) offset16, C0 := alloc16(offset16, slab, N) // Bonus point for each position offset16, B := alloc16(offset16, slab, N) // The first occurrence of each character in the pattern offset32, F := alloc32(offset32, slab, M) // Rune array _, T := alloc32(offset32, slab, N) input.CopyRunes(T) // Phase 2. Calculate bonus for each point maxScore, maxScorePos := int16(0), 0 pidx, lastIdx := 0, 0 pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), charNonWord, false Tsub := T[idx:] H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)] for off, char := range Tsub { var class charClass if char <= unicode.MaxASCII { class = charClassOfAscii(char) if !caseSensitive && class == charUpper { char += 32 } } else { class = charClassOfNonAscii(char) if !caseSensitive && class == charUpper { char = unicode.To(unicode.LowerCase, char) } if normalize { char = normalizeRune(char) } } Tsub[off] = char bonus := bonusFor(prevClass, class) Bsub[off] = bonus prevClass = class if char == pchar { if pidx < M { F[pidx] = int32(idx + off) pidx++ pchar = pattern[util.Min(pidx, M-1)] } lastIdx = idx + off } if char == pchar0 { score := scoreMatch + bonus*bonusFirstCharMultiplier H0sub[off] = score C0sub[off] = 1 if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) { maxScore, maxScorePos = score, idx+off if forward && bonus == bonusBoundary { break } } inGap = false } else { if inGap { H0sub[off] = util.Max16(prevH0+scoreGapExtention, 0) } else { H0sub[off] = util.Max16(prevH0+scoreGapStart, 0) } C0sub[off] = 0 inGap = true } prevH0 = H0sub[off] } if pidx != M { return Result{-1, -1, 0}, nil } if M == 1 { result := Result{maxScorePos, maxScorePos + 1, int(maxScore)} if !withPos { return result, nil } pos := []int{maxScorePos} return result, &pos } // Phase 3. Fill in score matrix (H) // Unlike the original algorithm, we do not allow omission. f0 := int(F[0]) width := lastIdx - f0 + 1 offset16, H := alloc16(offset16, slab, width*M) copy(H, H0[f0:lastIdx+1]) // Possible length of consecutive chunk at each position. _, C := alloc16(offset16, slab, width*M) copy(C, C0[f0:lastIdx+1]) Fsub := F[1:] Psub := pattern[1:][:len(Fsub)] for off, f := range Fsub { f := int(f) pchar := Psub[off] pidx := off + 1 row := pidx * width inGap := false Tsub := T[f : lastIdx+1] Bsub := B[f:][:len(Tsub)] Csub := C[row+f-f0:][:len(Tsub)] Cdiag := C[row+f-f0-1-width:][:len(Tsub)] Hsub := H[row+f-f0:][:len(Tsub)] Hdiag := H[row+f-f0-1-width:][:len(Tsub)] Hleft := H[row+f-f0-1:][:len(Tsub)] Hleft[0] = 0 for off, char := range Tsub { col := off + f var s1, s2, consecutive int16 if inGap { s2 = Hleft[off] + scoreGapExtention } else { s2 = Hleft[off] + scoreGapStart } if pchar == char { s1 = Hdiag[off] + scoreMatch b := Bsub[off] consecutive = Cdiag[off] + 1 // Break consecutive chunk if b == bonusBoundary { consecutive = 1 } else if consecutive > 1 { b = util.Max16(b, util.Max16(bonusConsecutive, B[col-int(consecutive)+1])) } if s1+b < s2 { s1 += Bsub[off] consecutive = 0 } else { s1 += b } } Csub[off] = consecutive inGap = s1 < s2 score := util.Max16(util.Max16(s1, s2), 0) if pidx == M-1 && (forward && score > maxScore || !forward && score >= maxScore) { maxScore, maxScorePos = score, col } Hsub[off] = score } } if DEBUG { debugV2(T, pattern, F, lastIdx, H, C) } // Phase 4. (Optional) Backtrace to find character positions pos := posArray(withPos, M) j := f0 if withPos { i := M - 1 j = maxScorePos preferMatch := true for { I := i * width j0 := j - f0 s := H[I+j0] var s1, s2 int16 if i > 0 && j >= int(F[i]) { s1 = H[I-width+j0-1] } if j > int(F[i]) { s2 = H[I+j0-1] } if s > s1 && (s > s2 || s == s2 && preferMatch) { *pos = append(*pos, j) if i == 0 { break } i-- } preferMatch = C[I+j0] > 1 || I+width+j0+1 < len(C) && C[I+width+j0+1] > 0 j-- } } // Start offset we return here is only relevant when begin tiebreak is used. // However finding the accurate offset requires backtracking, and we don't // want to pay extra cost for the option that has lost its importance. return Result{j, maxScorePos + 1, int(maxScore)}, pos } // Implement the same sorting criteria as V2 func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) { pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0) pos := posArray(withPos, len(pattern)) prevClass := charNonWord if sidx > 0 { prevClass = charClassOf(text.Get(sidx - 1)) } for idx := sidx; idx < eidx; idx++ { char := text.Get(idx) class := charClassOf(char) if !caseSensitive { if char >= 'A' && char <= 'Z' { char += 32 } else if char > unicode.MaxASCII { char = unicode.To(unicode.LowerCase, char) } } // pattern is already normalized if normalize { char = normalizeRune(char) } if char == pattern[pidx] { if withPos { *pos = append(*pos, idx) } score += scoreMatch bonus := bonusFor(prevClass, class) if consecutive == 0 { firstBonus = bonus } else { // Break consecutive chunk if bonus == bonusBoundary { firstBonus = bonus } bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive) } if pidx == 0 { score += int(bonus * bonusFirstCharMultiplier) } else { score += int(bonus) } inGap = false consecutive++ pidx++ } else { if inGap { score += scoreGapExtention } else { score += scoreGapStart } inGap = true consecutive = 0 firstBonus = 0 } prevClass = class } return score, pos } // FuzzyMatchV1 performs fuzzy-match func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { if len(pattern) == 0 { return Result{0, 0, 0}, nil } if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 { return Result{-1, -1, 0}, nil } pidx := 0 sidx := -1 eidx := -1 lenRunes := text.Length() lenPattern := len(pattern) for index := 0; index < lenRunes; index++ { char := text.Get(indexAt(index, lenRunes, forward)) // This is considerably faster than blindly applying strings.ToLower to the // whole string if !caseSensitive { // Partially inlining `unicode.ToLower`. Ugly, but makes a noticeable // difference in CPU cost. (Measured on Go 1.4.1. Also note that the Go // compiler as of now does not inline non-leaf functions.) if char >= 'A' && char <= 'Z' { char += 32 } else if char > unicode.MaxASCII { char = unicode.To(unicode.LowerCase, char) } } if normalize { char = normalizeRune(char) } pchar := pattern[indexAt(pidx, lenPattern, forward)] if char == pchar { if sidx < 0 { sidx = index } if pidx++; pidx == lenPattern { eidx = index + 1 break } } } if sidx >= 0 && eidx >= 0 { pidx-- for index := eidx - 1; index >= sidx; index-- { tidx := indexAt(index, lenRunes, forward) char := text.Get(tidx) if !caseSensitive { if char >= 'A' && char <= 'Z' { char += 32 } else if char > unicode.MaxASCII { char = unicode.To(unicode.LowerCase, char) } } pidx_ := indexAt(pidx, lenPattern, forward) pchar := pattern[pidx_] if char == pchar { if pidx--; pidx < 0 { sidx = index break } } } if !forward { sidx, eidx = lenRunes-eidx, lenRunes-sidx } score, pos := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, withPos) return Result{sidx, eidx, score}, pos } return Result{-1, -1, 0}, nil } // ExactMatchNaive is a basic string searching algorithm that handles case // sensitivity. Although naive, it still performs better than the combination // of strings.ToLower + strings.Index for typical fzf use cases where input // strings and patterns are not very long. // // Since 0.15.0, this function searches for the match with the highest // bonus point, instead of stopping immediately after finding the first match. // The solution is much cheaper since there is only one possible alignment of // the pattern. func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { if len(pattern) == 0 { return Result{0, 0, 0}, nil } lenRunes := text.Length() lenPattern := len(pattern) if lenRunes < lenPattern { return Result{-1, -1, 0}, nil } if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 { return Result{-1, -1, 0}, nil } // For simplicity, only look at the bonus at the first character position pidx := 0 bestPos, bonus, bestBonus := -1, int16(0), int16(-1) for index := 0; index < lenRunes; index++ { index_ := indexAt(index, lenRunes, forward) char := text.Get(index_) if !caseSensitive { if char >= 'A' && char <= 'Z' { char += 32 } else if char > unicode.MaxASCII { char = unicode.To(unicode.LowerCase, char) } } if normalize { char = normalizeRune(char) } pidx_ := indexAt(pidx, lenPattern, forward) pchar := pattern[pidx_] if pchar == char { if pidx_ == 0 { bonus = bonusAt(text, index_) } pidx++ if pidx == lenPattern { if bonus > bestBonus { bestPos, bestBonus = index, bonus } if bonus == bonusBoundary { break } index -= pidx - 1 pidx, bonus = 0, 0 } } else { index -= pidx pidx, bonus = 0, 0 } } if bestPos >= 0 { var sidx, eidx int if forward { sidx = bestPos - lenPattern + 1 eidx = bestPos + 1 } else { sidx = lenRunes - (bestPos + 1) eidx = lenRunes - (bestPos - lenPattern + 1) } score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false) return Result{sidx, eidx, score}, nil } return Result{-1, -1, 0}, nil } // PrefixMatch performs prefix-match func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { if len(pattern) == 0 { return Result{0, 0, 0}, nil } if text.Length() < len(pattern) { return Result{-1, -1, 0}, nil } for index, r := range pattern { char := text.Get(index) if !caseSensitive { char = unicode.ToLower(char) } if normalize { char = normalizeRune(char) } if char != r { return Result{-1, -1, 0}, nil } } lenPattern := len(pattern) score, _ := calculateScore(caseSensitive, normalize, text, pattern, 0, lenPattern, false) return Result{0, lenPattern, score}, nil } // SuffixMatch performs suffix-match func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { lenRunes := text.Length() trimmedLen := lenRunes - text.TrailingWhitespaces() if len(pattern) == 0 { return Result{trimmedLen, trimmedLen, 0}, nil } diff := trimmedLen - len(pattern) if diff < 0 { return Result{-1, -1, 0}, nil } for index, r := range pattern { char := text.Get(index + diff) if !caseSensitive { char = unicode.ToLower(char) } if normalize { char = normalizeRune(char) } if char != r { return Result{-1, -1, 0}, nil } } lenPattern := len(pattern) sidx := trimmedLen - lenPattern eidx := trimmedLen score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false) return Result{sidx, eidx, score}, nil } // EqualMatch performs equal-match func EqualMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { lenPattern := len(pattern) if text.Length() != lenPattern { return Result{-1, -1, 0}, nil } match := true if normalize { runes := text.ToRunes() for idx, pchar := range pattern { char := runes[idx] if !caseSensitive { char = unicode.To(unicode.LowerCase, char) } if normalizeRune(pchar) != normalizeRune(char) { match = false break } } } else { runesStr := text.ToString() if !caseSensitive { runesStr = strings.ToLower(runesStr) } match = runesStr == string(pattern) } if match { return Result{0, lenPattern, (scoreMatch+bonusBoundary)*lenPattern + (bonusFirstCharMultiplier-1)*bonusBoundary}, nil } return Result{-1, -1, 0}, nil } fzf-0.20.0/src/algo/algo_test.go000066400000000000000000000174511357617647500164340ustar00rootroot00000000000000package algo import ( "math" "sort" "strings" "testing" "github.com/junegunn/fzf/src/util" ) func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) { assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score) } func assertMatch2(t *testing.T, fun Algo, caseSensitive, normalize, forward bool, input, pattern string, sidx int, eidx int, score int) { if !caseSensitive { pattern = strings.ToLower(pattern) } chars := util.ToChars([]byte(input)) res, pos := fun(caseSensitive, normalize, forward, &chars, []rune(pattern), true, nil) var start, end int if pos == nil || len(*pos) == 0 { start = res.Start end = res.End } else { sort.Ints(*pos) start = (*pos)[0] end = (*pos)[len(*pos)-1] + 1 } if start != sidx { t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", start, sidx, input, pattern) } if end != eidx { t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", end, eidx, input, pattern) } if res.Score != score { t.Errorf("Invalid score: %d (expected: %d, %s / %s)", res.Score, score, input, pattern) } } func TestFuzzyMatch(t *testing.T) { for _, fn := range []Algo{FuzzyMatchV1, FuzzyMatchV2} { for _, forward := range []bool{true, false} { assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9, scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3) assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9, scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+ bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention) assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13, scoreMatch*4+bonusCamel123+bonusConsecutive*2) assertMatch(t, fn, false, forward, "/man1/zshcompctl.1", "zshc", 6, 10, scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3) assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13, scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3+scoreGapStart) assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10, scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtention) assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10, scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtention) assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9, scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+ bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention) assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7, scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+ bonusCamel123*2+2*scoreGapStart+2*scoreGapExtention) assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8, scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary+ scoreGapStart*2+scoreGapExtention*3) assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4, scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3) assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6, scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+ bonusNonWord+bonusBoundary) assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9, scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3) assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9, scoreMatch*3+bonusBoundary*(bonusFirstCharMultiplier+2)+ scoreGapStart*2+scoreGapExtention*4) assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7, scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusCamel123*2+ scoreGapStart*2+scoreGapExtention*2) assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4, scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+ util.Max(bonusCamel123, bonusBoundary)) // Consecutive bonus updated assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6, scoreMatch*4+bonusBoundary*3) // Non-match assertMatch(t, fn, true, forward, "fooBarbaz", "oBZ", -1, -1, 0) assertMatch(t, fn, true, forward, "Foo Bar Baz", "fbb", -1, -1, 0) assertMatch(t, fn, true, forward, "fooBarbaz", "fooBarbazz", -1, -1, 0) } } } func TestFuzzyMatchBackward(t *testing.T) { assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4, scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+ scoreGapStart+scoreGapExtention) assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9, scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary) } func TestExactMatchNaive(t *testing.T) { for _, dir := range []bool{true, false} { assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1, 0) assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1, 0) assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5, scoreMatch*3+bonusCamel123+bonusConsecutive) assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13, scoreMatch*4+bonusCamel123+bonusConsecutive*2) assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10, scoreMatch*4+bonusBoundary*(bonusFirstCharMultiplier+3)) assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13, scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+4)) } } func TestExactMatchNaiveBackward(t *testing.T) { assertMatch(t, ExactMatchNaive, false, true, "foobar foob", "oo", 1, 3, scoreMatch*2+bonusConsecutive) assertMatch(t, ExactMatchNaive, false, false, "foobar foob", "oo", 8, 10, scoreMatch*2+bonusConsecutive) } func TestPrefixMatch(t *testing.T) { score := (scoreMatch+bonusBoundary)*3 + bonusBoundary*(bonusFirstCharMultiplier-1) for _, dir := range []bool{true, false} { assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0) assertMatch(t, PrefixMatch, false, dir, "fooBarBaz", "baz", -1, -1, 0) assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3, score) assertMatch(t, PrefixMatch, false, dir, "foOBarBaZ", "foo", 0, 3, score) assertMatch(t, PrefixMatch, false, dir, "f-oBarbaz", "f-o", 0, 3, score) } } func TestSuffixMatch(t *testing.T) { for _, dir := range []bool{true, false} { assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1, 0) assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1, 0) assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9, scoreMatch*3+bonusConsecutive*2) assertMatch(t, SuffixMatch, false, dir, "fooBarBaZ", "baz", 6, 9, (scoreMatch+bonusCamel123)*3+bonusCamel123*(bonusFirstCharMultiplier-1)) } } func TestEmptyPattern(t *testing.T) { for _, dir := range []bool{true, false} { assertMatch(t, FuzzyMatchV1, true, dir, "foobar", "", 0, 0, 0) assertMatch(t, FuzzyMatchV2, true, dir, "foobar", "", 0, 0, 0) assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0, 0) assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0, 0) assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6, 0) } } func TestNormalize(t *testing.T) { caseSensitive := false normalize := true forward := true test := func(input, pattern string, sidx, eidx, score int, funs ...Algo) { for _, fun := range funs { assertMatch2(t, fun, caseSensitive, normalize, forward, input, pattern, sidx, eidx, score) } } test("Só Danço Samba", "So", 0, 2, 56, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive) test("Só Danço Samba", "sodc", 0, 7, 89, FuzzyMatchV1, FuzzyMatchV2) test("Danço", "danco", 0, 5, 128, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch) } func TestLongString(t *testing.T) { bytes := make([]byte, math.MaxUint16*2) for i := range bytes { bytes[i] = 'x' } bytes[math.MaxUint16] = 'z' assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive) } fzf-0.20.0/src/algo/normalize.go000066400000000000000000000507771357617647500164630ustar00rootroot00000000000000// Normalization of latin script letters // Reference: http://www.unicode.org/Public/UCD/latest/ucd/Index.txt package algo var normalized map[rune]rune = map[rune]rune{ 0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER 0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER 0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER 0x00E2: 'a', // WITH CIRCUMFLEX, LATIN SMALL LETTER 0x00E4: 'a', // WITH DIAERESIS, LATIN SMALL LETTER 0x0227: 'a', // WITH DOT ABOVE, LATIN SMALL LETTER 0x1EA1: 'a', // WITH DOT BELOW, LATIN SMALL LETTER 0x0201: 'a', // WITH DOUBLE GRAVE, LATIN SMALL LETTER 0x00E0: 'a', // WITH GRAVE, LATIN SMALL LETTER 0x1EA3: 'a', // WITH HOOK ABOVE, LATIN SMALL LETTER 0x0203: 'a', // WITH INVERTED BREVE, LATIN SMALL LETTER 0x0101: 'a', // WITH MACRON, LATIN SMALL LETTER 0x0105: 'a', // WITH OGONEK, LATIN SMALL LETTER 0x1E9A: 'a', // WITH RIGHT HALF RING, LATIN SMALL LETTER 0x00E5: 'a', // WITH RING ABOVE, LATIN SMALL LETTER 0x1E01: 'a', // WITH RING BELOW, LATIN SMALL LETTER 0x00E3: 'a', // WITH TILDE, LATIN SMALL LETTER 0x0363: 'a', // , COMBINING LATIN SMALL LETTER 0x0250: 'a', // , LATIN SMALL LETTER TURNED 0x1E03: 'b', // WITH DOT ABOVE, LATIN SMALL LETTER 0x1E05: 'b', // WITH DOT BELOW, LATIN SMALL LETTER 0x0253: 'b', // WITH HOOK, LATIN SMALL LETTER 0x1E07: 'b', // WITH LINE BELOW, LATIN SMALL LETTER 0x0180: 'b', // WITH STROKE, LATIN SMALL LETTER 0x0183: 'b', // WITH TOPBAR, LATIN SMALL LETTER 0x0107: 'c', // WITH ACUTE, LATIN SMALL LETTER 0x010D: 'c', // WITH CARON, LATIN SMALL LETTER 0x00E7: 'c', // WITH CEDILLA, LATIN SMALL LETTER 0x0109: 'c', // WITH CIRCUMFLEX, LATIN SMALL LETTER 0x0255: 'c', // WITH CURL, LATIN SMALL LETTER 0x010B: 'c', // WITH DOT ABOVE, LATIN SMALL LETTER 0x0188: 'c', // WITH HOOK, LATIN SMALL LETTER 0x023C: 'c', // WITH STROKE, LATIN SMALL LETTER 0x0368: 'c', // , COMBINING LATIN SMALL LETTER 0x0297: 'c', // , LATIN LETTER STRETCHED 0x2184: 'c', // , LATIN SMALL LETTER REVERSED 0x010F: 'd', // WITH CARON, LATIN SMALL LETTER 0x1E11: 'd', // WITH CEDILLA, LATIN SMALL LETTER 0x1E13: 'd', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER 0x0221: 'd', // WITH CURL, LATIN SMALL LETTER 0x1E0B: 'd', // WITH DOT ABOVE, LATIN SMALL LETTER 0x1E0D: 'd', // WITH DOT BELOW, LATIN SMALL LETTER 0x0257: 'd', // WITH HOOK, LATIN SMALL LETTER 0x1E0F: 'd', // WITH LINE BELOW, LATIN SMALL LETTER 0x0111: 'd', // WITH STROKE, LATIN SMALL LETTER 0x0256: 'd', // WITH TAIL, LATIN SMALL LETTER 0x018C: 'd', // WITH TOPBAR, LATIN SMALL LETTER 0x0369: 'd', // , COMBINING LATIN SMALL LETTER 0x00E9: 'e', // WITH ACUTE, LATIN SMALL LETTER 0x0115: 'e', // WITH BREVE, LATIN SMALL LETTER 0x011B: 'e', // WITH CARON, LATIN SMALL LETTER 0x0229: 'e', // WITH CEDILLA, LATIN SMALL LETTER 0x1E19: 'e', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER 0x00EA: 'e', // WITH CIRCUMFLEX, LATIN SMALL LETTER 0x00EB: 'e', // WITH DIAERESIS, LATIN SMALL LETTER 0x0117: 'e', // WITH DOT ABOVE, LATIN SMALL LETTER 0x1EB9: 'e', // WITH DOT BELOW, LATIN SMALL LETTER 0x0205: 'e', // WITH DOUBLE GRAVE, LATIN SMALL LETTER 0x00E8: 'e', // WITH GRAVE, LATIN SMALL LETTER 0x1EBB: 'e', // WITH HOOK ABOVE, LATIN SMALL LETTER 0x025D: 'e', // WITH HOOK, LATIN SMALL LETTER REVERSED OPEN 0x0207: 'e', // WITH INVERTED BREVE, LATIN SMALL LETTER 0x0113: 'e', // WITH MACRON, LATIN SMALL LETTER 0x0119: 'e', // WITH OGONEK, LATIN SMALL LETTER 0x0247: 'e', // WITH STROKE, LATIN SMALL LETTER 0x1E1B: 'e', // WITH TILDE BELOW, LATIN SMALL LETTER 0x1EBD: 'e', // WITH TILDE, LATIN SMALL LETTER 0x0364: 'e', // , COMBINING LATIN SMALL LETTER 0x029A: 'e', // , LATIN SMALL LETTER CLOSED OPEN 0x025E: 'e', // , LATIN SMALL LETTER CLOSED REVERSED OPEN 0x025B: 'e', // , LATIN SMALL LETTER OPEN 0x0258: 'e', // , LATIN SMALL LETTER REVERSED 0x025C: 'e', // , LATIN SMALL LETTER REVERSED OPEN 0x01DD: 'e', // , LATIN SMALL LETTER TURNED 0x1D08: 'e', // , LATIN SMALL LETTER TURNED OPEN 0x1E1F: 'f', // WITH DOT ABOVE, LATIN SMALL LETTER 0x0192: 'f', // WITH HOOK, LATIN SMALL LETTER 0x01F5: 'g', // WITH ACUTE, LATIN SMALL LETTER 0x011F: 'g', // WITH BREVE, LATIN SMALL LETTER 0x01E7: 'g', // WITH CARON, LATIN SMALL LETTER 0x0123: 'g', // WITH CEDILLA, LATIN SMALL LETTER 0x011D: 'g', // WITH CIRCUMFLEX, LATIN SMALL LETTER 0x0121: 'g', // WITH DOT ABOVE, LATIN SMALL LETTER 0x0260: 'g', // WITH HOOK, LATIN SMALL LETTER 0x1E21: 'g', // WITH MACRON, LATIN SMALL LETTER 0x01E5: 'g', // WITH STROKE, LATIN SMALL LETTER 0x0261: 'g', // , LATIN SMALL LETTER SCRIPT 0x1E2B: 'h', // WITH BREVE BELOW, LATIN SMALL LETTER 0x021F: 'h', // WITH CARON, LATIN SMALL LETTER 0x1E29: 'h', // WITH CEDILLA, LATIN SMALL LETTER 0x0125: 'h', // WITH CIRCUMFLEX, LATIN SMALL LETTER 0x1E27: 'h', // WITH DIAERESIS, LATIN SMALL LETTER 0x1E23: 'h', // WITH DOT ABOVE, LATIN SMALL LETTER 0x1E25: 'h', // WITH DOT BELOW, LATIN SMALL LETTER 0x02AE: 'h', // WITH FISHHOOK, LATIN SMALL LETTER TURNED 0x0266: 'h', // WITH HOOK, LATIN SMALL LETTER 0x1E96: 'h', // WITH LINE BELOW, LATIN SMALL LETTER 0x0127: 'h', // WITH STROKE, LATIN SMALL LETTER 0x036A: 'h', // , COMBINING LATIN SMALL LETTER 0x0265: 'h', // , LATIN SMALL LETTER TURNED 0x2095: 'h', // , LATIN SUBSCRIPT SMALL LETTER 0x00ED: 'i', // WITH ACUTE, LATIN SMALL LETTER 0x012D: 'i', // WITH BREVE, LATIN SMALL LETTER 0x01D0: 'i', // WITH CARON, LATIN SMALL LETTER 0x00EE: 'i', // WITH CIRCUMFLEX, LATIN SMALL LETTER 0x00EF: 'i', // WITH DIAERESIS, LATIN SMALL LETTER 0x1ECB: 'i', // WITH DOT BELOW, LATIN SMALL LETTER 0x0209: 'i', // WITH DOUBLE GRAVE, LATIN SMALL LETTER 0x00EC: 'i', // WITH GRAVE, LATIN SMALL LETTER 0x1EC9: 'i', // WITH HOOK ABOVE, LATIN SMALL LETTER 0x020B: 'i', // WITH INVERTED BREVE, LATIN SMALL LETTER 0x012B: 'i', // WITH MACRON, LATIN SMALL LETTER 0x012F: 'i', // WITH OGONEK, LATIN SMALL LETTER 0x0268: 'i', // WITH STROKE, LATIN SMALL LETTER 0x1E2D: 'i', // WITH TILDE BELOW, LATIN SMALL LETTER 0x0129: 'i', // WITH TILDE, LATIN SMALL LETTER 0x0365: 'i', // , COMBINING LATIN SMALL LETTER 0x0131: 'i', // , LATIN SMALL LETTER DOTLESS 0x1D09: 'i', // , LATIN SMALL LETTER TURNED 0x1D62: 'i', // , LATIN SUBSCRIPT SMALL LETTER 0x2071: 'i', // , SUPERSCRIPT LATIN SMALL LETTER 0x01F0: 'j', // WITH CARON, LATIN SMALL LETTER 0x0135: 'j', // WITH CIRCUMFLEX, LATIN SMALL LETTER 0x029D: 'j', // WITH CROSSED-TAIL, LATIN SMALL LETTER 0x0249: 'j', // WITH STROKE, LATIN SMALL LETTER 0x025F: 'j', // WITH STROKE, LATIN SMALL LETTER DOTLESS 0x0237: 'j', // , LATIN SMALL LETTER DOTLESS 0x1E31: 'k', // WITH ACUTE, LATIN SMALL LETTER 0x01E9: 'k', // WITH CARON, LATIN SMALL LETTER 0x0137: 'k', // WITH CEDILLA, LATIN SMALL LETTER 0x1E33: 'k', // WITH DOT BELOW, LATIN SMALL LETTER 0x0199: 'k', // WITH HOOK, LATIN SMALL LETTER 0x1E35: 'k', // WITH LINE BELOW, LATIN SMALL LETTER 0x029E: 'k', // , LATIN SMALL LETTER TURNED 0x2096: 'k', // , LATIN SUBSCRIPT SMALL LETTER 0x013A: 'l', // WITH ACUTE, LATIN SMALL LETTER 0x019A: 'l', // WITH BAR, LATIN SMALL LETTER 0x026C: 'l', // WITH BELT, LATIN SMALL LETTER 0x013E: 'l', // WITH CARON, LATIN SMALL LETTER 0x013C: 'l', // WITH CEDILLA, LATIN SMALL LETTER 0x1E3D: 'l', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER 0x0234: 'l', // WITH CURL, LATIN SMALL LETTER 0x1E37: 'l', // WITH DOT BELOW, LATIN SMALL LETTER 0x1E3B: 'l', // WITH LINE BELOW, LATIN SMALL LETTER 0x0140: 'l', // WITH MIDDLE DOT, LATIN SMALL LETTER 0x026B: 'l', // WITH MIDDLE TILDE, LATIN SMALL LETTER 0x026D: 'l', // WITH RETROFLEX HOOK, LATIN SMALL LETTER 0x0142: 'l', // WITH STROKE, LATIN SMALL LETTER 0x2097: 'l', // , LATIN SUBSCRIPT SMALL LETTER 0x1E3F: 'm', // WITH ACUTE, LATIN SMALL LETTER 0x1E41: 'm', // WITH DOT ABOVE, LATIN SMALL LETTER 0x1E43: 'm', // WITH DOT BELOW, LATIN SMALL LETTER 0x0271: 'm', // WITH HOOK, LATIN SMALL LETTER 0x0270: 'm', // WITH LONG LEG, LATIN SMALL LETTER TURNED 0x036B: 'm', // , COMBINING LATIN SMALL LETTER 0x1D1F: 'm', // , LATIN SMALL LETTER SIDEWAYS TURNED 0x026F: 'm', // , LATIN SMALL LETTER TURNED 0x2098: 'm', // , LATIN SUBSCRIPT SMALL LETTER 0x0144: 'n', // WITH ACUTE, LATIN SMALL LETTER 0x0148: 'n', // WITH CARON, LATIN SMALL LETTER 0x0146: 'n', // WITH CEDILLA, LATIN SMALL LETTER 0x1E4B: 'n', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER 0x0235: 'n', // WITH CURL, LATIN SMALL LETTER 0x1E45: 'n', // WITH DOT ABOVE, LATIN SMALL LETTER 0x1E47: 'n', // WITH DOT BELOW, LATIN SMALL LETTER 0x01F9: 'n', // WITH GRAVE, LATIN SMALL LETTER 0x0272: 'n', // WITH LEFT HOOK, LATIN SMALL LETTER 0x1E49: 'n', // WITH LINE BELOW, LATIN SMALL LETTER 0x019E: 'n', // WITH LONG RIGHT LEG, LATIN SMALL LETTER 0x0273: 'n', // WITH RETROFLEX HOOK, LATIN SMALL LETTER 0x00F1: 'n', // WITH TILDE, LATIN SMALL LETTER 0x2099: 'n', // , LATIN SUBSCRIPT SMALL LETTER 0x00F3: 'o', // WITH ACUTE, LATIN SMALL LETTER 0x014F: 'o', // WITH BREVE, LATIN SMALL LETTER 0x01D2: 'o', // WITH CARON, LATIN SMALL LETTER 0x00F4: 'o', // WITH CIRCUMFLEX, LATIN SMALL LETTER 0x00F6: 'o', // WITH DIAERESIS, LATIN SMALL LETTER 0x022F: 'o', // WITH DOT ABOVE, LATIN SMALL LETTER 0x1ECD: 'o', // WITH DOT BELOW, LATIN SMALL LETTER 0x0151: 'o', // WITH DOUBLE ACUTE, LATIN SMALL LETTER 0x020D: 'o', // WITH DOUBLE GRAVE, LATIN SMALL LETTER 0x00F2: 'o', // WITH GRAVE, LATIN SMALL LETTER 0x1ECF: 'o', // WITH HOOK ABOVE, LATIN SMALL LETTER 0x01A1: 'o', // WITH HORN, LATIN SMALL LETTER 0x020F: 'o', // WITH INVERTED BREVE, LATIN SMALL LETTER 0x014D: 'o', // WITH MACRON, LATIN SMALL LETTER 0x01EB: 'o', // WITH OGONEK, LATIN SMALL LETTER 0x00F8: 'o', // WITH STROKE, LATIN SMALL LETTER 0x1D13: 'o', // WITH STROKE, LATIN SMALL LETTER SIDEWAYS 0x00F5: 'o', // WITH TILDE, LATIN SMALL LETTER 0x0366: 'o', // , COMBINING LATIN SMALL LETTER 0x0275: 'o', // , LATIN SMALL LETTER BARRED 0x1D17: 'o', // , LATIN SMALL LETTER BOTTOM HALF 0x0254: 'o', // , LATIN SMALL LETTER OPEN 0x1D11: 'o', // , LATIN SMALL LETTER SIDEWAYS 0x1D12: 'o', // , LATIN SMALL LETTER SIDEWAYS OPEN 0x1D16: 'o', // , LATIN SMALL LETTER TOP HALF 0x1E55: 'p', // WITH ACUTE, LATIN SMALL LETTER 0x1E57: 'p', // WITH DOT ABOVE, LATIN SMALL LETTER 0x01A5: 'p', // WITH HOOK, LATIN SMALL LETTER 0x209A: 'p', // , LATIN SUBSCRIPT SMALL LETTER 0x024B: 'q', // WITH HOOK TAIL, LATIN SMALL LETTER 0x02A0: 'q', // WITH HOOK, LATIN SMALL LETTER 0x0155: 'r', // WITH ACUTE, LATIN SMALL LETTER 0x0159: 'r', // WITH CARON, LATIN SMALL LETTER 0x0157: 'r', // WITH CEDILLA, LATIN SMALL LETTER 0x1E59: 'r', // WITH DOT ABOVE, LATIN SMALL LETTER 0x1E5B: 'r', // WITH DOT BELOW, LATIN SMALL LETTER 0x0211: 'r', // WITH DOUBLE GRAVE, LATIN SMALL LETTER 0x027E: 'r', // WITH FISHHOOK, LATIN SMALL LETTER 0x027F: 'r', // WITH FISHHOOK, LATIN SMALL LETTER REVERSED 0x027B: 'r', // WITH HOOK, LATIN SMALL LETTER TURNED 0x0213: 'r', // WITH INVERTED BREVE, LATIN SMALL LETTER 0x1E5F: 'r', // WITH LINE BELOW, LATIN SMALL LETTER 0x027C: 'r', // WITH LONG LEG, LATIN SMALL LETTER 0x027A: 'r', // WITH LONG LEG, LATIN SMALL LETTER TURNED 0x024D: 'r', // WITH STROKE, LATIN SMALL LETTER 0x027D: 'r', // WITH TAIL, LATIN SMALL LETTER 0x036C: 'r', // , COMBINING LATIN SMALL LETTER 0x0279: 'r', // , LATIN SMALL LETTER TURNED 0x1D63: 'r', // , LATIN SUBSCRIPT SMALL LETTER 0x015B: 's', // WITH ACUTE, LATIN SMALL LETTER 0x0161: 's', // WITH CARON, LATIN SMALL LETTER 0x015F: 's', // WITH CEDILLA, LATIN SMALL LETTER 0x015D: 's', // WITH CIRCUMFLEX, LATIN SMALL LETTER 0x0219: 's', // WITH COMMA BELOW, LATIN SMALL LETTER 0x1E61: 's', // WITH DOT ABOVE, LATIN SMALL LETTER 0x1E9B: 's', // WITH DOT ABOVE, LATIN SMALL LETTER LONG 0x1E63: 's', // WITH DOT BELOW, LATIN SMALL LETTER 0x0282: 's', // WITH HOOK, LATIN SMALL LETTER 0x023F: 's', // WITH SWASH TAIL, LATIN SMALL LETTER 0x017F: 's', // , LATIN SMALL LETTER LONG 0x00DF: 's', // , LATIN SMALL LETTER SHARP 0x209B: 's', // , LATIN SUBSCRIPT SMALL LETTER 0x0165: 't', // WITH CARON, LATIN SMALL LETTER 0x0163: 't', // WITH CEDILLA, LATIN SMALL LETTER 0x1E71: 't', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER 0x021B: 't', // WITH COMMA BELOW, LATIN SMALL LETTER 0x0236: 't', // WITH CURL, LATIN SMALL LETTER 0x1E97: 't', // WITH DIAERESIS, LATIN SMALL LETTER 0x1E6B: 't', // WITH DOT ABOVE, LATIN SMALL LETTER 0x1E6D: 't', // WITH DOT BELOW, LATIN SMALL LETTER 0x01AD: 't', // WITH HOOK, LATIN SMALL LETTER 0x1E6F: 't', // WITH LINE BELOW, LATIN SMALL LETTER 0x01AB: 't', // WITH PALATAL HOOK, LATIN SMALL LETTER 0x0288: 't', // WITH RETROFLEX HOOK, LATIN SMALL LETTER 0x0167: 't', // WITH STROKE, LATIN SMALL LETTER 0x036D: 't', // , COMBINING LATIN SMALL LETTER 0x0287: 't', // , LATIN SMALL LETTER TURNED 0x209C: 't', // , LATIN SUBSCRIPT SMALL LETTER 0x0289: 'u', // BAR, LATIN SMALL LETTER 0x00FA: 'u', // WITH ACUTE, LATIN SMALL LETTER 0x016D: 'u', // WITH BREVE, LATIN SMALL LETTER 0x01D4: 'u', // WITH CARON, LATIN SMALL LETTER 0x1E77: 'u', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER 0x00FB: 'u', // WITH CIRCUMFLEX, LATIN SMALL LETTER 0x1E73: 'u', // WITH DIAERESIS BELOW, LATIN SMALL LETTER 0x00FC: 'u', // WITH DIAERESIS, LATIN SMALL LETTER 0x1EE5: 'u', // WITH DOT BELOW, LATIN SMALL LETTER 0x0171: 'u', // WITH DOUBLE ACUTE, LATIN SMALL LETTER 0x0215: 'u', // WITH DOUBLE GRAVE, LATIN SMALL LETTER 0x00F9: 'u', // WITH GRAVE, LATIN SMALL LETTER 0x1EE7: 'u', // WITH HOOK ABOVE, LATIN SMALL LETTER 0x01B0: 'u', // WITH HORN, LATIN SMALL LETTER 0x0217: 'u', // WITH INVERTED BREVE, LATIN SMALL LETTER 0x016B: 'u', // WITH MACRON, LATIN SMALL LETTER 0x0173: 'u', // WITH OGONEK, LATIN SMALL LETTER 0x016F: 'u', // WITH RING ABOVE, LATIN SMALL LETTER 0x1E75: 'u', // WITH TILDE BELOW, LATIN SMALL LETTER 0x0169: 'u', // WITH TILDE, LATIN SMALL LETTER 0x0367: 'u', // , COMBINING LATIN SMALL LETTER 0x1D1D: 'u', // , LATIN SMALL LETTER SIDEWAYS 0x1D1E: 'u', // , LATIN SMALL LETTER SIDEWAYS DIAERESIZED 0x1D64: 'u', // , LATIN SUBSCRIPT SMALL LETTER 0x1E7F: 'v', // WITH DOT BELOW, LATIN SMALL LETTER 0x028B: 'v', // WITH HOOK, LATIN SMALL LETTER 0x1E7D: 'v', // WITH TILDE, LATIN SMALL LETTER 0x036E: 'v', // , COMBINING LATIN SMALL LETTER 0x028C: 'v', // , LATIN SMALL LETTER TURNED 0x1D65: 'v', // , LATIN SUBSCRIPT SMALL LETTER 0x1E83: 'w', // WITH ACUTE, LATIN SMALL LETTER 0x0175: 'w', // WITH CIRCUMFLEX, LATIN SMALL LETTER 0x1E85: 'w', // WITH DIAERESIS, LATIN SMALL LETTER 0x1E87: 'w', // WITH DOT ABOVE, LATIN SMALL LETTER 0x1E89: 'w', // WITH DOT BELOW, LATIN SMALL LETTER 0x1E81: 'w', // WITH GRAVE, LATIN SMALL LETTER 0x1E98: 'w', // WITH RING ABOVE, LATIN SMALL LETTER 0x028D: 'w', // , LATIN SMALL LETTER TURNED 0x1E8D: 'x', // WITH DIAERESIS, LATIN SMALL LETTER 0x1E8B: 'x', // WITH DOT ABOVE, LATIN SMALL LETTER 0x036F: 'x', // , COMBINING LATIN SMALL LETTER 0x00FD: 'y', // WITH ACUTE, LATIN SMALL LETTER 0x0177: 'y', // WITH CIRCUMFLEX, LATIN SMALL LETTER 0x00FF: 'y', // WITH DIAERESIS, LATIN SMALL LETTER 0x1E8F: 'y', // WITH DOT ABOVE, LATIN SMALL LETTER 0x1EF5: 'y', // WITH DOT BELOW, LATIN SMALL LETTER 0x1EF3: 'y', // WITH GRAVE, LATIN SMALL LETTER 0x1EF7: 'y', // WITH HOOK ABOVE, LATIN SMALL LETTER 0x01B4: 'y', // WITH HOOK, LATIN SMALL LETTER 0x0233: 'y', // WITH MACRON, LATIN SMALL LETTER 0x1E99: 'y', // WITH RING ABOVE, LATIN SMALL LETTER 0x024F: 'y', // WITH STROKE, LATIN SMALL LETTER 0x1EF9: 'y', // WITH TILDE, LATIN SMALL LETTER 0x028E: 'y', // , LATIN SMALL LETTER TURNED 0x017A: 'z', // WITH ACUTE, LATIN SMALL LETTER 0x017E: 'z', // WITH CARON, LATIN SMALL LETTER 0x1E91: 'z', // WITH CIRCUMFLEX, LATIN SMALL LETTER 0x0291: 'z', // WITH CURL, LATIN SMALL LETTER 0x017C: 'z', // WITH DOT ABOVE, LATIN SMALL LETTER 0x1E93: 'z', // WITH DOT BELOW, LATIN SMALL LETTER 0x0225: 'z', // WITH HOOK, LATIN SMALL LETTER 0x1E95: 'z', // WITH LINE BELOW, LATIN SMALL LETTER 0x0290: 'z', // WITH RETROFLEX HOOK, LATIN SMALL LETTER 0x01B6: 'z', // WITH STROKE, LATIN SMALL LETTER 0x0240: 'z', // WITH SWASH TAIL, LATIN SMALL LETTER 0x0251: 'a', // , latin small letter script 0x00C1: 'A', // WITH ACUTE, LATIN CAPITAL LETTER 0x00C2: 'A', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER 0x00C4: 'A', // WITH DIAERESIS, LATIN CAPITAL LETTER 0x00C0: 'A', // WITH GRAVE, LATIN CAPITAL LETTER 0x00C5: 'A', // WITH RING ABOVE, LATIN CAPITAL LETTER 0x023A: 'A', // WITH STROKE, LATIN CAPITAL LETTER 0x00C3: 'A', // WITH TILDE, LATIN CAPITAL LETTER 0x1D00: 'A', // , LATIN LETTER SMALL CAPITAL 0x0181: 'B', // WITH HOOK, LATIN CAPITAL LETTER 0x0243: 'B', // WITH STROKE, LATIN CAPITAL LETTER 0x0299: 'B', // , LATIN LETTER SMALL CAPITAL 0x1D03: 'B', // , LATIN LETTER SMALL CAPITAL BARRED 0x00C7: 'C', // WITH CEDILLA, LATIN CAPITAL LETTER 0x023B: 'C', // WITH STROKE, LATIN CAPITAL LETTER 0x1D04: 'C', // , LATIN LETTER SMALL CAPITAL 0x018A: 'D', // WITH HOOK, LATIN CAPITAL LETTER 0x0189: 'D', // , LATIN CAPITAL LETTER AFRICAN 0x1D05: 'D', // , LATIN LETTER SMALL CAPITAL 0x00C9: 'E', // WITH ACUTE, LATIN CAPITAL LETTER 0x00CA: 'E', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER 0x00CB: 'E', // WITH DIAERESIS, LATIN CAPITAL LETTER 0x00C8: 'E', // WITH GRAVE, LATIN CAPITAL LETTER 0x0246: 'E', // WITH STROKE, LATIN CAPITAL LETTER 0x0190: 'E', // , LATIN CAPITAL LETTER OPEN 0x018E: 'E', // , LATIN CAPITAL LETTER REVERSED 0x1D07: 'E', // , LATIN LETTER SMALL CAPITAL 0x0193: 'G', // WITH HOOK, LATIN CAPITAL LETTER 0x029B: 'G', // WITH HOOK, LATIN LETTER SMALL CAPITAL 0x0262: 'G', // , LATIN LETTER SMALL CAPITAL 0x029C: 'H', // , LATIN LETTER SMALL CAPITAL 0x00CD: 'I', // WITH ACUTE, LATIN CAPITAL LETTER 0x00CE: 'I', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER 0x00CF: 'I', // WITH DIAERESIS, LATIN CAPITAL LETTER 0x0130: 'I', // WITH DOT ABOVE, LATIN CAPITAL LETTER 0x00CC: 'I', // WITH GRAVE, LATIN CAPITAL LETTER 0x0197: 'I', // WITH STROKE, LATIN CAPITAL LETTER 0x026A: 'I', // , LATIN LETTER SMALL CAPITAL 0x0248: 'J', // WITH STROKE, LATIN CAPITAL LETTER 0x1D0A: 'J', // , LATIN LETTER SMALL CAPITAL 0x1D0B: 'K', // , LATIN LETTER SMALL CAPITAL 0x023D: 'L', // WITH BAR, LATIN CAPITAL LETTER 0x1D0C: 'L', // WITH STROKE, LATIN LETTER SMALL CAPITAL 0x029F: 'L', // , LATIN LETTER SMALL CAPITAL 0x019C: 'M', // , LATIN CAPITAL LETTER TURNED 0x1D0D: 'M', // , LATIN LETTER SMALL CAPITAL 0x019D: 'N', // WITH LEFT HOOK, LATIN CAPITAL LETTER 0x0220: 'N', // WITH LONG RIGHT LEG, LATIN CAPITAL LETTER 0x00D1: 'N', // WITH TILDE, LATIN CAPITAL LETTER 0x0274: 'N', // , LATIN LETTER SMALL CAPITAL 0x1D0E: 'N', // , LATIN LETTER SMALL CAPITAL REVERSED 0x00D3: 'O', // WITH ACUTE, LATIN CAPITAL LETTER 0x00D4: 'O', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER 0x00D6: 'O', // WITH DIAERESIS, LATIN CAPITAL LETTER 0x00D2: 'O', // WITH GRAVE, LATIN CAPITAL LETTER 0x019F: 'O', // WITH MIDDLE TILDE, LATIN CAPITAL LETTER 0x00D8: 'O', // WITH STROKE, LATIN CAPITAL LETTER 0x00D5: 'O', // WITH TILDE, LATIN CAPITAL LETTER 0x0186: 'O', // , LATIN CAPITAL LETTER OPEN 0x1D0F: 'O', // , LATIN LETTER SMALL CAPITAL 0x1D10: 'O', // , LATIN LETTER SMALL CAPITAL OPEN 0x1D18: 'P', // , LATIN LETTER SMALL CAPITAL 0x024A: 'Q', // WITH HOOK TAIL, LATIN CAPITAL LETTER SMALL 0x024C: 'R', // WITH STROKE, LATIN CAPITAL LETTER 0x0280: 'R', // , LATIN LETTER SMALL CAPITAL 0x0281: 'R', // , LATIN LETTER SMALL CAPITAL INVERTED 0x1D19: 'R', // , LATIN LETTER SMALL CAPITAL REVERSED 0x1D1A: 'R', // , LATIN LETTER SMALL CAPITAL TURNED 0x023E: 'T', // WITH DIAGONAL STROKE, LATIN CAPITAL LETTER 0x01AE: 'T', // WITH RETROFLEX HOOK, LATIN CAPITAL LETTER 0x1D1B: 'T', // , LATIN LETTER SMALL CAPITAL 0x0244: 'U', // BAR, LATIN CAPITAL LETTER 0x00DA: 'U', // WITH ACUTE, LATIN CAPITAL LETTER 0x00DB: 'U', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER 0x00DC: 'U', // WITH DIAERESIS, LATIN CAPITAL LETTER 0x00D9: 'U', // WITH GRAVE, LATIN CAPITAL LETTER 0x1D1C: 'U', // , LATIN LETTER SMALL CAPITAL 0x01B2: 'V', // WITH HOOK, LATIN CAPITAL LETTER 0x0245: 'V', // , LATIN CAPITAL LETTER TURNED 0x1D20: 'V', // , LATIN LETTER SMALL CAPITAL 0x1D21: 'W', // , LATIN LETTER SMALL CAPITAL 0x00DD: 'Y', // WITH ACUTE, LATIN CAPITAL LETTER 0x0178: 'Y', // WITH DIAERESIS, LATIN CAPITAL LETTER 0x024E: 'Y', // WITH STROKE, LATIN CAPITAL LETTER 0x028F: 'Y', // , LATIN LETTER SMALL CAPITAL 0x1D22: 'Z', // , LATIN LETTER SMALL CAPITAL } // NormalizeRunes normalizes latin script letters func NormalizeRunes(runes []rune) []rune { ret := make([]rune, len(runes)) copy(ret, runes) for idx, r := range runes { if r < 0x00C0 || r > 0x2184 { continue } n := normalized[r] if n > 0 { ret[idx] = normalized[r] } } return ret } fzf-0.20.0/src/ansi.go000066400000000000000000000135301357617647500144550ustar00rootroot00000000000000package fzf import ( "bytes" "regexp" "strconv" "strings" "unicode/utf8" "github.com/junegunn/fzf/src/tui" ) type ansiOffset struct { offset [2]int32 color ansiState } type ansiState struct { fg tui.Color bg tui.Color attr tui.Attr } func (s *ansiState) colored() bool { return s.fg != -1 || s.bg != -1 || s.attr > 0 } func (s *ansiState) equals(t *ansiState) bool { if t == nil { return !s.colored() } return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr } func (s *ansiState) ToString() string { if !s.colored() { return "" } ret := "" if s.attr&tui.Bold > 0 { ret += "1;" } if s.attr&tui.Dim > 0 { ret += "2;" } if s.attr&tui.Italic > 0 { ret += "3;" } if s.attr&tui.Underline > 0 { ret += "4;" } if s.attr&tui.Blink > 0 { ret += "5;" } if s.attr&tui.Reverse > 0 { ret += "7;" } ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40) return "\x1b[" + strings.TrimSuffix(ret, ";") + "m" } func toAnsiString(color tui.Color, offset int) string { col := int(color) ret := "" if col == -1 { ret += strconv.Itoa(offset + 9) } else if col < 8 { ret += strconv.Itoa(offset + col) } else if col < 16 { ret += strconv.Itoa(offset - 30 + 90 + col - 8) } else if col < 256 { ret += strconv.Itoa(offset+8) + ";5;" + strconv.Itoa(col) } else if col >= (1 << 24) { r := strconv.Itoa((col >> 16) & 0xff) g := strconv.Itoa((col >> 8) & 0xff) b := strconv.Itoa(col & 0xff) ret += strconv.Itoa(offset+8) + ";2;" + r + ";" + g + ";" + b } return ret + ";" } var ansiRegex *regexp.Regexp func init() { /* References: - https://github.com/gnachman/iTerm2 - http://ascii-table.com/ansi-escape-sequences.php - http://ascii-table.com/ansi-escape-sequences-vt-100.php - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html */ // The following regular expression will include not all but most of the // frequently used ANSI sequences ansiRegex = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x0e\x0f]|.\x08)") } func findAnsiStart(str string) int { idx := 0 for ; idx < len(str); idx++ { b := str[idx] if b == 0x1b || b == 0x0e || b == 0x0f { return idx } if b == 0x08 && idx > 0 { return idx - 1 } } return idx } func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) { var offsets []ansiOffset var output bytes.Buffer if state != nil { offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state}) } prevIdx := 0 runeCount := 0 for idx := 0; idx < len(str); { idx += findAnsiStart(str[idx:]) if idx == len(str) { break } // Make sure that we found an ANSI code offset := ansiRegex.FindStringIndex(str[idx:]) if len(offset) < 2 { idx++ continue } offset[0] += idx offset[1] += idx idx = offset[1] // Check if we should continue prev := str[prevIdx:offset[0]] if proc != nil && !proc(prev, state) { return "", nil, nil } prevIdx = offset[1] runeCount += utf8.RuneCountInString(prev) output.WriteString(prev) newState := interpretCode(str[offset[0]:offset[1]], state) if !newState.equals(state) { if state != nil { // Update last offset (&offsets[len(offsets)-1]).offset[1] = int32(runeCount) } if newState.colored() { // Append new offset state = newState offsets = append(offsets, ansiOffset{[2]int32{int32(runeCount), int32(runeCount)}, *state}) } else { // Discard state state = nil } } } var rest string var trimmed string if prevIdx == 0 { // No ANSI code found rest = str trimmed = str } else { rest = str[prevIdx:] output.WriteString(rest) trimmed = output.String() } if len(rest) > 0 && state != nil { // Update last offset runeCount += utf8.RuneCountInString(rest) (&offsets[len(offsets)-1]).offset[1] = int32(runeCount) } if proc != nil { proc(rest, state) } if len(offsets) == 0 { return trimmed, nil, state } return trimmed, &offsets, state } func interpretCode(ansiCode string, prevState *ansiState) *ansiState { // State var state *ansiState if prevState == nil { state = &ansiState{-1, -1, 0} } else { state = &ansiState{prevState.fg, prevState.bg, prevState.attr} } if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' { return state } ptr := &state.fg state256 := 0 init := func() { state.fg = -1 state.bg = -1 state.attr = 0 state256 = 0 } ansiCode = ansiCode[2 : len(ansiCode)-1] if len(ansiCode) == 0 { init() } for _, code := range strings.Split(ansiCode, ";") { if num, err := strconv.Atoi(code); err == nil { switch state256 { case 0: switch num { case 38: ptr = &state.fg state256++ case 48: ptr = &state.bg state256++ case 39: state.fg = -1 case 49: state.bg = -1 case 1: state.attr = state.attr | tui.Bold case 2: state.attr = state.attr | tui.Dim case 3: state.attr = state.attr | tui.Italic case 4: state.attr = state.attr | tui.Underline case 5: state.attr = state.attr | tui.Blink case 7: state.attr = state.attr | tui.Reverse case 0: init() default: if num >= 30 && num <= 37 { state.fg = tui.Color(num - 30) } else if num >= 40 && num <= 47 { state.bg = tui.Color(num - 40) } else if num >= 90 && num <= 97 { state.fg = tui.Color(num - 90 + 8) } else if num >= 100 && num <= 107 { state.bg = tui.Color(num - 100 + 8) } } case 1: switch num { case 2: state256 = 10 // MAGIC case 5: state256++ default: state256 = 0 } case 2: *ptr = tui.Color(num) state256 = 0 case 10: *ptr = tui.Color(1<<24) | tui.Color(num<<16) state256++ case 11: *ptr = *ptr | tui.Color(num<<8) state256++ case 12: *ptr = *ptr | tui.Color(num) state256 = 0 } } } if state256 > 0 { *ptr = -1 } return state } fzf-0.20.0/src/ansi_test.go000066400000000000000000000113411357617647500155120ustar00rootroot00000000000000package fzf import ( "fmt" "strings" "testing" "github.com/junegunn/fzf/src/tui" ) func TestExtractColor(t *testing.T) { assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) { var attr tui.Attr if bold { attr = tui.Bold } if offset.offset[0] != b || offset.offset[1] != e || offset.color.fg != fg || offset.color.bg != bg || offset.color.attr != attr { t.Error(offset, b, e, fg, bg, attr) } } src := "hello world" var state *ansiState clean := "\x1b[0m" check := func(assertion func(ansiOffsets *[]ansiOffset, state *ansiState)) { output, ansiOffsets, newState := extractColor(src, state, nil) state = newState if output != "hello world" { t.Errorf("Invalid output: %s %v", output, []rune(output)) } fmt.Println(src, ansiOffsets, clean) assertion(ansiOffsets, state) } check(func(offsets *[]ansiOffset, state *ansiState) { if offsets != nil { t.Fail() } }) state = nil src = "\x1b[0mhello world" check(func(offsets *[]ansiOffset, state *ansiState) { if offsets != nil { t.Fail() } }) state = nil src = "\x1b[1mhello world" check(func(offsets *[]ansiOffset, state *ansiState) { if len(*offsets) != 1 { t.Fail() } assert((*offsets)[0], 0, 11, -1, -1, true) }) state = nil src = "\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d" check(func(offsets *[]ansiOffset, state *ansiState) { if len(*offsets) != 1 { t.Fail() } assert((*offsets)[0], 0, 6, -1, -1, true) }) state = nil src = "\x1b[1mhello \x1b[Kworld" check(func(offsets *[]ansiOffset, state *ansiState) { if len(*offsets) != 1 { t.Fail() } assert((*offsets)[0], 0, 11, -1, -1, true) }) state = nil src = "hello \x1b[34;45;1mworld" check(func(offsets *[]ansiOffset, state *ansiState) { if len(*offsets) != 1 { t.Fail() } assert((*offsets)[0], 6, 11, 4, 5, true) }) state = nil src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld" check(func(offsets *[]ansiOffset, state *ansiState) { if len(*offsets) != 1 { t.Fail() } assert((*offsets)[0], 6, 11, 4, 5, true) }) state = nil src = "hello \x1b[34;45;1mwor\x1b[0mld" check(func(offsets *[]ansiOffset, state *ansiState) { if len(*offsets) != 1 { t.Fail() } assert((*offsets)[0], 6, 9, 4, 5, true) }) state = nil src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md" check(func(offsets *[]ansiOffset, state *ansiState) { if len(*offsets) != 3 { t.Fail() } assert((*offsets)[0], 6, 8, 4, 233, true) assert((*offsets)[1], 8, 9, 161, 233, true) assert((*offsets)[2], 10, 11, 161, -1, false) }) // {38,48};5;{38,48} state = nil src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md" check(func(offsets *[]ansiOffset, state *ansiState) { if len(*offsets) != 2 { t.Fail() } assert((*offsets)[0], 6, 9, 38, 48, true) assert((*offsets)[1], 9, 10, 48, 38, true) }) src = "hello \x1b[32;1mworld" check(func(offsets *[]ansiOffset, state *ansiState) { if len(*offsets) != 1 { t.Fail() } if state.fg != 2 || state.bg != -1 || state.attr == 0 { t.Fail() } assert((*offsets)[0], 6, 11, 2, -1, true) }) src = "hello world" check(func(offsets *[]ansiOffset, state *ansiState) { if len(*offsets) != 1 { t.Fail() } if state.fg != 2 || state.bg != -1 || state.attr == 0 { t.Fail() } assert((*offsets)[0], 0, 11, 2, -1, true) }) src = "hello \x1b[0;38;5;200;48;5;100mworld" check(func(offsets *[]ansiOffset, state *ansiState) { if len(*offsets) != 2 { t.Fail() } if state.fg != 200 || state.bg != 100 || state.attr > 0 { t.Fail() } assert((*offsets)[0], 0, 6, 2, -1, true) assert((*offsets)[1], 6, 11, 200, 100, false) }) } func TestAnsiCodeStringConversion(t *testing.T) { assert := func(code string, prevState *ansiState, expected string) { state := interpretCode(code, prevState) if expected != state.ToString() { t.Errorf("expected: %s, actual: %s", strings.Replace(expected, "\x1b[", "\\x1b[", -1), strings.Replace(state.ToString(), "\x1b[", "\\x1b[", -1)) } } assert("\x1b[m", nil, "") assert("\x1b[m", &ansiState{attr: tui.Blink}, "") assert("\x1b[31m", nil, "\x1b[31;49m") assert("\x1b[41m", nil, "\x1b[39;41m") assert("\x1b[92m", nil, "\x1b[92;49m") assert("\x1b[102m", nil, "\x1b[39;102m") assert("\x1b[31m", &ansiState{fg: 4, bg: 4}, "\x1b[31;44m") assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, attr: tui.Reverse}, "\x1b[1;2;7;31;49m") assert("\x1b[38;5;100;48;5;200m", nil, "\x1b[38;5;100;48;5;200m") assert("\x1b[48;5;100;38;5;200m", nil, "\x1b[38;5;200;48;5;100m") assert("\x1b[48;5;100;38;2;10;20;30;1m", nil, "\x1b[1;38;2;10;20;30;48;5;100m") assert("\x1b[48;5;100;38;2;10;20;30;7m", &ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1}, "\x1b[2;3;7;38;2;10;20;30;48;5;100m") } fzf-0.20.0/src/cache.go000066400000000000000000000031231357617647500145630ustar00rootroot00000000000000package fzf import "sync" // queryCache associates strings to lists of items type queryCache map[string][]Result // ChunkCache associates Chunk and query string to lists of items type ChunkCache struct { mutex sync.Mutex cache map[*Chunk]*queryCache } // NewChunkCache returns a new ChunkCache func NewChunkCache() ChunkCache { return ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)} } // Add adds the list to the cache func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) { if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax { return } cc.mutex.Lock() defer cc.mutex.Unlock() qc, ok := cc.cache[chunk] if !ok { cc.cache[chunk] = &queryCache{} qc = cc.cache[chunk] } (*qc)[key] = list } // Lookup is called to lookup ChunkCache func (cc *ChunkCache) Lookup(chunk *Chunk, key string) []Result { if len(key) == 0 || !chunk.IsFull() { return nil } cc.mutex.Lock() defer cc.mutex.Unlock() qc, ok := cc.cache[chunk] if ok { list, ok := (*qc)[key] if ok { return list } } return nil } func (cc *ChunkCache) Search(chunk *Chunk, key string) []Result { if len(key) == 0 || !chunk.IsFull() { return nil } cc.mutex.Lock() defer cc.mutex.Unlock() qc, ok := cc.cache[chunk] if !ok { return nil } for idx := 1; idx < len(key); idx++ { // [---------| ] | [ |---------] // [--------| ] | [ |--------] // [-------| ] | [ |-------] prefix := key[:len(key)-idx] suffix := key[idx:] for _, substr := range [2]string{prefix, suffix} { if cached, found := (*qc)[substr]; found { return cached } } } return nil } fzf-0.20.0/src/cache_test.go000066400000000000000000000015551357617647500156310ustar00rootroot00000000000000package fzf import "testing" func TestChunkCache(t *testing.T) { cache := NewChunkCache() chunk1p := &Chunk{} chunk2p := &Chunk{count: chunkSize} items1 := []Result{Result{}} items2 := []Result{Result{}, Result{}} cache.Add(chunk1p, "foo", items1) cache.Add(chunk2p, "foo", items1) cache.Add(chunk2p, "bar", items2) { // chunk1 is not full cached := cache.Lookup(chunk1p, "foo") if cached != nil { t.Error("Cached disabled for non-empty chunks", cached) } } { cached := cache.Lookup(chunk2p, "foo") if cached == nil || len(cached) != 1 { t.Error("Expected 1 item cached", cached) } } { cached := cache.Lookup(chunk2p, "bar") if cached == nil || len(cached) != 2 { t.Error("Expected 2 items cached", cached) } } { cached := cache.Lookup(chunk1p, "foobar") if cached != nil { t.Error("Expected 0 item cached", cached) } } } fzf-0.20.0/src/chunklist.go000066400000000000000000000034161357617647500155310ustar00rootroot00000000000000package fzf import "sync" // Chunk is a list of Items whose size has the upper limit of chunkSize type Chunk struct { items [chunkSize]Item count int } // ItemBuilder is a closure type that builds Item object from byte array type ItemBuilder func(*Item, []byte) bool // ChunkList is a list of Chunks type ChunkList struct { chunks []*Chunk mutex sync.Mutex trans ItemBuilder } // NewChunkList returns a new ChunkList func NewChunkList(trans ItemBuilder) *ChunkList { return &ChunkList{ chunks: []*Chunk{}, mutex: sync.Mutex{}, trans: trans} } func (c *Chunk) push(trans ItemBuilder, data []byte) bool { if trans(&c.items[c.count], data) { c.count++ return true } return false } // IsFull returns true if the Chunk is full func (c *Chunk) IsFull() bool { return c.count == chunkSize } func (cl *ChunkList) lastChunk() *Chunk { return cl.chunks[len(cl.chunks)-1] } // CountItems returns the total number of Items func CountItems(cs []*Chunk) int { if len(cs) == 0 { return 0 } return chunkSize*(len(cs)-1) + cs[len(cs)-1].count } // Push adds the item to the list func (cl *ChunkList) Push(data []byte) bool { cl.mutex.Lock() if len(cl.chunks) == 0 || cl.lastChunk().IsFull() { cl.chunks = append(cl.chunks, &Chunk{}) } ret := cl.lastChunk().push(cl.trans, data) cl.mutex.Unlock() return ret } // Clear clears the data func (cl *ChunkList) Clear() { cl.mutex.Lock() cl.chunks = nil cl.mutex.Unlock() } // Snapshot returns immutable snapshot of the ChunkList func (cl *ChunkList) Snapshot() ([]*Chunk, int) { cl.mutex.Lock() ret := make([]*Chunk, len(cl.chunks)) copy(ret, cl.chunks) // Duplicate the last chunk if cnt := len(ret); cnt > 0 { newChunk := *ret[cnt-1] ret[cnt-1] = &newChunk } cl.mutex.Unlock() return ret, CountItems(ret) } fzf-0.20.0/src/chunklist_test.go000066400000000000000000000035061357617647500165700ustar00rootroot00000000000000package fzf import ( "fmt" "testing" "github.com/junegunn/fzf/src/util" ) func TestChunkList(t *testing.T) { // FIXME global sortCriteria = []criterion{byScore, byLength} cl := NewChunkList(func(item *Item, s []byte) bool { item.text = util.ToChars(s) return true }) // Snapshot snapshot, count := cl.Snapshot() if len(snapshot) > 0 || count > 0 { t.Error("Snapshot should be empty now") } // Add some data cl.Push([]byte("hello")) cl.Push([]byte("world")) // Previously created snapshot should remain the same if len(snapshot) > 0 { t.Error("Snapshot should not have changed") } // But the new snapshot should contain the added items snapshot, count = cl.Snapshot() if len(snapshot) != 1 && count != 2 { t.Error("Snapshot should not be empty now") } // Check the content of the ChunkList chunk1 := snapshot[0] if chunk1.count != 2 { t.Error("Snapshot should contain only two items") } if chunk1.items[0].text.ToString() != "hello" || chunk1.items[1].text.ToString() != "world" { t.Error("Invalid data") } if chunk1.IsFull() { t.Error("Chunk should not have been marked full yet") } // Add more data for i := 0; i < chunkSize*2; i++ { cl.Push([]byte(fmt.Sprintf("item %d", i))) } // Previous snapshot should remain the same if len(snapshot) != 1 { t.Error("Snapshot should stay the same") } // New snapshot snapshot, count = cl.Snapshot() if len(snapshot) != 3 || !snapshot[0].IsFull() || !snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 { t.Error("Expected two full chunks and one more chunk") } if snapshot[2].count != 2 { t.Error("Unexpected number of items") } cl.Push([]byte("hello")) cl.Push([]byte("world")) lastChunkCount := snapshot[len(snapshot)-1].count if lastChunkCount != 2 { t.Error("Unexpected number of items:", lastChunkCount) } } fzf-0.20.0/src/constants.go000066400000000000000000000042701357617647500155400ustar00rootroot00000000000000package fzf import ( "math" "os" "time" "github.com/junegunn/fzf/src/util" ) const ( // Current version version = "0.20.0" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayStep time.Duration = 10 * time.Millisecond // Reader readerBufferSize = 64 * 1024 readerPollIntervalMin = 10 * time.Millisecond readerPollIntervalStep = 5 * time.Millisecond readerPollIntervalMax = 50 * time.Millisecond // Terminal initialDelay = 20 * time.Millisecond initialDelayTac = 100 * time.Millisecond spinnerDuration = 200 * time.Millisecond previewCancelWait = 500 * time.Millisecond maxPatternLength = 300 maxMulti = math.MaxInt32 // Matcher numPartitionsMultiplier = 8 maxPartitions = 32 progressMinDuration = 200 * time.Millisecond // Capacity of each chunk chunkSize int = 100 // Pre-allocated memory slices to minimize GC slab16Size int = 100 * 1024 // 200KB * 32 = 12.8MB slab32Size int = 2048 // 8KB * 32 = 256KB // Do not cache results of low selectivity queries queryCacheMax int = chunkSize / 5 // Not to cache mergers with large lists mergerCacheMax int = 100000 // History defaultHistoryMax int = 1000 // Jump labels defaultJumpLabels string = "asdfghjklqwertyuiopzxcvbnm1234567890ASDFGHJKLQWERTYUIOPZXCVBNM`~;:,<.>/?'\"!@#$%^&*()[{]}-_=+" ) var defaultCommand string func init() { if !util.IsWindows() { defaultCommand = `set -o pipefail; command find -L . -mindepth 1 \( -path '*/\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-` } else if os.Getenv("TERM") == "cygwin" { defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"` } else { defaultCommand = `for /r %P in (*) do @(set "_curfile=%P" & set "_curfile=!_curfile:%__CD__%=!" & echo !_curfile!)` } } // fzf events const ( EvtReadNew util.EventType = iota EvtReadFin EvtSearchNew EvtSearchProgress EvtSearchFin EvtHeader EvtReady ) const ( exitCancel = -1 exitOk = 0 exitNoMatch = 1 exitError = 2 exitInterrupt = 130 ) fzf-0.20.0/src/core.go000066400000000000000000000217621357617647500144610ustar00rootroot00000000000000/* Package fzf implements fzf, a command-line fuzzy finder. The MIT License (MIT) Copyright (c) 2017 Junegunn Choi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package fzf import ( "fmt" "os" "time" "github.com/junegunn/fzf/src/util" ) /* Reader -> EvtReadFin Reader -> EvtReadNew -> Matcher (restart) Terminal -> EvtSearchNew:bool -> Matcher (restart) Matcher -> EvtSearchProgress -> Terminal (update info) Matcher -> EvtSearchFin -> Terminal (update list) Matcher -> EvtHeader -> Terminal (update header) */ // Run starts fzf func Run(opts *Options, revision string) { sort := opts.Sort > 0 sortCriteria = opts.Criteria if opts.Version { if len(revision) > 0 { fmt.Printf("%s (%s)\n", version, revision) } else { fmt.Println(version) } os.Exit(exitOk) } // Event channel eventBox := util.NewEventBox() // ANSI code processor ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) { return util.ToChars(data), nil } var lineAnsiState, prevLineAnsiState *ansiState if opts.Ansi { if opts.Theme != nil { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { prevLineAnsiState = lineAnsiState trimmed, offsets, newState := extractColor(string(data), lineAnsiState, nil) lineAnsiState = newState return util.ToChars([]byte(trimmed)), offsets } } else { // When color is disabled but ansi option is given, // we simply strip out ANSI codes from the input ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { trimmed, _, _ := extractColor(string(data), nil, nil) return util.ToChars([]byte(trimmed)), nil } } } // Chunk list var chunkList *ChunkList var itemIndex int32 header := make([]string, 0, opts.HeaderLines) if len(opts.WithNth) == 0 { chunkList = NewChunkList(func(item *Item, data []byte) bool { if len(header) < opts.HeaderLines { header = append(header, string(data)) eventBox.Set(EvtHeader, header) return false } item.text, item.colors = ansiProcessor(data) item.text.Index = itemIndex itemIndex++ return true }) } else { chunkList = NewChunkList(func(item *Item, data []byte) bool { tokens := Tokenize(string(data), opts.Delimiter) if opts.Ansi && opts.Theme != nil && len(tokens) > 1 { var ansiState *ansiState if prevLineAnsiState != nil { ansiStateDup := *prevLineAnsiState ansiState = &ansiStateDup } for _, token := range tokens { prevAnsiState := ansiState _, _, ansiState = extractColor(token.text.ToString(), ansiState, nil) if prevAnsiState != nil { token.text.Prepend("\x1b[m" + prevAnsiState.ToString()) } else { token.text.Prepend("\x1b[m") } } } trans := Transform(tokens, opts.WithNth) transformed := joinTokens(trans) if len(header) < opts.HeaderLines { header = append(header, transformed) eventBox.Set(EvtHeader, header) return false } item.text, item.colors = ansiProcessor([]byte(transformed)) item.text.TrimTrailingWhitespaces() item.text.Index = itemIndex item.origText = &data itemIndex++ return true }) } // Reader streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync var reader *Reader if !streamingFilter { reader = NewReader(func(data []byte) bool { return chunkList.Push(data) }, eventBox, opts.ReadZero, opts.Filter == nil) go reader.ReadSource() } // Matcher forward := true for _, cri := range opts.Criteria[1:] { if cri == byEnd { forward = false break } if cri == byBegin { break } } patternBuilder := func(runes []rune) *Pattern { return BuildPattern( opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, opts.Filter == nil, opts.Nth, opts.Delimiter, runes) } matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) // Filtering mode if opts.Filter != nil { if opts.PrintQuery { opts.Printer(*opts.Filter) } pattern := patternBuilder([]rune(*opts.Filter)) matcher.sort = pattern.sortable found := false if streamingFilter { slab := util.MakeSlab(slab16Size, slab32Size) reader := NewReader( func(runes []byte) bool { item := Item{} if chunkList.trans(&item, runes) { if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil { opts.Printer(item.text.ToString()) found = true } } return false }, eventBox, opts.ReadZero, false) reader.ReadSource() } else { eventBox.Unwatch(EvtReadNew) eventBox.WaitFor(EvtReadFin) snapshot, _ := chunkList.Snapshot() merger, _ := matcher.scan(MatchRequest{ chunks: snapshot, pattern: pattern}) for i := 0; i < merger.Length(); i++ { opts.Printer(merger.Get(i).item.AsString(opts.Ansi)) found = true } } if found { os.Exit(exitOk) } os.Exit(exitNoMatch) } // Synchronous search if opts.Sync { eventBox.Unwatch(EvtReadNew) eventBox.WaitFor(EvtReadFin) } // Go interactive go matcher.Loop() // Terminal I/O terminal := NewTerminal(opts, eventBox) deferred := opts.Select1 || opts.Exit0 go terminal.Loop() if !deferred { terminal.startChan <- true } // Event coordination reading := true clearCache := util.Once(false) clearSelection := util.Once(false) ticks := 0 var nextCommand *string restart := func(command string) { reading = true clearCache = util.Once(true) clearSelection = util.Once(true) chunkList.Clear() header = make([]string, 0, opts.HeaderLines) go reader.restart(command) } eventBox.Watch(EvtReadNew) for { delay := true ticks++ input := func() []rune { if opts.Phony { return []rune{} } return []rune(terminal.Input()) } eventBox.Wait(func(events *util.Events) { if _, fin := (*events)[EvtReadFin]; fin { delete(*events, EvtReadNew) } for evt, value := range *events { switch evt { case EvtReadNew, EvtReadFin: if evt == EvtReadFin && nextCommand != nil { restart(*nextCommand) nextCommand = nil break } else { reading = reading && evt == EvtReadNew } snapshot, count := chunkList.Snapshot() terminal.UpdateCount(count, !reading, value.(*string)) if opts.Sync { opts.Sync = false terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false) } matcher.Reset(snapshot, input(), false, !reading, sort, clearCache()) case EvtSearchNew: var command *string switch val := value.(type) { case searchRequest: sort = val.sort command = val.command } if command != nil { if reading { reader.terminate() nextCommand = command } else { restart(*command) } break } snapshot, _ := chunkList.Snapshot() matcher.Reset(snapshot, input(), true, !reading, sort, clearCache()) delay = false case EvtSearchProgress: switch val := value.(type) { case float32: terminal.UpdateProgress(val) } case EvtHeader: headerPadded := make([]string, opts.HeaderLines) copy(headerPadded, value.([]string)) terminal.UpdateHeader(headerPadded) case EvtSearchFin: switch val := value.(type) { case *Merger: if deferred { count := val.Length() if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 { deferred = false terminal.startChan <- true } else if val.final { if opts.Exit0 && count == 0 || opts.Select1 && count == 1 { if opts.PrintQuery { opts.Printer(opts.Query) } if len(opts.Expect) > 0 { opts.Printer("") } for i := 0; i < count; i++ { opts.Printer(val.Get(i).item.AsString(opts.Ansi)) } if count > 0 { os.Exit(exitOk) } os.Exit(exitNoMatch) } deferred = false terminal.startChan <- true } } terminal.UpdateList(val, clearSelection()) } } } events.Clear() }) if delay && reading { dur := util.DurWithin( time.Duration(ticks)*coordinatorDelayStep, 0, coordinatorDelayMax) time.Sleep(dur) } } } fzf-0.20.0/src/history.go000066400000000000000000000040571357617647500152300ustar00rootroot00000000000000package fzf import ( "errors" "io/ioutil" "os" "strings" ) // History struct represents input history type History struct { path string lines []string modified map[int]string maxSize int cursor int } // NewHistory returns the pointer to a new History struct func NewHistory(path string, maxSize int) (*History, error) { fmtError := func(e error) error { if os.IsPermission(e) { return errors.New("permission denied: " + path) } return errors.New("invalid history file: " + e.Error()) } // Read history file data, err := ioutil.ReadFile(path) if err != nil { // If it doesn't exist, check if we can create a file with the name if os.IsNotExist(err) { data = []byte{} if err := ioutil.WriteFile(path, data, 0600); err != nil { return nil, fmtError(err) } } else { return nil, fmtError(err) } } // Split lines and limit the maximum number of lines lines := strings.Split(strings.Trim(string(data), "\n"), "\n") if len(lines[len(lines)-1]) > 0 { lines = append(lines, "") } return &History{ path: path, maxSize: maxSize, lines: lines, modified: make(map[int]string), cursor: len(lines) - 1}, nil } func (h *History) append(line string) error { // We don't append empty lines if len(line) == 0 { return nil } lines := append(h.lines[:len(h.lines)-1], line) if len(lines) > h.maxSize { lines = lines[len(lines)-h.maxSize:] } h.lines = append(lines, "") return ioutil.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600) } func (h *History) override(str string) { // You can update the history but they're not written to the file if h.cursor == len(h.lines)-1 { h.lines[h.cursor] = str } else if h.cursor < len(h.lines)-1 { h.modified[h.cursor] = str } } func (h *History) current() string { if str, prs := h.modified[h.cursor]; prs { return str } return h.lines[h.cursor] } func (h *History) previous() string { if h.cursor > 0 { h.cursor-- } return h.current() } func (h *History) next() string { if h.cursor < len(h.lines)-1 { h.cursor++ } return h.current() } fzf-0.20.0/src/history_test.go000066400000000000000000000031401357617647500162570ustar00rootroot00000000000000package fzf import ( "io/ioutil" "os" "os/user" "runtime" "testing" ) func TestHistory(t *testing.T) { maxHistory := 50 // Invalid arguments user, _ := user.Current() var paths []string if runtime.GOOS == "windows" { // GOPATH should exist, so we shouldn't be able to override it paths = []string{os.Getenv("GOPATH")} } else { paths = []string{"/etc", "/proc"} if user.Name != "root" { paths = append(paths, "/etc/sudoers") } } for _, path := range paths { if _, e := NewHistory(path, maxHistory); e == nil { t.Error("Error expected for: " + path) } } f, _ := ioutil.TempFile("", "fzf-history") f.Close() { // Append lines h, _ := NewHistory(f.Name(), maxHistory) for i := 0; i < maxHistory+10; i++ { h.append("foobar") } } { // Read lines h, _ := NewHistory(f.Name(), maxHistory) if len(h.lines) != maxHistory+1 { t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) } for i := 0; i < maxHistory; i++ { if h.lines[i] != "foobar" { t.Error("Expected: foobar, actual: " + h.lines[i]) } } } { // Append lines h, _ := NewHistory(f.Name(), maxHistory) h.append("barfoo") h.append("") h.append("foobarbaz") } { // Read lines again h, _ := NewHistory(f.Name(), maxHistory) if len(h.lines) != maxHistory+1 { t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) } compare := func(idx int, exp string) { if h.lines[idx] != exp { t.Errorf("Expected: %s, actual: %s\n", exp, h.lines[idx]) } } compare(maxHistory-3, "foobar") compare(maxHistory-2, "barfoo") compare(maxHistory-1, "foobarbaz") } } fzf-0.20.0/src/item.go000066400000000000000000000016751357617647500144700ustar00rootroot00000000000000package fzf import ( "github.com/junegunn/fzf/src/util" ) // Item represents each input line. 56 bytes. type Item struct { text util.Chars // 32 = 24 + 1 + 1 + 2 + 4 transformed *[]Token // 8 origText *[]byte // 8 colors *[]ansiOffset // 8 } // Index returns ordinal index of the Item func (item *Item) Index() int32 { return item.text.Index } var minItem = Item{text: util.Chars{Index: -1}} func (item *Item) TrimLength() uint16 { return item.text.TrimLength() } // Colors returns ansiOffsets of the Item func (item *Item) Colors() []ansiOffset { if item.colors == nil { return []ansiOffset{} } return *item.colors } // AsString returns the original string func (item *Item) AsString(stripAnsi bool) string { if item.origText != nil { if stripAnsi { trimmed, _, _ := extractColor(string(*item.origText), nil, nil) return trimmed } return string(*item.origText) } return item.text.ToString() } fzf-0.20.0/src/item_test.go000066400000000000000000000007441357617647500155230ustar00rootroot00000000000000package fzf import ( "testing" "github.com/junegunn/fzf/src/util" ) func TestStringPtr(t *testing.T) { orig := []byte("\x1b[34mfoo") text := []byte("\x1b[34mbar") item := Item{origText: &orig, text: util.ToChars(text)} if item.AsString(true) != "foo" || item.AsString(false) != string(orig) { t.Fail() } if item.AsString(true) != "foo" { t.Fail() } item.origText = nil if item.AsString(true) != string(text) || item.AsString(false) != string(text) { t.Fail() } } fzf-0.20.0/src/matcher.go000066400000000000000000000122221357617647500151430ustar00rootroot00000000000000package fzf import ( "fmt" "runtime" "sort" "sync" "time" "github.com/junegunn/fzf/src/util" ) // MatchRequest represents a search request type MatchRequest struct { chunks []*Chunk pattern *Pattern final bool sort bool clearCache bool } // Matcher is responsible for performing search type Matcher struct { patternBuilder func([]rune) *Pattern sort bool tac bool eventBox *util.EventBox reqBox *util.EventBox partitions int slab []*util.Slab mergerCache map[string]*Merger } const ( reqRetry util.EventType = iota reqReset ) // NewMatcher returns a new Matcher func NewMatcher(patternBuilder func([]rune) *Pattern, sort bool, tac bool, eventBox *util.EventBox) *Matcher { partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions) return &Matcher{ patternBuilder: patternBuilder, sort: sort, tac: tac, eventBox: eventBox, reqBox: util.NewEventBox(), partitions: partitions, slab: make([]*util.Slab, partitions), mergerCache: make(map[string]*Merger)} } // Loop puts Matcher in action func (m *Matcher) Loop() { prevCount := 0 for { var request MatchRequest m.reqBox.Wait(func(events *util.Events) { for _, val := range *events { switch val := val.(type) { case MatchRequest: request = val default: panic(fmt.Sprintf("Unexpected type: %T", val)) } } events.Clear() }) if request.sort != m.sort || request.clearCache { m.sort = request.sort m.mergerCache = make(map[string]*Merger) clearChunkCache() } // Restart search patternString := request.pattern.AsString() var merger *Merger cancelled := false count := CountItems(request.chunks) foundCache := false if count == prevCount { // Look up mergerCache if cached, found := m.mergerCache[patternString]; found { foundCache = true merger = cached } } else { // Invalidate mergerCache prevCount = count m.mergerCache = make(map[string]*Merger) } if !foundCache { merger, cancelled = m.scan(request) } if !cancelled { if merger.cacheable() { m.mergerCache[patternString] = merger } merger.final = request.final m.eventBox.Set(EvtSearchFin, merger) } } } func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk { partitions := m.partitions perSlice := len(chunks) / partitions if perSlice == 0 { partitions = len(chunks) perSlice = 1 } slices := make([][]*Chunk, partitions) for i := 0; i < partitions; i++ { start := i * perSlice end := start + perSlice if i == partitions-1 { end = len(chunks) } slices[i] = chunks[start:end] } return slices } type partialResult struct { index int matches []Result } func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { startedAt := time.Now() numChunks := len(request.chunks) if numChunks == 0 { return EmptyMerger, false } pattern := request.pattern if pattern.IsEmpty() { return PassMerger(&request.chunks, m.tac), false } cancelled := util.NewAtomicBool(false) slices := m.sliceChunks(request.chunks) numSlices := len(slices) resultChan := make(chan partialResult, numSlices) countChan := make(chan int, numChunks) waitGroup := sync.WaitGroup{} for idx, chunks := range slices { waitGroup.Add(1) if m.slab[idx] == nil { m.slab[idx] = util.MakeSlab(slab16Size, slab32Size) } go func(idx int, slab *util.Slab, chunks []*Chunk) { defer func() { waitGroup.Done() }() count := 0 allMatches := make([][]Result, len(chunks)) for idx, chunk := range chunks { matches := request.pattern.Match(chunk, slab) allMatches[idx] = matches count += len(matches) if cancelled.Get() { return } countChan <- len(matches) } sliceMatches := make([]Result, 0, count) for _, matches := range allMatches { sliceMatches = append(sliceMatches, matches...) } if m.sort { if m.tac { sort.Sort(ByRelevanceTac(sliceMatches)) } else { sort.Sort(ByRelevance(sliceMatches)) } } resultChan <- partialResult{idx, sliceMatches} }(idx, m.slab[idx], chunks) } wait := func() bool { cancelled.Set(true) waitGroup.Wait() return true } count := 0 matchCount := 0 for matchesInChunk := range countChan { count++ matchCount += matchesInChunk if count == numChunks { break } if m.reqBox.Peek(reqReset) { return nil, wait() } if time.Since(startedAt) > progressMinDuration { m.eventBox.Set(EvtSearchProgress, float32(count)/float32(numChunks)) } } partialResults := make([][]Result, numSlices) for range slices { partialResult := <-resultChan partialResults[partialResult.index] = partialResult.matches } return NewMerger(pattern, partialResults, m.sort, m.tac), false } // Reset is called to interrupt/signal the ongoing search func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, clearCache bool) { pattern := m.patternBuilder(patternRunes) var event util.EventType if cancel { event = reqReset } else { event = reqRetry } m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, clearCache}) } fzf-0.20.0/src/merger.go000066400000000000000000000050211357617647500150000ustar00rootroot00000000000000package fzf import "fmt" // EmptyMerger is a Merger with no data var EmptyMerger = NewMerger(nil, [][]Result{}, false, false) // Merger holds a set of locally sorted lists of items and provides the view of // a single, globally-sorted list type Merger struct { pattern *Pattern lists [][]Result merged []Result chunks *[]*Chunk cursors []int sorted bool tac bool final bool count int } // PassMerger returns a new Merger that simply returns the items in the // original order func PassMerger(chunks *[]*Chunk, tac bool) *Merger { mg := Merger{ pattern: nil, chunks: chunks, tac: tac, count: 0} for _, chunk := range *mg.chunks { mg.count += chunk.count } return &mg } // NewMerger returns a new Merger func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merger { mg := Merger{ pattern: pattern, lists: lists, merged: []Result{}, chunks: nil, cursors: make([]int, len(lists)), sorted: sorted, tac: tac, final: false, count: 0} for _, list := range mg.lists { mg.count += len(list) } return &mg } // Length returns the number of items func (mg *Merger) Length() int { return mg.count } // Get returns the pointer to the Result object indexed by the given integer func (mg *Merger) Get(idx int) Result { if mg.chunks != nil { if mg.tac { idx = mg.count - idx - 1 } chunk := (*mg.chunks)[idx/chunkSize] return Result{item: &chunk.items[idx%chunkSize]} } if mg.sorted { return mg.mergedGet(idx) } if mg.tac { idx = mg.count - idx - 1 } for _, list := range mg.lists { numItems := len(list) if idx < numItems { return list[idx] } idx -= numItems } panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count)) } func (mg *Merger) cacheable() bool { return mg.count < mergerCacheMax } func (mg *Merger) mergedGet(idx int) Result { for i := len(mg.merged); i <= idx; i++ { minRank := minRank() minIdx := -1 for listIdx, list := range mg.lists { cursor := mg.cursors[listIdx] if cursor < 0 || cursor == len(list) { mg.cursors[listIdx] = -1 continue } if cursor >= 0 { rank := list[cursor] if minIdx < 0 || compareRanks(rank, minRank, mg.tac) { minRank = rank minIdx = listIdx } } mg.cursors[listIdx] = cursor } if minIdx >= 0 { chosen := mg.lists[minIdx] mg.merged = append(mg.merged, chosen[mg.cursors[minIdx]]) mg.cursors[minIdx]++ } else { panic(fmt.Sprintf("Index out of bounds (sorted, %d/%d)", i, mg.count)) } } return mg.merged[idx] } fzf-0.20.0/src/merger_test.go000066400000000000000000000036601357617647500160460ustar00rootroot00000000000000package fzf import ( "fmt" "math/rand" "sort" "testing" "github.com/junegunn/fzf/src/util" ) func assert(t *testing.T, cond bool, msg ...string) { if !cond { t.Error(msg) } } func randResult() Result { str := fmt.Sprintf("%d", rand.Uint32()) chars := util.ToChars([]byte(str)) chars.Index = rand.Int31() return Result{item: &Item{text: chars}} } func TestEmptyMerger(t *testing.T) { assert(t, EmptyMerger.Length() == 0, "Not empty") assert(t, EmptyMerger.count == 0, "Invalid count") assert(t, len(EmptyMerger.lists) == 0, "Invalid lists") assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list") } func buildLists(partiallySorted bool) ([][]Result, []Result) { numLists := 4 lists := make([][]Result, numLists) cnt := 0 for i := 0; i < numLists; i++ { numResults := rand.Int() % 20 cnt += numResults lists[i] = make([]Result, numResults) for j := 0; j < numResults; j++ { item := randResult() lists[i][j] = item } if partiallySorted { sort.Sort(ByRelevance(lists[i])) } } items := []Result{} for _, list := range lists { items = append(items, list...) } return lists, items } func TestMergerUnsorted(t *testing.T) { lists, items := buildLists(false) cnt := len(items) // Not sorted: same order mg := NewMerger(nil, lists, false, false) assert(t, cnt == mg.Length(), "Invalid Length") for i := 0; i < cnt; i++ { assert(t, items[i] == mg.Get(i), "Invalid Get") } } func TestMergerSorted(t *testing.T) { lists, items := buildLists(true) cnt := len(items) // Sorted sorted order mg := NewMerger(nil, lists, true, false) assert(t, cnt == mg.Length(), "Invalid Length") sort.Sort(ByRelevance(items)) for i := 0; i < cnt; i++ { if items[i] != mg.Get(i) { t.Error("Not sorted", items[i], mg.Get(i)) } } // Inverse order mg2 := NewMerger(nil, lists, true, false) for i := cnt - 1; i >= 0; i-- { if items[i] != mg2.Get(i) { t.Error("Not sorted", items[i], mg2.Get(i)) } } } fzf-0.20.0/src/options.go000066400000000000000000001104121357617647500152130ustar00rootroot00000000000000package fzf import ( "fmt" "os" "regexp" "strconv" "strings" "unicode/utf8" "github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" "github.com/mattn/go-shellwords" ) const usage = `usage: fzf [options] Search -x, --extended Extended-search mode (enabled by default; +x or --no-extended to disable) -e, --exact Enable Exact-match --algo=TYPE Fuzzy matching algorithm: [v1|v2] (default: v2) -i Case-insensitive match (default: smart-case match) +i Case-sensitive match --literal Do not normalize latin script letters before matching -n, --nth=N[,..] Comma-separated list of field index expressions for limiting search scope. Each can be a non-zero integer or a range expression ([BEGIN]..[END]). --with-nth=N[,..] Transform the presentation of each line using field index expressions -d, --delimiter=STR Field delimiter regex (default: AWK-style) +s, --no-sort Do not sort the result --tac Reverse the order of the input --phony Do not perform search --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply when the scores are tied [length|begin|end|index] (default: length) Interface -m, --multi[=MAX] Enable multi-select with tab/shift-tab --no-mouse Disable mouse --bind=KEYBINDS Custom key bindings. Refer to the man page. --cycle Enable cyclic scroll --no-hscroll Disable horizontal scroll --hscroll-off=COL Number of screen columns to keep to the right of the highlighted substring (default: 10) --filepath-word Make word-wise movements respect path separators --jump-labels=CHARS Label characters for jump and jump-accept Layout --height=HEIGHT[%] Display fzf window below the cursor with the given height instead of using fullscreen --min-height=HEIGHT Minimum height when --height is given in percent (default: 10) --layout=LAYOUT Choose layout: [default|reverse|reverse-list] --border Draw border above and below the finder --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L) --info=STYLE Finder info style [default|inline|hidden] --prompt=STR Input prompt (default: '> ') --header=STR String to print as header --header-lines=N The first N lines of the input are treated as header Display --ansi Enable processing of ANSI color codes --tabstop=SPACES Number of spaces for a tab character (default: 8) --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors --no-bold Do not use bold text History --history=FILE History file --history-size=N Maximum number of history entries (default: 1000) Preview --preview=COMMAND Command to preview highlighted line ({}) --preview-window=OPT Preview window layout (default: right:50%) [up|down|left|right][:SIZE[%]][:wrap][:hidden] Scripting -q, --query=STR Start the finder with the given query -1, --select-1 Automatically select the only match -0, --exit-0 Exit immediately when there's no match -f, --filter=STR Filter mode. Do not start interactive finder. --print-query Print query as the first line --expect=KEYS Comma-separated list of keys to complete fzf --read0 Read input delimited by ASCII NUL characters --print0 Print output delimited by ASCII NUL characters --sync Synchronous search for multi-staged filtering --version Display version information and exit Environment variables FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_OPTS Default options (e.g. '--layout=reverse --inline-info') ` // Case denotes case-sensitivity of search type Case int // Case-sensitivities const ( CaseSmart Case = iota CaseIgnore CaseRespect ) // Sort criteria type criterion int const ( byScore criterion = iota byLength byBegin byEnd ) type sizeSpec struct { size float64 percent bool } func defaultMargin() [4]sizeSpec { return [4]sizeSpec{} } type windowPosition int const ( posUp windowPosition = iota posDown posLeft posRight ) type layoutType int const ( layoutDefault layoutType = iota layoutReverse layoutReverseList ) type infoStyle int const ( infoDefault infoStyle = iota infoInline infoHidden ) type previewOpts struct { command string position windowPosition size sizeSpec hidden bool wrap bool border bool } // Options stores the values of command-line options type Options struct { Fuzzy bool FuzzyAlgo algo.Algo Extended bool Phony bool Case Case Normalize bool Nth []Range WithNth []Range Delimiter Delimiter Sort int Tac bool Criteria []criterion Multi int Ansi bool Mouse bool Theme *tui.ColorTheme Black bool Bold bool Height sizeSpec MinHeight int Layout layoutType Cycle bool Hscroll bool HscrollOff int FileWord bool InfoStyle infoStyle JumpLabels string Prompt string Query string Select1 bool Exit0 bool Filter *string ToggleSort bool Expect map[int]string Keymap map[int][]action Preview previewOpts PrintQuery bool ReadZero bool Printer func(string) PrintSep string Sync bool History *History Header []string HeaderLines int Margin [4]sizeSpec Bordered bool Unicode bool Tabstop int ClearOnExit bool Version bool } func defaultOptions() *Options { return &Options{ Fuzzy: true, FuzzyAlgo: algo.FuzzyMatchV2, Extended: true, Phony: false, Case: CaseSmart, Normalize: true, Nth: make([]Range, 0), WithNth: make([]Range, 0), Delimiter: Delimiter{}, Sort: 1000, Tac: false, Criteria: []criterion{byScore, byLength}, Multi: 0, Ansi: false, Mouse: true, Theme: tui.EmptyTheme(), Black: false, Bold: true, MinHeight: 10, Layout: layoutDefault, Cycle: false, Hscroll: true, HscrollOff: 10, FileWord: false, InfoStyle: infoDefault, JumpLabels: defaultJumpLabels, Prompt: "> ", Query: "", Select1: false, Exit0: false, Filter: nil, ToggleSort: false, Expect: make(map[int]string), Keymap: make(map[int][]action), Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false, true}, PrintQuery: false, ReadZero: false, Printer: func(str string) { fmt.Println(str) }, PrintSep: "\n", Sync: false, History: nil, Header: make([]string, 0), HeaderLines: 0, Margin: defaultMargin(), Unicode: true, Tabstop: 8, ClearOnExit: true, Version: false} } func help(code int) { os.Stdout.WriteString(usage) os.Exit(code) } func errorExit(msg string) { os.Stderr.WriteString(msg + "\n") os.Exit(exitError) } func optString(arg string, prefixes ...string) (bool, string) { for _, prefix := range prefixes { if strings.HasPrefix(arg, prefix) { return true, arg[len(prefix):] } } return false, "" } func nextString(args []string, i *int, message string) string { if len(args) > *i+1 { *i++ } else { errorExit(message) } return args[*i] } func optionalNextString(args []string, i *int) string { if len(args) > *i+1 && !strings.HasPrefix(args[*i+1], "-") { *i++ return args[*i] } return "" } func atoi(str string) int { num, err := strconv.Atoi(str) if err != nil { errorExit("not a valid integer: " + str) } return num } func atof(str string) float64 { num, err := strconv.ParseFloat(str, 64) if err != nil { errorExit("not a valid number: " + str) } return num } func nextInt(args []string, i *int, message string) int { if len(args) > *i+1 { *i++ } else { errorExit(message) } return atoi(args[*i]) } func optionalNumeric(args []string, i *int, defaultValue int) int { if len(args) > *i+1 { if strings.IndexAny(args[*i+1], "0123456789") == 0 { *i++ return atoi(args[*i]) } } return defaultValue } func splitNth(str string) []Range { if match, _ := regexp.MatchString("^[0-9,-.]+$", str); !match { errorExit("invalid format: " + str) } tokens := strings.Split(str, ",") ranges := make([]Range, len(tokens)) for idx, s := range tokens { r, ok := ParseRange(&s) if !ok { errorExit("invalid format: " + str) } ranges[idx] = r } return ranges } func delimiterRegexp(str string) Delimiter { // Special handling of \t str = strings.Replace(str, "\\t", "\t", -1) // 1. Pattern does not contain any special character if regexp.QuoteMeta(str) == str { return Delimiter{str: &str} } rx, e := regexp.Compile(str) // 2. Pattern is not a valid regular expression if e != nil { return Delimiter{str: &str} } // 3. Pattern as regular expression. Slow. return Delimiter{regex: rx} } func isAlphabet(char uint8) bool { return char >= 'a' && char <= 'z' } func isNumeric(char uint8) bool { return char >= '0' && char <= '9' } func parseAlgo(str string) algo.Algo { switch str { case "v1": return algo.FuzzyMatchV1 case "v2": return algo.FuzzyMatchV2 default: errorExit("invalid algorithm (expected: v1 or v2)") } return algo.FuzzyMatchV2 } func parseKeyChords(str string, message string) map[int]string { if len(str) == 0 { errorExit(message) } tokens := strings.Split(str, ",") if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Contains(str, ",,,") { tokens = append(tokens, ",") } chords := make(map[int]string) for _, key := range tokens { if len(key) == 0 { continue // ignore } lkey := strings.ToLower(key) chord := 0 switch lkey { case "up": chord = tui.Up case "down": chord = tui.Down case "left": chord = tui.Left case "right": chord = tui.Right case "enter", "return": chord = tui.CtrlM case "space": chord = tui.AltZ + int(' ') case "bspace", "bs": chord = tui.BSpace case "ctrl-space": chord = tui.CtrlSpace case "ctrl-^", "ctrl-6": chord = tui.CtrlCaret case "ctrl-/", "ctrl-_": chord = tui.CtrlSlash case "ctrl-\\": chord = tui.CtrlBackSlash case "ctrl-]": chord = tui.CtrlRightBracket case "change": chord = tui.Change case "alt-enter", "alt-return": chord = tui.CtrlAltM case "alt-space": chord = tui.AltSpace case "alt-/": chord = tui.AltSlash case "alt-bs", "alt-bspace": chord = tui.AltBS case "alt-up": chord = tui.AltUp case "alt-down": chord = tui.AltDown case "alt-left": chord = tui.AltLeft case "alt-right": chord = tui.AltRight case "tab": chord = tui.Tab case "btab", "shift-tab": chord = tui.BTab case "esc": chord = tui.ESC case "del": chord = tui.Del case "home": chord = tui.Home case "end": chord = tui.End case "pgup", "page-up": chord = tui.PgUp case "pgdn", "page-down": chord = tui.PgDn case "shift-up": chord = tui.SUp case "shift-down": chord = tui.SDown case "shift-left": chord = tui.SLeft case "shift-right": chord = tui.SRight case "left-click": chord = tui.LeftClick case "right-click": chord = tui.RightClick case "double-click": chord = tui.DoubleClick case "f10": chord = tui.F10 case "f11": chord = tui.F11 case "f12": chord = tui.F12 default: if len(key) == 10 && strings.HasPrefix(lkey, "ctrl-alt-") && isAlphabet(lkey[9]) { chord = tui.CtrlAltA + int(lkey[9]) - 'a' } else if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { chord = tui.CtrlA + int(lkey[5]) - 'a' } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { chord = tui.AltA + int(lkey[4]) - 'a' } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isNumeric(lkey[4]) { chord = tui.Alt0 + int(lkey[4]) - '0' } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' { chord = tui.F1 + int(key[1]) - '1' } else if utf8.RuneCountInString(key) == 1 { chord = tui.AltZ + int([]rune(key)[0]) } else { errorExit("unsupported key: " + key) } } if chord > 0 { chords[chord] = key } } return chords } func parseTiebreak(str string) []criterion { criteria := []criterion{byScore} hasIndex := false hasLength := false hasBegin := false hasEnd := false check := func(notExpected *bool, name string) { if *notExpected { errorExit("duplicate sort criteria: " + name) } if hasIndex { errorExit("index should be the last criterion") } *notExpected = true } for _, str := range strings.Split(strings.ToLower(str), ",") { switch str { case "index": check(&hasIndex, "index") case "length": check(&hasLength, "length") criteria = append(criteria, byLength) case "begin": check(&hasBegin, "begin") criteria = append(criteria, byBegin) case "end": check(&hasEnd, "end") criteria = append(criteria, byEnd) default: errorExit("invalid sort criterion: " + str) } } return criteria } func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme { if theme != nil { dupe := *theme return &dupe } return nil } func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { theme := dupeTheme(defaultTheme) rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$") for _, str := range strings.Split(strings.ToLower(str), ",") { switch str { case "dark": theme = dupeTheme(tui.Dark256) case "light": theme = dupeTheme(tui.Light256) case "16": theme = dupeTheme(tui.Default16) case "bw", "no": theme = nil default: fail := func() { errorExit("invalid color specification: " + str) } // Color is disabled if theme == nil { continue } pair := strings.Split(str, ":") if len(pair) != 2 { fail() } var ansi tui.Color if rrggbb.MatchString(pair[1]) { ansi = tui.HexToColor(pair[1]) } else { ansi32, err := strconv.Atoi(pair[1]) if err != nil || ansi32 < -1 || ansi32 > 255 { fail() } ansi = tui.Color(ansi32) } switch pair[0] { case "fg": theme.Fg = ansi case "bg": theme.Bg = ansi case "preview-fg": theme.PreviewFg = ansi case "preview-bg": theme.PreviewBg = ansi case "fg+": theme.Current = ansi case "bg+": theme.DarkBg = ansi case "gutter": theme.Gutter = ansi case "hl": theme.Match = ansi case "hl+": theme.CurrentMatch = ansi case "border": theme.Border = ansi case "prompt": theme.Prompt = ansi case "spinner": theme.Spinner = ansi case "info": theme.Info = ansi case "pointer": theme.Cursor = ansi case "marker": theme.Selected = ansi case "header": theme.Header = ansi default: fail() } } } return theme } var executeRegexp *regexp.Regexp func firstKey(keymap map[int]string) int { for k := range keymap { return k } return 0 } const ( escapedColon = 0 escapedComma = 1 escapedPlus = 2 ) func init() { // Backreferences are not supported. // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') executeRegexp = regexp.MustCompile( `(?si)[:+](execute(?:-multi|-silent)?|reload):.+|[:+](execute(?:-multi|-silent)?|reload)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`) } func parseKeymap(keymap map[int][]action, str string) { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { symbol := ":" if strings.HasPrefix(src, "+") { symbol = "+" } prefix := symbol + "execute" if strings.HasPrefix(src[1:], "reload") { prefix = symbol + "reload" } else if src[len(prefix)] == '-' { c := src[len(prefix)+1] if c == 's' || c == 'S' { prefix += "-silent" } else { prefix += "-multi" } } return prefix + "(" + strings.Repeat(" ", len(src)-len(prefix)-2) + ")" }) masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1) idx := 0 for _, pairStr := range strings.Split(masked, ",") { origPairStr := str[idx : idx+len(pairStr)] idx += len(pairStr) + 1 pair := strings.SplitN(pairStr, ":", 2) if len(pair) < 2 { errorExit("bind action not specified: " + origPairStr) } var key int if len(pair[0]) == 1 && pair[0][0] == escapedColon { key = ':' + tui.AltZ } else if len(pair[0]) == 1 && pair[0][0] == escapedComma { key = ',' + tui.AltZ } else if len(pair[0]) == 1 && pair[0][0] == escapedPlus { key = '+' + tui.AltZ } else { keys := parseKeyChords(pair[0], "key name required") key = firstKey(keys) } idx2 := len(pair[0]) + 1 specs := strings.Split(pair[1], "+") actions := make([]action, 0, len(specs)) appendAction := func(types ...actionType) { actions = append(actions, toActions(types...)...) } prevSpec := "" for specIndex, maskedSpec := range specs { spec := origPairStr[idx2 : idx2+len(maskedSpec)] idx2 += len(maskedSpec) + 1 spec = prevSpec + spec specLower := strings.ToLower(spec) switch specLower { case "ignore": appendAction(actIgnore) case "beginning-of-line": appendAction(actBeginningOfLine) case "abort": appendAction(actAbort) case "accept": appendAction(actAccept) case "accept-non-empty": appendAction(actAcceptNonEmpty) case "print-query": appendAction(actPrintQuery) case "replace-query": appendAction(actReplaceQuery) case "backward-char": appendAction(actBackwardChar) case "backward-delete-char": appendAction(actBackwardDeleteChar) case "backward-word": appendAction(actBackwardWord) case "clear-screen": appendAction(actClearScreen) case "delete-char": appendAction(actDeleteChar) case "delete-char/eof": appendAction(actDeleteCharEOF) case "end-of-line": appendAction(actEndOfLine) case "cancel": appendAction(actCancel) case "clear-query": appendAction(actClearQuery) case "clear-selection": appendAction(actClearSelection) case "forward-char": appendAction(actForwardChar) case "forward-word": appendAction(actForwardWord) case "jump": appendAction(actJump) case "jump-accept": appendAction(actJumpAccept) case "kill-line": appendAction(actKillLine) case "kill-word": appendAction(actKillWord) case "unix-line-discard", "line-discard": appendAction(actUnixLineDiscard) case "unix-word-rubout", "word-rubout": appendAction(actUnixWordRubout) case "yank": appendAction(actYank) case "backward-kill-word": appendAction(actBackwardKillWord) case "toggle-down": appendAction(actToggle, actDown) case "toggle-up": appendAction(actToggle, actUp) case "toggle-in": appendAction(actToggleIn) case "toggle-out": appendAction(actToggleOut) case "toggle-all": appendAction(actToggleAll) case "select-all": appendAction(actSelectAll) case "deselect-all": appendAction(actDeselectAll) case "toggle": appendAction(actToggle) case "down": appendAction(actDown) case "up": appendAction(actUp) case "top": appendAction(actTop) case "page-up": appendAction(actPageUp) case "page-down": appendAction(actPageDown) case "half-page-up": appendAction(actHalfPageUp) case "half-page-down": appendAction(actHalfPageDown) case "previous-history": appendAction(actPreviousHistory) case "next-history": appendAction(actNextHistory) case "toggle-preview": appendAction(actTogglePreview) case "toggle-preview-wrap": appendAction(actTogglePreviewWrap) case "toggle-sort": appendAction(actToggleSort) case "preview-up": appendAction(actPreviewUp) case "preview-down": appendAction(actPreviewDown) case "preview-page-up": appendAction(actPreviewPageUp) case "preview-page-down": appendAction(actPreviewPageDown) default: t := isExecuteAction(specLower) if t == actIgnore { if specIndex == 0 && specLower == "" { actions = append(keymap[key], actions...) } else { errorExit("unknown action: " + spec) } } else { var offset int switch t { case actReload: offset = len("reload") case actExecuteSilent: offset = len("execute-silent") case actExecuteMulti: offset = len("execute-multi") default: offset = len("execute") } if spec[offset] == ':' { if specIndex == len(specs)-1 { actions = append(actions, action{t: t, a: spec[offset+1:]}) } else { prevSpec = spec + "+" continue } } else { actions = append(actions, action{t: t, a: spec[offset+1 : len(spec)-1]}) } } } prevSpec = "" } keymap[key] = actions } } func isExecuteAction(str string) actionType { matches := executeRegexp.FindAllStringSubmatch(":"+str, -1) if matches == nil || len(matches) != 1 { return actIgnore } prefix := matches[0][1] if len(prefix) == 0 { prefix = matches[0][2] } switch prefix { case "reload": return actReload case "execute": return actExecute case "execute-silent": return actExecuteSilent case "execute-multi": return actExecuteMulti } return actIgnore } func parseToggleSort(keymap map[int][]action, str string) { keys := parseKeyChords(str, "key name required") if len(keys) != 1 { errorExit("multiple keys specified") } keymap[firstKey(keys)] = toActions(actToggleSort) } func strLines(str string) []string { return strings.Split(strings.TrimSuffix(str, "\n"), "\n") } func parseSize(str string, maxPercent float64, label string) sizeSpec { var val float64 percent := strings.HasSuffix(str, "%") if percent { val = atof(str[:len(str)-1]) if val < 0 { errorExit(label + " must be non-negative") } if val > maxPercent { errorExit(fmt.Sprintf("%s too large (max: %d%%)", label, int(maxPercent))) } } else { if strings.Contains(str, ".") { errorExit(label + " (without %) must be a non-negative integer") } val = float64(atoi(str)) if val < 0 { errorExit(label + " must be non-negative") } } return sizeSpec{val, percent} } func parseHeight(str string) sizeSpec { size := parseSize(str, 100, "height") return size } func parseLayout(str string) layoutType { switch str { case "default": return layoutDefault case "reverse": return layoutReverse case "reverse-list": return layoutReverseList default: errorExit("invalid layout (expected: default / reverse / reverse-list)") } return layoutDefault } func parseInfoStyle(str string) infoStyle { switch str { case "default": return infoDefault case "inline": return infoInline case "hidden": return infoHidden default: errorExit("invalid info style (expected: default / inline / hidden)") } return infoDefault } func parsePreviewWindow(opts *previewOpts, input string) { // Default opts.position = posRight opts.size = sizeSpec{50, true} opts.hidden = false opts.wrap = false tokens := strings.Split(input, ":") sizeRegex := regexp.MustCompile("^[0-9]+%?$") for _, token := range tokens { switch token { case "": case "hidden": opts.hidden = true case "wrap": opts.wrap = true case "up", "top": opts.position = posUp case "down", "bottom": opts.position = posDown case "left": opts.position = posLeft case "right": opts.position = posRight case "border": opts.border = true case "noborder": opts.border = false default: if sizeRegex.MatchString(token) { opts.size = parseSize(token, 99, "window size") } else { errorExit("invalid preview window layout: " + input) } } } if !opts.size.percent && opts.size.size > 0 { // Adjust size for border opts.size.size += 2 // And padding if opts.position == posLeft || opts.position == posRight { opts.size.size += 2 } } } func parseMargin(margin string) [4]sizeSpec { margins := strings.Split(margin, ",") checked := func(str string) sizeSpec { return parseSize(str, 49, "margin") } switch len(margins) { case 1: m := checked(margins[0]) return [4]sizeSpec{m, m, m, m} case 2: tb := checked(margins[0]) rl := checked(margins[1]) return [4]sizeSpec{tb, rl, tb, rl} case 3: t := checked(margins[0]) rl := checked(margins[1]) b := checked(margins[2]) return [4]sizeSpec{t, rl, b, rl} case 4: return [4]sizeSpec{ checked(margins[0]), checked(margins[1]), checked(margins[2]), checked(margins[3])} default: errorExit("invalid margin: " + margin) } return defaultMargin() } func parseOptions(opts *Options, allArgs []string) { var historyMax int if opts.History == nil { historyMax = defaultHistoryMax } else { historyMax = opts.History.maxSize } setHistory := func(path string) { h, e := NewHistory(path, historyMax) if e != nil { errorExit(e.Error()) } opts.History = h } setHistoryMax := func(max int) { historyMax = max if historyMax < 1 { errorExit("history max must be a positive integer") } if opts.History != nil { opts.History.maxSize = historyMax } } validateJumpLabels := false for i := 0; i < len(allArgs); i++ { arg := allArgs[i] switch arg { case "-h", "--help": help(exitOk) case "-x", "--extended": opts.Extended = true case "-e", "--exact": opts.Fuzzy = false case "--extended-exact": // Note that we now don't have --no-extended-exact opts.Fuzzy = false opts.Extended = true case "+x", "--no-extended": opts.Extended = false case "+e", "--no-exact": opts.Fuzzy = true case "-q", "--query": opts.Query = nextString(allArgs, &i, "query string required") case "-f", "--filter": filter := nextString(allArgs, &i, "query string required") opts.Filter = &filter case "--literal": opts.Normalize = false case "--no-literal": opts.Normalize = true case "--algo": opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)")) case "--expect": for k, v := range parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") { opts.Expect[k] = v } case "--no-expect": opts.Expect = make(map[int]string) case "--no-phony": opts.Phony = false case "--phony": opts.Phony = true case "--tiebreak": opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) case "--bind": parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required")) case "--color": spec := optionalNextString(allArgs, &i) if len(spec) == 0 { opts.Theme = tui.EmptyTheme() } else { opts.Theme = parseTheme(opts.Theme, spec) } case "--toggle-sort": parseToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required")) case "-d", "--delimiter": opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) case "-n", "--nth": opts.Nth = splitNth(nextString(allArgs, &i, "nth expression required")) case "--with-nth": opts.WithNth = splitNth(nextString(allArgs, &i, "nth expression required")) case "-s", "--sort": opts.Sort = optionalNumeric(allArgs, &i, 1) case "+s", "--no-sort": opts.Sort = 0 case "--tac": opts.Tac = true case "--no-tac": opts.Tac = false case "-i": opts.Case = CaseIgnore case "+i": opts.Case = CaseRespect case "-m", "--multi": opts.Multi = optionalNumeric(allArgs, &i, maxMulti) case "+m", "--no-multi": opts.Multi = 0 case "--ansi": opts.Ansi = true case "--no-ansi": opts.Ansi = false case "--no-mouse": opts.Mouse = false case "+c", "--no-color": opts.Theme = nil case "+2", "--no-256": opts.Theme = tui.Default16 case "--black": opts.Black = true case "--no-black": opts.Black = false case "--bold": opts.Bold = true case "--no-bold": opts.Bold = false case "--layout": opts.Layout = parseLayout( nextString(allArgs, &i, "layout required (default / reverse / reverse-list)")) case "--reverse": opts.Layout = layoutReverse case "--no-reverse": opts.Layout = layoutDefault case "--cycle": opts.Cycle = true case "--no-cycle": opts.Cycle = false case "--hscroll": opts.Hscroll = true case "--no-hscroll": opts.Hscroll = false case "--hscroll-off": opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required") case "--filepath-word": opts.FileWord = true case "--no-filepath-word": opts.FileWord = false case "--info": opts.InfoStyle = parseInfoStyle( nextString(allArgs, &i, "info style required")) case "--no-info": opts.InfoStyle = infoHidden case "--inline-info": opts.InfoStyle = infoInline case "--no-inline-info": opts.InfoStyle = infoDefault case "--jump-labels": opts.JumpLabels = nextString(allArgs, &i, "label characters required") validateJumpLabels = true case "-1", "--select-1": opts.Select1 = true case "+1", "--no-select-1": opts.Select1 = false case "-0", "--exit-0": opts.Exit0 = true case "+0", "--no-exit-0": opts.Exit0 = false case "--read0": opts.ReadZero = true case "--no-read0": opts.ReadZero = false case "--print0": opts.Printer = func(str string) { fmt.Print(str, "\x00") } opts.PrintSep = "\x00" case "--no-print0": opts.Printer = func(str string) { fmt.Println(str) } opts.PrintSep = "\n" case "--print-query": opts.PrintQuery = true case "--no-print-query": opts.PrintQuery = false case "--prompt": opts.Prompt = nextString(allArgs, &i, "prompt string required") case "--sync": opts.Sync = true case "--no-sync": opts.Sync = false case "--async": opts.Sync = false case "--no-history": opts.History = nil case "--history": setHistory(nextString(allArgs, &i, "history file path required")) case "--history-size": setHistoryMax(nextInt(allArgs, &i, "history max size required")) case "--no-header": opts.Header = []string{} case "--no-header-lines": opts.HeaderLines = 0 case "--header": opts.Header = strLines(nextString(allArgs, &i, "header string required")) case "--header-lines": opts.HeaderLines = atoi( nextString(allArgs, &i, "number of header lines required")) case "--preview": opts.Preview.command = nextString(allArgs, &i, "preview command required") case "--no-preview": opts.Preview.command = "" case "--preview-window": parsePreviewWindow(&opts.Preview, nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:noborder][:wrap][:hidden]")) case "--height": opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]")) case "--min-height": opts.MinHeight = nextInt(allArgs, &i, "height required: HEIGHT") case "--no-height": opts.Height = sizeSpec{} case "--no-margin": opts.Margin = defaultMargin() case "--no-border": opts.Bordered = false case "--border": opts.Bordered = true case "--no-unicode": opts.Unicode = false case "--unicode": opts.Unicode = true case "--margin": opts.Margin = parseMargin( nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)")) case "--tabstop": opts.Tabstop = nextInt(allArgs, &i, "tab stop required") case "--clear": opts.ClearOnExit = true case "--no-clear": opts.ClearOnExit = false case "--version": opts.Version = true default: if match, value := optString(arg, "--algo="); match { opts.FuzzyAlgo = parseAlgo(value) } else if match, value := optString(arg, "-q", "--query="); match { opts.Query = value } else if match, value := optString(arg, "-f", "--filter="); match { opts.Filter = &value } else if match, value := optString(arg, "-d", "--delimiter="); match { opts.Delimiter = delimiterRegexp(value) } else if match, value := optString(arg, "--prompt="); match { opts.Prompt = value } else if match, value := optString(arg, "-n", "--nth="); match { opts.Nth = splitNth(value) } else if match, value := optString(arg, "--with-nth="); match { opts.WithNth = splitNth(value) } else if match, _ := optString(arg, "-s", "--sort="); match { opts.Sort = 1 // Don't care } else if match, value := optString(arg, "-m", "--multi="); match { opts.Multi = atoi(value) } else if match, value := optString(arg, "--height="); match { opts.Height = parseHeight(value) } else if match, value := optString(arg, "--min-height="); match { opts.MinHeight = atoi(value) } else if match, value := optString(arg, "--layout="); match { opts.Layout = parseLayout(value) } else if match, value := optString(arg, "--info="); match { opts.InfoStyle = parseInfoStyle(value) } else if match, value := optString(arg, "--toggle-sort="); match { parseToggleSort(opts.Keymap, value) } else if match, value := optString(arg, "--expect="); match { for k, v := range parseKeyChords(value, "key names required") { opts.Expect[k] = v } } else if match, value := optString(arg, "--tiebreak="); match { opts.Criteria = parseTiebreak(value) } else if match, value := optString(arg, "--color="); match { opts.Theme = parseTheme(opts.Theme, value) } else if match, value := optString(arg, "--bind="); match { parseKeymap(opts.Keymap, value) } else if match, value := optString(arg, "--history="); match { setHistory(value) } else if match, value := optString(arg, "--history-size="); match { setHistoryMax(atoi(value)) } else if match, value := optString(arg, "--header="); match { opts.Header = strLines(value) } else if match, value := optString(arg, "--header-lines="); match { opts.HeaderLines = atoi(value) } else if match, value := optString(arg, "--preview="); match { opts.Preview.command = value } else if match, value := optString(arg, "--preview-window="); match { parsePreviewWindow(&opts.Preview, value) } else if match, value := optString(arg, "--margin="); match { opts.Margin = parseMargin(value) } else if match, value := optString(arg, "--tabstop="); match { opts.Tabstop = atoi(value) } else if match, value := optString(arg, "--hscroll-off="); match { opts.HscrollOff = atoi(value) } else if match, value := optString(arg, "--jump-labels="); match { opts.JumpLabels = value } else { errorExit("unknown option: " + arg) } } } if opts.HeaderLines < 0 { errorExit("header lines must be a non-negative integer") } if opts.HscrollOff < 0 { errorExit("hscroll offset must be a non-negative integer") } if opts.Tabstop < 1 { errorExit("tab stop must be a positive integer") } if len(opts.JumpLabels) == 0 { errorExit("empty jump labels") } if validateJumpLabels { for _, r := range opts.JumpLabels { if r < 32 || r > 126 { errorExit("non-ascii jump labels are not allowed") } } } } func postProcessOptions(opts *Options) { if util.IsWindows() && opts.Height.size > 0 { errorExit("--height option is currently not supported on Windows") } // Default actions for CTRL-N / CTRL-P when --history is set if opts.History != nil { if _, prs := opts.Keymap[tui.CtrlP]; !prs { opts.Keymap[tui.CtrlP] = toActions(actPreviousHistory) } if _, prs := opts.Keymap[tui.CtrlN]; !prs { opts.Keymap[tui.CtrlN] = toActions(actNextHistory) } } // Extend the default key map keymap := defaultKeymap() for key, actions := range opts.Keymap { for _, act := range actions { if act.t == actToggleSort { opts.ToggleSort = true } } keymap[key] = actions } opts.Keymap = keymap // If we're not using extended search mode, --nth option becomes irrelevant // if it contains the whole range if !opts.Extended || len(opts.Nth) == 1 { for _, r := range opts.Nth { if r.begin == rangeEllipsis && r.end == rangeEllipsis { opts.Nth = make([]Range, 0) return } } } } // ParseOptions parses command-line options func ParseOptions() *Options { opts := defaultOptions() // Options from Env var words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS")) if len(words) > 0 { parseOptions(opts, words) } // Options from command-line arguments parseOptions(opts, os.Args[1:]) postProcessOptions(opts) return opts } fzf-0.20.0/src/options_test.go000066400000000000000000000274431357617647500162650ustar00rootroot00000000000000package fzf import ( "fmt" "io/ioutil" "testing" "github.com/junegunn/fzf/src/tui" ) func TestDelimiterRegex(t *testing.T) { // Valid regex delim := delimiterRegexp(".") if delim.regex == nil || delim.str != nil { t.Error(delim) } // Broken regex -> string delim = delimiterRegexp("[0-9") if delim.regex != nil || *delim.str != "[0-9" { t.Error(delim) } // Valid regex delim = delimiterRegexp("[0-9]") if delim.regex.String() != "[0-9]" || delim.str != nil { t.Error(delim) } // Tab character delim = delimiterRegexp("\t") if delim.regex != nil || *delim.str != "\t" { t.Error(delim) } // Tab expression delim = delimiterRegexp("\\t") if delim.regex != nil || *delim.str != "\t" { t.Error(delim) } // Tabs -> regex delim = delimiterRegexp("\t+") if delim.regex == nil || delim.str != nil { t.Error(delim) } } func TestDelimiterRegexString(t *testing.T) { delim := delimiterRegexp("*") tokens := Tokenize("-*--*---**---", delim) if delim.regex != nil || tokens[0].text.ToString() != "-*" || tokens[1].text.ToString() != "--*" || tokens[2].text.ToString() != "---*" || tokens[3].text.ToString() != "*" || tokens[4].text.ToString() != "---" { t.Errorf("%s %v %d", delim, tokens, len(tokens)) } } func TestDelimiterRegexRegex(t *testing.T) { delim := delimiterRegexp("--\\*") tokens := Tokenize("-*--*---**---", delim) if delim.str != nil || tokens[0].text.ToString() != "-*--*" || tokens[1].text.ToString() != "---*" || tokens[2].text.ToString() != "*---" { t.Errorf("%s %d", tokens, len(tokens)) } } func TestSplitNth(t *testing.T) { { ranges := splitNth("..") if len(ranges) != 1 || ranges[0].begin != rangeEllipsis || ranges[0].end != rangeEllipsis { t.Errorf("%v", ranges) } } { ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1") if len(ranges) != 10 || ranges[0].begin != rangeEllipsis || ranges[0].end != 3 || ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis || ranges[2].begin != 2 || ranges[2].end != 3 || ranges[3].begin != 4 || ranges[3].end != rangeEllipsis || ranges[4].begin != -3 || ranges[4].end != -2 || ranges[5].begin != rangeEllipsis || ranges[5].end != rangeEllipsis || ranges[6].begin != 2 || ranges[6].end != 2 || ranges[7].begin != -2 || ranges[7].end != -2 || ranges[8].begin != 2 || ranges[8].end != -2 || ranges[9].begin != rangeEllipsis || ranges[9].end != rangeEllipsis { t.Errorf("%v", ranges) } } } func TestIrrelevantNth(t *testing.T) { { opts := defaultOptions() words := []string{"--nth", "..", "-x"} parseOptions(opts, words) postProcessOptions(opts) if len(opts.Nth) != 0 { t.Errorf("nth should be empty: %v", opts.Nth) } } for _, words := range [][]string{[]string{"--nth", "..,3", "+x"}, []string{"--nth", "3,1..", "+x"}, []string{"--nth", "..-1,1", "+x"}} { { opts := defaultOptions() parseOptions(opts, words) postProcessOptions(opts) if len(opts.Nth) != 0 { t.Errorf("nth should be empty: %v", opts.Nth) } } { opts := defaultOptions() words = append(words, "-x") parseOptions(opts, words) postProcessOptions(opts) if len(opts.Nth) != 2 { t.Errorf("nth should not be empty: %v", opts.Nth) } } } } func TestParseKeys(t *testing.T) { pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "") check := func(i int, s string) { if pairs[i] != s { t.Errorf("%s != %s", pairs[i], s) } } if len(pairs) != 12 { t.Error(12) } check(tui.CtrlZ, "ctrl-z") check(tui.AltZ, "alt-z") check(tui.F2, "f2") check(tui.AltZ+'@', "@") check(tui.AltA, "Alt-a") check(tui.AltZ+'!', "!") check(tui.CtrlA+'g'-'a', "ctrl-G") check(tui.AltZ+'J', "J") check(tui.AltZ+'g', "g") check(tui.CtrlAltA, "ctrl-alt-a") check(tui.CtrlAltM, "ALT-enter") check(tui.AltSpace, "alt-SPACE") // Synonyms pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "") if len(pairs) != 9 { t.Error(9) } check(tui.CtrlM, "Return") check(tui.AltZ+' ', "space") check(tui.Tab, "tab") check(tui.BTab, "btab") check(tui.ESC, "esc") check(tui.Up, "up") check(tui.Down, "down") check(tui.Left, "left") check(tui.Right, "right") pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "") if len(pairs) != 11 { t.Error(11) } check(tui.Tab, "Ctrl-I") check(tui.PgUp, "page-up") check(tui.PgDn, "Page-Down") check(tui.Home, "Home") check(tui.End, "End") check(tui.AltBS, "Alt-BSpace") check(tui.SLeft, "shift-left") check(tui.SRight, "shift-right") check(tui.BTab, "shift-tab") check(tui.CtrlM, "Enter") check(tui.BSpace, "bspace") } func TestParseKeysWithComma(t *testing.T) { checkN := func(a int, b int) { if a != b { t.Errorf("%d != %d", a, b) } } check := func(pairs map[int]string, i int, s string) { if pairs[i] != s { t.Errorf("%s != %s", pairs[i], s) } } pairs := parseKeyChords(",", "") checkN(len(pairs), 1) check(pairs, tui.AltZ+',', ",") pairs = parseKeyChords(",,a,b", "") checkN(len(pairs), 3) check(pairs, tui.AltZ+'a', "a") check(pairs, tui.AltZ+'b', "b") check(pairs, tui.AltZ+',', ",") pairs = parseKeyChords("a,b,,", "") checkN(len(pairs), 3) check(pairs, tui.AltZ+'a', "a") check(pairs, tui.AltZ+'b', "b") check(pairs, tui.AltZ+',', ",") pairs = parseKeyChords("a,,,b", "") checkN(len(pairs), 3) check(pairs, tui.AltZ+'a', "a") check(pairs, tui.AltZ+'b', "b") check(pairs, tui.AltZ+',', ",") pairs = parseKeyChords("a,,,b,c", "") checkN(len(pairs), 4) check(pairs, tui.AltZ+'a', "a") check(pairs, tui.AltZ+'b', "b") check(pairs, tui.AltZ+'c', "c") check(pairs, tui.AltZ+',', ",") pairs = parseKeyChords(",,,", "") checkN(len(pairs), 1) check(pairs, tui.AltZ+',', ",") } func TestBind(t *testing.T) { keymap := defaultKeymap() check := func(keyName int, arg1 string, types ...actionType) { if len(keymap[keyName]) != len(types) { t.Errorf("invalid number of actions (%d != %d)", len(types), len(keymap[keyName])) return } for idx, action := range keymap[keyName] { if types[idx] != action.t { t.Errorf("invalid action type (%d != %d)", types[idx], action.t) } } if len(arg1) > 0 && keymap[keyName][0].a != arg1 { t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[keyName][0].a) } } check(tui.CtrlA, "", actBeginningOfLine) parseKeymap(keymap, "ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+ "f1:execute(ls {+})+abort+execute(echo {+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ "alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+ "x:Execute(foo+bar),X:execute/bar+baz/"+ ",f1:+top,f1:+top"+ ",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up") check(tui.CtrlA, "", actKillLine) check(tui.CtrlB, "", actToggleSort, actUp, actDown) check(tui.AltZ+'c', "", actPageUp) check(tui.AltZ+',', "", actAbort) check(tui.AltZ+':', "", actAccept) check(tui.AltZ, "", actPageDown) check(tui.F1, "ls {+}", actExecute, actAbort, actExecute, actSelectAll, actTop, actTop) check(tui.F2, "echo {}, {}, {}", actExecute) check(tui.F3, "echo '({})'", actExecute) check(tui.F4, "less {}", actExecute) check(tui.AltZ+'x', "foo+bar", actExecute) check(tui.AltZ+'X', "bar+baz", actExecute) check(tui.AltA, "echo (,),[,],/,:,;,%,{}", actExecuteMulti) check(tui.AltB, "echo (,),[,],/,:,@,%,{}", actExecute) check(tui.AltZ+'+', "++\nfoobar,Y:execute(baz)+up", actExecute) for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) check(tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute) } parseKeymap(keymap, "f1:abort") check(tui.F1, "", actAbort) } func TestColorSpec(t *testing.T) { theme := tui.Dark256 dark := parseTheme(theme, "dark") if *dark != *theme { t.Errorf("colors should be equivalent") } if dark == theme { t.Errorf("point should not be equivalent") } light := parseTheme(theme, "dark,light") if *light == *theme { t.Errorf("should not be equivalent") } if *light != *tui.Light256 { t.Errorf("colors should be equivalent") } if light == theme { t.Errorf("point should not be equivalent") } customized := parseTheme(theme, "fg:231,bg:232") if customized.Fg != 231 || customized.Bg != 232 { t.Errorf("color not customized") } if *tui.Dark256 == *customized { t.Errorf("colors should not be equivalent") } customized.Fg = tui.Dark256.Fg customized.Bg = tui.Dark256.Bg if *tui.Dark256 != *customized { t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized) } customized = parseTheme(theme, "fg:231,dark,bg:232") if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg { t.Errorf("color not customized") } } func TestParseNilTheme(t *testing.T) { var theme *tui.ColorTheme newTheme := parseTheme(theme, "prompt:12") if newTheme != nil { t.Errorf("color is disabled. keep it that way.") } newTheme = parseTheme(theme, "prompt:12,dark,prompt:13") if newTheme.Prompt != 13 { t.Errorf("color should now be enabled and customized") } } func TestDefaultCtrlNP(t *testing.T) { check := func(words []string, key int, expected actionType) { opts := defaultOptions() parseOptions(opts, words) postProcessOptions(opts) if opts.Keymap[key][0].t != expected { t.Error() } } check([]string{}, tui.CtrlN, actDown) check([]string{}, tui.CtrlP, actUp) check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept) check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept) f, _ := ioutil.TempFile("", "fzf-history") f.Close() hist := "--history=" + f.Name() check([]string{hist}, tui.CtrlN, actNextHistory) check([]string{hist}, tui.CtrlP, actPreviousHistory) check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlN, actAccept) check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPreviousHistory) check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlN, actNextHistory) check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept) } func optsFor(words ...string) *Options { opts := defaultOptions() parseOptions(opts, words) postProcessOptions(opts) return opts } func TestToggle(t *testing.T) { opts := optsFor() if opts.ToggleSort { t.Error() } opts = optsFor("--bind=a:toggle-sort") if !opts.ToggleSort { t.Error() } opts = optsFor("--bind=a:toggle-sort", "--bind=a:up") if opts.ToggleSort { t.Error() } } func TestPreviewOpts(t *testing.T) { opts := optsFor() if !(opts.Preview.command == "" && opts.Preview.hidden == false && opts.Preview.wrap == false && opts.Preview.position == posRight && opts.Preview.size.percent == true && opts.Preview.size.size == 50) { t.Error() } opts = optsFor("--preview", "cat {}", "--preview-window=left:15:hidden:wrap") if !(opts.Preview.command == "cat {}" && opts.Preview.hidden == true && opts.Preview.wrap == true && opts.Preview.position == posLeft && opts.Preview.size.percent == false && opts.Preview.size.size == 15+2+2) { t.Error(opts.Preview) } opts = optsFor("--preview-window=up:15:wrap:hidden", "--preview-window=down") if !(opts.Preview.command == "" && opts.Preview.hidden == false && opts.Preview.wrap == false && opts.Preview.position == posDown && opts.Preview.size.percent == true && opts.Preview.size.size == 50) { t.Error(opts.Preview) } opts = optsFor("--preview-window=up:15:wrap:hidden") if !(opts.Preview.command == "" && opts.Preview.hidden == true && opts.Preview.wrap == true && opts.Preview.position == posUp && opts.Preview.size.percent == false && opts.Preview.size.size == 15+2) { t.Error(opts.Preview) } } func TestAdditiveExpect(t *testing.T) { opts := optsFor("--expect=a", "--expect", "b", "--expect=c") if len(opts.Expect) != 3 { t.Error(opts.Expect) } } fzf-0.20.0/src/pattern.go000066400000000000000000000240461357617647500152040ustar00rootroot00000000000000package fzf import ( "fmt" "regexp" "strings" "github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/util" ) // fuzzy // 'exact // ^prefix-exact // suffix-exact$ // !inverse-exact // !'inverse-fuzzy // !^inverse-prefix-exact // !inverse-suffix-exact$ type termType int const ( termFuzzy termType = iota termExact termPrefix termSuffix termEqual ) type term struct { typ termType inv bool text []rune caseSensitive bool } // String returns the string representation of a term. func (t term) String() string { return fmt.Sprintf("term{typ: %d, inv: %v, text: []rune(%q), caseSensitive: %v}", t.typ, t.inv, string(t.text), t.caseSensitive) } type termSet []term // Pattern represents search pattern type Pattern struct { fuzzy bool fuzzyAlgo algo.Algo extended bool caseSensitive bool normalize bool forward bool text []rune termSets []termSet sortable bool cacheable bool cacheKey string delimiter Delimiter nth []Range procFun map[termType]algo.Algo } var ( _patternCache map[string]*Pattern _splitRegex *regexp.Regexp _cache ChunkCache ) func init() { _splitRegex = regexp.MustCompile(" +") clearPatternCache() clearChunkCache() } func clearPatternCache() { // We can uniquely identify the pattern for a given string since // search mode and caseMode do not change while the program is running _patternCache = make(map[string]*Pattern) } func clearChunkCache() { _cache = NewChunkCache() } // BuildPattern builds Pattern object from the given arguments func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { var asString string if extended { asString = strings.TrimLeft(string(runes), " ") for strings.HasSuffix(asString, " ") && !strings.HasSuffix(asString, "\\ ") { asString = asString[:len(asString)-1] } } else { asString = string(runes) } cached, found := _patternCache[asString] if found { return cached } caseSensitive := true sortable := true termSets := []termSet{} if extended { termSets = parseTerms(fuzzy, caseMode, normalize, asString) // We should not sort the result if there are only inverse search terms sortable = false Loop: for _, termSet := range termSets { for idx, term := range termSet { if !term.inv { sortable = true } // If the query contains inverse search terms or OR operators, // we cannot cache the search scope if !cacheable || idx > 0 || term.inv || fuzzy && term.typ != termFuzzy || !fuzzy && term.typ != termExact { cacheable = false if sortable { // Can't break until we see at least one non-inverse term break Loop } } } } } else { lowerString := strings.ToLower(asString) caseSensitive = caseMode == CaseRespect || caseMode == CaseSmart && lowerString != asString if !caseSensitive { asString = lowerString } } ptr := &Pattern{ fuzzy: fuzzy, fuzzyAlgo: fuzzyAlgo, extended: extended, caseSensitive: caseSensitive, normalize: normalize, forward: forward, text: []rune(asString), termSets: termSets, sortable: sortable, cacheable: cacheable, nth: nth, delimiter: delimiter, procFun: make(map[termType]algo.Algo)} ptr.cacheKey = ptr.buildCacheKey() ptr.procFun[termFuzzy] = fuzzyAlgo ptr.procFun[termEqual] = algo.EqualMatch ptr.procFun[termExact] = algo.ExactMatchNaive ptr.procFun[termPrefix] = algo.PrefixMatch ptr.procFun[termSuffix] = algo.SuffixMatch _patternCache[asString] = ptr return ptr } func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet { str = strings.Replace(str, "\\ ", "\t", -1) tokens := _splitRegex.Split(str, -1) sets := []termSet{} set := termSet{} switchSet := false afterBar := false for _, token := range tokens { typ, inv, text := termFuzzy, false, strings.Replace(token, "\t", " ", -1) lowerText := strings.ToLower(text) caseSensitive := caseMode == CaseRespect || caseMode == CaseSmart && text != lowerText if !caseSensitive { text = lowerText } if !fuzzy { typ = termExact } if len(set) > 0 && !afterBar && text == "|" { switchSet = false afterBar = true continue } afterBar = false if strings.HasPrefix(text, "!") { inv = true typ = termExact text = text[1:] } if text != "$" && strings.HasSuffix(text, "$") { typ = termSuffix text = text[:len(text)-1] } if strings.HasPrefix(text, "'") { // Flip exactness if fuzzy && !inv { typ = termExact text = text[1:] } else { typ = termFuzzy text = text[1:] } } else if strings.HasPrefix(text, "^") { if typ == termSuffix { typ = termEqual } else { typ = termPrefix } text = text[1:] } if len(text) > 0 { if switchSet { sets = append(sets, set) set = termSet{} } textRunes := []rune(text) if normalize { textRunes = algo.NormalizeRunes(textRunes) } set = append(set, term{ typ: typ, inv: inv, text: textRunes, caseSensitive: caseSensitive}) switchSet = true } } if len(set) > 0 { sets = append(sets, set) } return sets } // IsEmpty returns true if the pattern is effectively empty func (p *Pattern) IsEmpty() bool { if !p.extended { return len(p.text) == 0 } return len(p.termSets) == 0 } // AsString returns the search query in string type func (p *Pattern) AsString() string { return string(p.text) } func (p *Pattern) buildCacheKey() string { if !p.extended { return p.AsString() } cacheableTerms := []string{} for _, termSet := range p.termSets { if len(termSet) == 1 && !termSet[0].inv && (p.fuzzy || termSet[0].typ == termExact) { cacheableTerms = append(cacheableTerms, string(termSet[0].text)) } } return strings.Join(cacheableTerms, "\t") } // CacheKey is used to build string to be used as the key of result cache func (p *Pattern) CacheKey() string { return p.cacheKey } // Match returns the list of matches Items in the given Chunk func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result { // ChunkCache: Exact match cacheKey := p.CacheKey() if p.cacheable { if cached := _cache.Lookup(chunk, cacheKey); cached != nil { return cached } } // Prefix/suffix cache space := _cache.Search(chunk, cacheKey) matches := p.matchChunk(chunk, space, slab) if p.cacheable { _cache.Add(chunk, cacheKey, matches) } return matches } func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result { matches := []Result{} if space == nil { for idx := 0; idx < chunk.count; idx++ { if match, _, _ := p.MatchItem(&chunk.items[idx], false, slab); match != nil { matches = append(matches, *match) } } } else { for _, result := range space { if match, _, _ := p.MatchItem(result.item, false, slab); match != nil { matches = append(matches, *match) } } } return matches } // MatchItem returns true if the Item is a match func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) { if p.extended { if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) { result := buildResult(item, offsets, bonus) return &result, offsets, pos } return nil, nil, nil } offset, bonus, pos := p.basicMatch(item, withPos, slab) if sidx := offset[0]; sidx >= 0 { offsets := []Offset{offset} result := buildResult(item, offsets, bonus) return &result, offsets, pos } return nil, nil, nil } func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) { var input []Token if len(p.nth) == 0 { input = []Token{Token{text: &item.text, prefixLength: 0}} } else { input = p.transformInput(item) } if p.fuzzy { return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab) } return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab) } func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) { var input []Token if len(p.nth) == 0 { input = []Token{Token{text: &item.text, prefixLength: 0}} } else { input = p.transformInput(item) } offsets := []Offset{} var totalScore int var allPos *[]int if withPos { allPos = &[]int{} } for _, termSet := range p.termSets { var offset Offset var currentScore int matched := false for _, term := range termSet { pfun := p.procFun[term.typ] off, score, pos := p.iter(pfun, input, term.caseSensitive, p.normalize, p.forward, term.text, withPos, slab) if sidx := off[0]; sidx >= 0 { if term.inv { continue } offset, currentScore = off, score matched = true if withPos { if pos != nil { *allPos = append(*allPos, *pos...) } else { for idx := off[0]; idx < off[1]; idx++ { *allPos = append(*allPos, int(idx)) } } } break } else if term.inv { offset, currentScore = Offset{0, 0}, 0 matched = true continue } } if matched { offsets = append(offsets, offset) totalScore += currentScore } } return offsets, totalScore, allPos } func (p *Pattern) transformInput(item *Item) []Token { if item.transformed != nil { return *item.transformed } tokens := Tokenize(item.text.ToString(), p.delimiter) ret := Transform(tokens, p.nth) item.transformed = &ret return ret } func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, normalize bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, *[]int) { for _, part := range tokens { if res, pos := pfun(caseSensitive, normalize, forward, part.text, pattern, withPos, slab); res.Start >= 0 { sidx := int32(res.Start) + part.prefixLength eidx := int32(res.End) + part.prefixLength if pos != nil { for idx := range *pos { (*pos)[idx] += int(part.prefixLength) } } return Offset{sidx, eidx}, res.Score, pos } } return Offset{-1, -1}, 0, nil } fzf-0.20.0/src/pattern_test.go000066400000000000000000000173621357617647500162460ustar00rootroot00000000000000package fzf import ( "reflect" "testing" "github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/util" ) var slab *util.Slab func init() { slab = util.MakeSlab(slab16Size, slab32Size) } func TestParseTermsExtended(t *testing.T) { terms := parseTerms(true, CaseSmart, false, "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | zzz$ | !ZZZ |") if len(terms) != 9 || terms[0][0].typ != termFuzzy || terms[0][0].inv || terms[1][0].typ != termExact || terms[1][0].inv || terms[2][0].typ != termPrefix || terms[2][0].inv || terms[3][0].typ != termSuffix || terms[3][0].inv || terms[4][0].typ != termExact || !terms[4][0].inv || terms[5][0].typ != termFuzzy || !terms[5][0].inv || terms[6][0].typ != termPrefix || !terms[6][0].inv || terms[7][0].typ != termSuffix || !terms[7][0].inv || terms[7][1].typ != termEqual || terms[7][1].inv || terms[8][0].typ != termPrefix || terms[8][0].inv || terms[8][1].typ != termExact || terms[8][1].inv || terms[8][2].typ != termSuffix || terms[8][2].inv || terms[8][3].typ != termExact || !terms[8][3].inv { t.Errorf("%v", terms) } for _, termSet := range terms[:8] { term := termSet[0] if len(term.text) != 3 { t.Errorf("%v", term) } } } func TestParseTermsExtendedExact(t *testing.T) { terms := parseTerms(false, CaseSmart, false, "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") if len(terms) != 8 || terms[0][0].typ != termExact || terms[0][0].inv || len(terms[0][0].text) != 3 || terms[1][0].typ != termFuzzy || terms[1][0].inv || len(terms[1][0].text) != 3 || terms[2][0].typ != termPrefix || terms[2][0].inv || len(terms[2][0].text) != 3 || terms[3][0].typ != termSuffix || terms[3][0].inv || len(terms[3][0].text) != 3 || terms[4][0].typ != termExact || !terms[4][0].inv || len(terms[4][0].text) != 3 || terms[5][0].typ != termFuzzy || !terms[5][0].inv || len(terms[5][0].text) != 3 || terms[6][0].typ != termPrefix || !terms[6][0].inv || len(terms[6][0].text) != 3 || terms[7][0].typ != termSuffix || !terms[7][0].inv || len(terms[7][0].text) != 3 { t.Errorf("%v", terms) } } func TestParseTermsEmpty(t *testing.T) { terms := parseTerms(true, CaseSmart, false, "' ^ !' !^") if len(terms) != 0 { t.Errorf("%v", terms) } } func TestExact(t *testing.T) { defer clearPatternCache() clearPatternCache() pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("'abc")) chars := util.ToChars([]byte("aabbcc abc")) res, pos := algo.ExactMatchNaive( pattern.caseSensitive, pattern.normalize, pattern.forward, &chars, pattern.termSets[0][0].text, true, nil) if res.Start != 7 || res.End != 10 { t.Errorf("%v / %d / %d", pattern.termSets, res.Start, res.End) } if pos != nil { t.Errorf("pos is expected to be nil") } } func TestEqual(t *testing.T) { defer clearPatternCache() clearPatternCache() pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("^AbC$")) match := func(str string, sidxExpected int, eidxExpected int) { chars := util.ToChars([]byte(str)) res, pos := algo.EqualMatch( pattern.caseSensitive, pattern.normalize, pattern.forward, &chars, pattern.termSets[0][0].text, true, nil) if res.Start != sidxExpected || res.End != eidxExpected { t.Errorf("%v / %d / %d", pattern.termSets, res.Start, res.End) } if pos != nil { t.Errorf("pos is expected to be nil") } } match("ABC", -1, -1) match("AbC", 0, 3) } func TestCaseSensitivity(t *testing.T) { defer clearPatternCache() clearPatternCache() pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("Abc")) if string(pat1.text) != "abc" || pat1.caseSensitive != false || string(pat2.text) != "Abc" || pat2.caseSensitive != true || string(pat3.text) != "abc" || pat3.caseSensitive != false || string(pat4.text) != "abc" || pat4.caseSensitive != false || string(pat5.text) != "abc" || pat5.caseSensitive != true || string(pat6.text) != "Abc" || pat6.caseSensitive != true { t.Error("Invalid case conversion") } } func TestOrigTextAndTransformed(t *testing.T) { pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg")) tokens := Tokenize("junegunn", Delimiter{}) trans := Transform(tokens, []Range{Range{1, 1}}) origBytes := []byte("junegunn.choi") for _, extended := range []bool{false, true} { chunk := Chunk{count: 1} chunk.items[0] = Item{ text: util.ToChars([]byte("junegunn")), origText: &origBytes, transformed: &trans} pattern.extended = extended matches := pattern.matchChunk(&chunk, nil, slab) // No cache if !(matches[0].item.text.ToString() == "junegunn" && string(*matches[0].item.origText) == "junegunn.choi" && reflect.DeepEqual(*matches[0].item.transformed, trans)) { t.Error("Invalid match result", matches) } match, offsets, pos := pattern.MatchItem(&chunk.items[0], true, slab) if !(match.item.text.ToString() == "junegunn" && string(*match.item.origText) == "junegunn.choi" && offsets[0][0] == 0 && offsets[0][1] == 5 && reflect.DeepEqual(*match.item.transformed, trans)) { t.Error("Invalid match result", match, offsets, extended) } if !((*pos)[0] == 4 && (*pos)[1] == 0) { t.Error("Invalid pos array", *pos) } } } func TestCacheKey(t *testing.T) { test := func(extended bool, patStr string, expected string, cacheable bool) { clearPatternCache() pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune(patStr)) if pat.CacheKey() != expected { t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) } if pat.cacheable != cacheable { t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr) } clearPatternCache() } test(false, "foo !bar", "foo !bar", true) test(false, "foo | bar !baz", "foo | bar !baz", true) test(true, "foo bar baz", "foo\tbar\tbaz", true) test(true, "foo !bar", "foo", false) test(true, "foo !bar baz", "foo\tbaz", false) test(true, "foo | bar baz", "baz", false) test(true, "foo | bar | baz", "", false) test(true, "foo | bar !baz", "", false) test(true, "| | foo", "", false) test(true, "| | | foo", "foo", false) } func TestCacheable(t *testing.T) { test := func(fuzzy bool, str string, expected string, cacheable bool) { clearPatternCache() pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, true, []Range{}, Delimiter{}, []rune(str)) if pat.CacheKey() != expected { t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) } if cacheable != pat.cacheable { t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable) } clearPatternCache() } test(true, "foo bar", "foo\tbar", true) test(true, "foo 'bar", "foo\tbar", false) test(true, "foo !bar", "foo", false) test(false, "foo bar", "foo\tbar", true) test(false, "foo 'bar", "foo", false) test(false, "foo '", "foo", true) test(false, "foo 'bar", "foo", false) test(false, "foo !bar", "foo", false) } fzf-0.20.0/src/reader.go000066400000000000000000000067351357617647500147760ustar00rootroot00000000000000package fzf import ( "bufio" "io" "os" "os/exec" "sync" "sync/atomic" "time" "github.com/junegunn/fzf/src/util" ) // Reader reads from command or standard input type Reader struct { pusher func([]byte) bool eventBox *util.EventBox delimNil bool event int32 finChan chan bool mutex sync.Mutex exec *exec.Cmd command *string killed bool wait bool } // NewReader returns new Reader object func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool, wait bool) *Reader { return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait} } func (r *Reader) startEventPoller() { go func() { ptr := &r.event pollInterval := readerPollIntervalMin for { if atomic.CompareAndSwapInt32(ptr, int32(EvtReadNew), int32(EvtReady)) { r.eventBox.Set(EvtReadNew, (*string)(nil)) pollInterval = readerPollIntervalMin } else if atomic.LoadInt32(ptr) == int32(EvtReadFin) { if r.wait { r.finChan <- true } return } else { pollInterval += readerPollIntervalStep if pollInterval > readerPollIntervalMax { pollInterval = readerPollIntervalMax } } time.Sleep(pollInterval) } }() } func (r *Reader) fin(success bool) { atomic.StoreInt32(&r.event, int32(EvtReadFin)) if r.wait { <-r.finChan } r.mutex.Lock() ret := r.command if success || r.killed { ret = nil } r.mutex.Unlock() r.eventBox.Set(EvtReadFin, ret) } func (r *Reader) terminate() { r.mutex.Lock() defer func() { r.mutex.Unlock() }() r.killed = true if r.exec != nil && r.exec.Process != nil { util.KillCommand(r.exec) } else { os.Stdin.Close() } } func (r *Reader) restart(command string) { r.event = int32(EvtReady) r.startEventPoller() success := r.readFromCommand(nil, command) r.fin(success) } // ReadSource reads data from the default command or from standard input func (r *Reader) ReadSource() { r.startEventPoller() var success bool if util.IsTty() { // The default command for *nix requires bash shell := "bash" cmd := os.Getenv("FZF_DEFAULT_COMMAND") if len(cmd) == 0 { success = r.readFromCommand(&shell, defaultCommand) } else { success = r.readFromCommand(nil, cmd) } } else { success = r.readFromStdin() } r.fin(success) } func (r *Reader) feed(src io.Reader) { delim := byte('\n') if r.delimNil { delim = '\000' } reader := bufio.NewReaderSize(src, readerBufferSize) for { // ReadBytes returns err != nil if and only if the returned data does not // end in delim. bytea, err := reader.ReadBytes(delim) byteaLen := len(bytea) if byteaLen > 0 { if err == nil { // get rid of carriage return if under Windows: if util.IsWindows() && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') { bytea = bytea[:byteaLen-2] } else { bytea = bytea[:byteaLen-1] } } if r.pusher(bytea) { atomic.StoreInt32(&r.event, int32(EvtReadNew)) } } if err != nil { break } } } func (r *Reader) readFromStdin() bool { r.feed(os.Stdin) return true } func (r *Reader) readFromCommand(shell *string, command string) bool { r.mutex.Lock() r.killed = false r.command = &command if shell != nil { r.exec = util.ExecCommandWith(*shell, command, true) } else { r.exec = util.ExecCommand(command, true) } out, err := r.exec.StdoutPipe() if err != nil { r.mutex.Unlock() return false } err = r.exec.Start() r.mutex.Unlock() if err != nil { return false } r.feed(out) return r.exec.Wait() == nil } fzf-0.20.0/src/reader_test.go000066400000000000000000000024011357617647500160170ustar00rootroot00000000000000package fzf import ( "testing" "time" "github.com/junegunn/fzf/src/util" ) func TestReadFromCommand(t *testing.T) { strs := []string{} eb := util.NewEventBox() reader := NewReader( func(s []byte) bool { strs = append(strs, string(s)); return true }, eb, false, true) reader.startEventPoller() // Check EventBox if eb.Peek(EvtReadNew) { t.Error("EvtReadNew should not be set yet") } // Normal command reader.fin(reader.readFromCommand(nil, `echo abc && echo def`)) if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" { t.Errorf("%s", strs) } // Check EventBox again eb.WaitFor(EvtReadFin) // Wait should return immediately eb.Wait(func(events *util.Events) { events.Clear() }) // EventBox is cleared if eb.Peek(EvtReadNew) { t.Error("EvtReadNew should not be set yet") } // Make sure that event poller is finished time.Sleep(readerPollIntervalMax) // Restart event poller reader.startEventPoller() // Failing command reader.fin(reader.readFromCommand(nil, `no-such-command`)) strs = []string{} if len(strs) > 0 { t.Errorf("%s", strs) } // Check EventBox again if eb.Peek(EvtReadNew) { t.Error("Command failed. EvtReadNew should not be set") } if !eb.Peek(EvtReadFin) { t.Error("EvtReadFin should be set") } } fzf-0.20.0/src/result.go000066400000000000000000000112231357617647500150360ustar00rootroot00000000000000package fzf import ( "math" "sort" "unicode" "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" ) // Offset holds two 32-bit integers denoting the offsets of a matched substring type Offset [2]int32 type colorOffset struct { offset [2]int32 color tui.ColorPair attr tui.Attr } type Result struct { item *Item points [4]uint16 } func buildResult(item *Item, offsets []Offset, score int) Result { if len(offsets) > 1 { sort.Sort(ByOrder(offsets)) } result := Result{item: item} numChars := item.text.Length() minBegin := math.MaxUint16 minEnd := math.MaxUint16 maxEnd := 0 validOffsetFound := false for _, offset := range offsets { b, e := int(offset[0]), int(offset[1]) if b < e { minBegin = util.Min(b, minBegin) minEnd = util.Min(e, minEnd) maxEnd = util.Max(e, maxEnd) validOffsetFound = true } } for idx, criterion := range sortCriteria { val := uint16(math.MaxUint16) switch criterion { case byScore: // Higher is better val = math.MaxUint16 - util.AsUint16(score) case byLength: val = item.TrimLength() case byBegin, byEnd: if validOffsetFound { whitePrefixLen := 0 for idx := 0; idx < numChars; idx++ { r := item.text.Get(idx) whitePrefixLen = idx if idx == minBegin || !unicode.IsSpace(r) { break } } if criterion == byBegin { val = util.AsUint16(minEnd - whitePrefixLen) } else { val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength())) } } } result.points[3-idx] = val } return result } // Sort criteria to use. Never changes once fzf is started. var sortCriteria []criterion // Index returns ordinal index of the Item func (result *Result) Index() int32 { return result.item.Index() } func minRank() Result { return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}} } func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, color tui.ColorPair, attr tui.Attr, current bool) []colorOffset { itemColors := result.item.Colors() // No ANSI code, or --color=no if len(itemColors) == 0 { var offsets []colorOffset for _, off := range matchOffsets { offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, attr: attr}) } return offsets } // Find max column var maxCol int32 for _, off := range matchOffsets { if off[1] > maxCol { maxCol = off[1] } } for _, ansi := range itemColors { if ansi.offset[1] > maxCol { maxCol = ansi.offset[1] } } cols := make([]int, maxCol) for colorIndex, ansi := range itemColors { for i := ansi.offset[0]; i < ansi.offset[1]; i++ { cols[i] = colorIndex + 1 // XXX } } for _, off := range matchOffsets { for i := off[0]; i < off[1]; i++ { cols[i] = -1 } } // sort.Sort(ByOrder(offsets)) // Merge offsets // ------------ ---- -- ---- // ++++++++ ++++++++++ // --++++++++-- --++++++++++--- curr := 0 start := 0 var colors []colorOffset add := func(idx int) { if curr != 0 && idx > start { if curr == -1 { colors = append(colors, colorOffset{ offset: [2]int32{int32(start), int32(idx)}, color: color, attr: attr}) } else { ansi := itemColors[curr-1] fg := ansi.color.fg bg := ansi.color.bg if theme != nil { if fg == -1 { if current { fg = theme.Current } else { fg = theme.Fg } } if bg == -1 { if current { bg = theme.DarkBg } else { bg = theme.Bg } } } colors = append(colors, colorOffset{ offset: [2]int32{int32(start), int32(idx)}, color: tui.NewColorPair(fg, bg), attr: ansi.color.attr.Merge(attr)}) } } } for idx, col := range cols { if col != curr { add(idx) start = idx curr = col } } add(int(maxCol)) return colors } // ByOrder is for sorting substring offsets type ByOrder []Offset func (a ByOrder) Len() int { return len(a) } func (a ByOrder) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByOrder) Less(i, j int) bool { ioff := a[i] joff := a[j] return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1]) } // ByRelevance is for sorting Items type ByRelevance []Result func (a ByRelevance) Len() int { return len(a) } func (a ByRelevance) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByRelevance) Less(i, j int) bool { return compareRanks(a[i], a[j], false) } // ByRelevanceTac is for sorting Items type ByRelevanceTac []Result func (a ByRelevanceTac) Len() int { return len(a) } func (a ByRelevanceTac) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByRelevanceTac) Less(i, j int) bool { return compareRanks(a[i], a[j], true) } fzf-0.20.0/src/result_others.go000066400000000000000000000005161357617647500164250ustar00rootroot00000000000000// +build !386,!amd64 package fzf func compareRanks(irank Result, jrank Result, tac bool) bool { for idx := 3; idx >= 0; idx-- { left := irank.points[idx] right := jrank.points[idx] if left < right { return true } else if left > right { return false } } return (irank.item.Index() <= jrank.item.Index()) != tac } fzf-0.20.0/src/result_test.go000066400000000000000000000103411357617647500160750ustar00rootroot00000000000000// +build !tcell package fzf import ( "math" "sort" "testing" "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" ) func withIndex(i *Item, index int) *Item { (*i).text.Index = int32(index) return i } func TestOffsetSort(t *testing.T) { offsets := []Offset{ Offset{3, 5}, Offset{2, 7}, Offset{1, 3}, Offset{2, 9}} sort.Sort(ByOrder(offsets)) if offsets[0][0] != 1 || offsets[0][1] != 3 || offsets[1][0] != 2 || offsets[1][1] != 7 || offsets[2][0] != 2 || offsets[2][1] != 9 || offsets[3][0] != 3 || offsets[3][1] != 5 { t.Error("Invalid order:", offsets) } } func TestRankComparison(t *testing.T) { rank := func(vals ...uint16) Result { return Result{ points: [4]uint16{vals[0], vals[1], vals[2], vals[3]}, item: &Item{text: util.Chars{Index: int32(vals[4])}}} } if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), false) || !compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) || !compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), false) || !compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) { t.Error("Invalid order") } if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), true) || !compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) || !compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), true) || !compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) { t.Error("Invalid order (tac)") } } // Match length, string length, index func TestResultRank(t *testing.T) { // FIXME global sortCriteria = []criterion{byScore, byLength} strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} item1 := buildResult( withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2) if item1.points[3] != math.MaxUint16-2 || // Bonus item1.points[2] != 3 || // Length item1.points[1] != 0 || // Unused item1.points[0] != 0 || // Unused item1.item.Index() != 1 { t.Error(item1) } // Only differ in index item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2) items := []Result{item1, item2} sort.Sort(ByRelevance(items)) if items[0] != item2 || items[1] != item1 { t.Error(items) } items = []Result{item2, item1, item1, item2} sort.Sort(ByRelevance(items)) if items[0] != item2 || items[1] != item2 || items[2] != item1 || items[3] != item1 { t.Error(items, item1, item1.item.Index(), item2, item2.item.Index()) } // Sort by relevance item3 := buildResult( withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 3) item4 := buildResult( withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 4) item5 := buildResult( withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 5) item6 := buildResult( withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 6) items = []Result{item1, item2, item3, item4, item5, item6} sort.Sort(ByRelevance(items)) if !(items[0] == item6 && items[1] == item5 && items[2] == item4 && items[3] == item3 && items[4] == item2 && items[5] == item1) { t.Error(items, item1, item2, item3, item4, item5, item6) } } func TestColorOffset(t *testing.T) { // ------------ 20 ---- -- ---- // ++++++++ ++++++++++ // --++++++++-- --++++++++++--- offsets := []Offset{Offset{5, 15}, Offset{25, 35}} item := Result{ item: &Item{ colors: &[]ansiOffset{ ansiOffset{[2]int32{0, 20}, ansiState{1, 5, 0}}, ansiOffset{[2]int32{22, 27}, ansiState{2, 6, tui.Bold}}, ansiOffset{[2]int32{30, 32}, ansiState{3, 7, 0}}, ansiOffset{[2]int32{33, 40}, ansiState{4, 8, tui.Bold}}}}} // [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}] pair := tui.NewColorPair(99, 199) colors := item.colorOffsets(offsets, tui.Dark256, pair, tui.AttrRegular, true) assert := func(idx int, b int32, e int32, c tui.ColorPair, bold bool) { var attr tui.Attr if bold { attr = tui.Bold } o := colors[idx] if o.offset[0] != b || o.offset[1] != e || o.color != c || o.attr != attr { t.Error(o) } } assert(0, 0, 5, tui.NewColorPair(1, 5), false) assert(1, 5, 15, pair, false) assert(2, 15, 20, tui.NewColorPair(1, 5), false) assert(3, 22, 25, tui.NewColorPair(2, 6), true) assert(4, 25, 35, pair, false) assert(5, 35, 40, tui.NewColorPair(4, 8), true) } fzf-0.20.0/src/result_x86.go000066400000000000000000000005501357617647500155440ustar00rootroot00000000000000// +build 386 amd64 package fzf import "unsafe" func compareRanks(irank Result, jrank Result, tac bool) bool { left := *(*uint64)(unsafe.Pointer(&irank.points[0])) right := *(*uint64)(unsafe.Pointer(&jrank.points[0])) if left < right { return true } else if left > right { return false } return (irank.item.Index() <= jrank.item.Index()) != tac } fzf-0.20.0/src/terminal.go000066400000000000000000001465231357617647500153470ustar00rootroot00000000000000package fzf import ( "bufio" "bytes" "fmt" "io" "io/ioutil" "os" "os/signal" "regexp" "sort" "strconv" "strings" "sync" "syscall" "time" "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" ) // import "github.com/pkg/profile" var placeholder *regexp.Regexp var activeTempFiles []string func init() { placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`) activeTempFiles = []string{} } type jumpMode int const ( jumpDisabled jumpMode = iota jumpEnabled jumpAcceptEnabled ) type previewer struct { text string lines int offset int enabled bool more bool } type itemLine struct { current bool selected bool label string queryLen int width int result Result } var emptyLine = itemLine{} // Terminal represents terminal input/output type Terminal struct { initDelay time.Duration infoStyle infoStyle prompt string promptLen int queryLen [2]int layout layoutType fullscreen bool hscroll bool hscrollOff int wordRubout string wordNext string cx int cy int offset int xoffset int yanked []rune input []rune multi int sort bool toggleSort bool delimiter Delimiter expect map[int]string keymap map[int][]action pressed string printQuery bool history *History cycle bool header []string header0 []string ansi bool tabstop int margin [4]sizeSpec strong tui.Attr unicode bool bordered bool cleanExit bool border tui.Window window tui.Window pborder tui.Window pwindow tui.Window count int progress int reading bool failed *string jumping jumpMode jumpLabels string printer func(string) printsep string merger *Merger selected map[int32]selectedItem version int64 reqBox *util.EventBox preview previewOpts previewer previewer previewBox *util.EventBox eventBox *util.EventBox mutex sync.Mutex initFunc func() prevLines []itemLine suppress bool startChan chan bool killChan chan int slab *util.Slab theme *tui.ColorTheme tui tui.Renderer } type selectedItem struct { at time.Time item *Item } type byTimeOrder []selectedItem func (a byTimeOrder) Len() int { return len(a) } func (a byTimeOrder) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a byTimeOrder) Less(i, j int) bool { return a[i].at.Before(a[j].at) } var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} const ( reqPrompt util.EventType = iota reqInfo reqHeader reqList reqJump reqRefresh reqReinit reqRedraw reqClose reqPrintQuery reqPreviewEnqueue reqPreviewDisplay reqPreviewRefresh reqQuit ) type action struct { t actionType a string } type actionType int const ( actIgnore actionType = iota actInvalid actRune actMouse actBeginningOfLine actAbort actAccept actAcceptNonEmpty actBackwardChar actBackwardDeleteChar actBackwardWord actCancel actClearScreen actClearQuery actClearSelection actDeleteChar actDeleteCharEOF actEndOfLine actForwardChar actForwardWord actKillLine actKillWord actUnixLineDiscard actUnixWordRubout actYank actBackwardKillWord actSelectAll actDeselectAll actToggle actToggleAll actToggleDown actToggleUp actToggleIn actToggleOut actDown actUp actPageUp actPageDown actHalfPageUp actHalfPageDown actJump actJumpAccept actPrintQuery actReplaceQuery actToggleSort actTogglePreview actTogglePreviewWrap actPreviewUp actPreviewDown actPreviewPageUp actPreviewPageDown actPreviousHistory actNextHistory actExecute actExecuteSilent actExecuteMulti // Deprecated actSigStop actTop actReload ) type placeholderFlags struct { plus bool preserveSpace bool number bool query bool file bool } type searchRequest struct { sort bool command *string } func toActions(types ...actionType) []action { actions := make([]action, len(types)) for idx, t := range types { actions[idx] = action{t: t, a: ""} } return actions } func defaultKeymap() map[int][]action { keymap := make(map[int][]action) keymap[tui.Invalid] = toActions(actInvalid) keymap[tui.Resize] = toActions(actClearScreen) keymap[tui.CtrlA] = toActions(actBeginningOfLine) keymap[tui.CtrlB] = toActions(actBackwardChar) keymap[tui.CtrlC] = toActions(actAbort) keymap[tui.CtrlG] = toActions(actAbort) keymap[tui.CtrlQ] = toActions(actAbort) keymap[tui.ESC] = toActions(actAbort) keymap[tui.CtrlD] = toActions(actDeleteCharEOF) keymap[tui.CtrlE] = toActions(actEndOfLine) keymap[tui.CtrlF] = toActions(actForwardChar) keymap[tui.CtrlH] = toActions(actBackwardDeleteChar) keymap[tui.BSpace] = toActions(actBackwardDeleteChar) keymap[tui.Tab] = toActions(actToggleDown) keymap[tui.BTab] = toActions(actToggleUp) keymap[tui.CtrlJ] = toActions(actDown) keymap[tui.CtrlK] = toActions(actUp) keymap[tui.CtrlL] = toActions(actClearScreen) keymap[tui.CtrlM] = toActions(actAccept) keymap[tui.CtrlN] = toActions(actDown) keymap[tui.CtrlP] = toActions(actUp) keymap[tui.CtrlU] = toActions(actUnixLineDiscard) keymap[tui.CtrlW] = toActions(actUnixWordRubout) keymap[tui.CtrlY] = toActions(actYank) if !util.IsWindows() { keymap[tui.CtrlZ] = toActions(actSigStop) } keymap[tui.AltB] = toActions(actBackwardWord) keymap[tui.SLeft] = toActions(actBackwardWord) keymap[tui.AltF] = toActions(actForwardWord) keymap[tui.SRight] = toActions(actForwardWord) keymap[tui.AltD] = toActions(actKillWord) keymap[tui.AltBS] = toActions(actBackwardKillWord) keymap[tui.Up] = toActions(actUp) keymap[tui.Down] = toActions(actDown) keymap[tui.Left] = toActions(actBackwardChar) keymap[tui.Right] = toActions(actForwardChar) keymap[tui.Home] = toActions(actBeginningOfLine) keymap[tui.End] = toActions(actEndOfLine) keymap[tui.Del] = toActions(actDeleteChar) keymap[tui.PgUp] = toActions(actPageUp) keymap[tui.PgDn] = toActions(actPageDown) keymap[tui.SUp] = toActions(actPreviewUp) keymap[tui.SDown] = toActions(actPreviewDown) keymap[tui.Rune] = toActions(actRune) keymap[tui.Mouse] = toActions(actMouse) keymap[tui.DoubleClick] = toActions(actAccept) keymap[tui.LeftClick] = toActions(actIgnore) keymap[tui.RightClick] = toActions(actToggle) return keymap } func trimQuery(query string) []rune { return []rune(strings.Replace(query, "\t", " ", -1)) } // NewTerminal returns new Terminal object func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { input := trimQuery(opts.Query) var header []string switch opts.Layout { case layoutDefault, layoutReverseList: header = reverseStringArray(opts.Header) default: header = opts.Header } var delay time.Duration if opts.Tac { delay = initialDelayTac } else { delay = initialDelay } var previewBox *util.EventBox if len(opts.Preview.command) > 0 { previewBox = util.NewEventBox() } strongAttr := tui.Bold if !opts.Bold { strongAttr = tui.AttrRegular } var renderer tui.Renderer fullscreen := opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100 if fullscreen { if tui.HasFullscreenRenderer() { renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) } else { renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, true, func(h int) int { return h }) } } else { maxHeightFunc := func(termHeight int) int { var maxHeight int if opts.Height.percent { maxHeight = util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight) } else { maxHeight = int(opts.Height.size) } effectiveMinHeight := minHeight if previewBox != nil && (opts.Preview.position == posUp || opts.Preview.position == posDown) { effectiveMinHeight *= 2 } if opts.InfoStyle != infoDefault { effectiveMinHeight -= 1 } if opts.Bordered { effectiveMinHeight += 2 } return util.Min(termHeight, util.Max(maxHeight, effectiveMinHeight)) } renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc) } wordRubout := "[^[:alnum:]][[:alnum:]]" wordNext := "[[:alnum:]][^[:alnum:]]|(.$)" if opts.FileWord { sep := regexp.QuoteMeta(string(os.PathSeparator)) wordRubout = fmt.Sprintf("%s[^%s]", sep, sep) wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep) } t := Terminal{ initDelay: delay, infoStyle: opts.InfoStyle, queryLen: [2]int{0, 0}, layout: opts.Layout, fullscreen: fullscreen, hscroll: opts.Hscroll, hscrollOff: opts.HscrollOff, wordRubout: wordRubout, wordNext: wordNext, cx: len(input), cy: 0, offset: 0, xoffset: 0, yanked: []rune{}, input: input, multi: opts.Multi, sort: opts.Sort > 0, toggleSort: opts.ToggleSort, delimiter: opts.Delimiter, expect: opts.Expect, keymap: opts.Keymap, pressed: "", printQuery: opts.PrintQuery, history: opts.History, margin: opts.Margin, unicode: opts.Unicode, bordered: opts.Bordered, cleanExit: opts.ClearOnExit, strong: strongAttr, cycle: opts.Cycle, header: header, header0: header, ansi: opts.Ansi, tabstop: opts.Tabstop, reading: true, failed: nil, jumping: jumpDisabled, jumpLabels: opts.JumpLabels, printer: opts.Printer, printsep: opts.PrintSep, merger: EmptyMerger, selected: make(map[int32]selectedItem), reqBox: util.NewEventBox(), preview: opts.Preview, previewer: previewer{"", 0, 0, previewBox != nil && !opts.Preview.hidden, false}, previewBox: previewBox, eventBox: eventBox, mutex: sync.Mutex{}, suppress: true, slab: util.MakeSlab(slab16Size, slab32Size), theme: opts.Theme, startChan: make(chan bool, 1), killChan: make(chan int), tui: renderer, initFunc: func() { renderer.Init() }} t.prompt, t.promptLen = t.processTabs([]rune(opts.Prompt), 0) return &t } func (t *Terminal) noInfoLine() bool { return t.infoStyle != infoDefault } // Input returns current query string func (t *Terminal) Input() []rune { t.mutex.Lock() defer t.mutex.Unlock() return copySlice(t.input) } // UpdateCount updates the count information func (t *Terminal) UpdateCount(cnt int, final bool, failedCommand *string) { t.mutex.Lock() t.count = cnt t.reading = !final t.failed = failedCommand t.mutex.Unlock() t.reqBox.Set(reqInfo, nil) if final { t.reqBox.Set(reqRefresh, nil) } } func reverseStringArray(input []string) []string { size := len(input) reversed := make([]string, size) for idx, str := range input { reversed[size-idx-1] = str } return reversed } // UpdateHeader updates the header func (t *Terminal) UpdateHeader(header []string) { t.mutex.Lock() t.header = append(append([]string{}, t.header0...), header...) t.mutex.Unlock() t.reqBox.Set(reqHeader, nil) } // UpdateProgress updates the search progress func (t *Terminal) UpdateProgress(progress float32) { t.mutex.Lock() newProgress := int(progress * 100) changed := t.progress != newProgress t.progress = newProgress t.mutex.Unlock() if changed { t.reqBox.Set(reqInfo, nil) } } // UpdateList updates Merger to display the list func (t *Terminal) UpdateList(merger *Merger, reset bool) { t.mutex.Lock() t.progress = 100 t.merger = merger if reset { t.selected = make(map[int32]selectedItem) } t.mutex.Unlock() t.reqBox.Set(reqInfo, nil) t.reqBox.Set(reqList, nil) } func (t *Terminal) output() bool { if t.printQuery { t.printer(string(t.input)) } if len(t.expect) > 0 { t.printer(t.pressed) } found := len(t.selected) > 0 if !found { current := t.currentItem() if current != nil { t.printer(current.AsString(t.ansi)) found = true } } else { for _, sel := range t.sortSelected() { t.printer(sel.item.AsString(t.ansi)) } } return found } func (t *Terminal) sortSelected() []selectedItem { sels := make([]selectedItem, 0, len(t.selected)) for _, sel := range t.selected { sels = append(sels, sel) } sort.Sort(byTimeOrder(sels)) return sels } func (t *Terminal) displayWidth(runes []rune) int { l := 0 for _, r := range runes { l += util.RuneWidth(r, l, t.tabstop) } return l } const ( minWidth = 16 minHeight = 4 maxDisplayWidthCalc = 1024 ) func calculateSize(base int, size sizeSpec, margin int, minSize int) int { max := base - margin if size.percent { return util.Constrain(int(float64(base)*0.01*size.size), minSize, max) } return util.Constrain(int(size.size), minSize, max) } func (t *Terminal) resizeWindows() { screenWidth := t.tui.MaxX() screenHeight := t.tui.MaxY() marginInt := [4]int{} t.prevLines = make([]itemLine, screenHeight) for idx, sizeSpec := range t.margin { if sizeSpec.percent { var max float64 if idx%2 == 0 { max = float64(screenHeight) } else { max = float64(screenWidth) } marginInt[idx] = int(max * sizeSpec.size * 0.01) } else { marginInt[idx] = int(sizeSpec.size) } if t.bordered && idx%2 == 0 { marginInt[idx] += 1 } } adjust := func(idx1 int, idx2 int, max int, min int) { if max >= min { margin := marginInt[idx1] + marginInt[idx2] if max-margin < min { desired := max - min marginInt[idx1] = desired * marginInt[idx1] / margin marginInt[idx2] = desired * marginInt[idx2] / margin } } } previewVisible := t.isPreviewEnabled() && t.preview.size.size > 0 minAreaWidth := minWidth minAreaHeight := minHeight if previewVisible { switch t.preview.position { case posUp, posDown: minAreaHeight *= 2 case posLeft, posRight: minAreaWidth *= 2 } } adjust(1, 3, screenWidth, minAreaWidth) adjust(0, 2, screenHeight, minAreaHeight) if t.border != nil { t.border.Close() } if t.window != nil { t.window.Close() } if t.pborder != nil { t.pborder.Close() t.pwindow.Close() } width := screenWidth - marginInt[1] - marginInt[3] height := screenHeight - marginInt[0] - marginInt[2] if t.bordered { t.border = t.tui.NewWindow( marginInt[0]-1, marginInt[3], width, height+2, false, tui.MakeBorderStyle(tui.BorderHorizontal, t.unicode)) } noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode) if previewVisible { createPreviewWindow := func(y int, x int, w int, h int) { previewBorder := tui.MakeBorderStyle(tui.BorderAround, t.unicode) if !t.preview.border { previewBorder = tui.MakeTransparentBorder() } t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder) pwidth := w - 4 // ncurses auto-wraps the line when the cursor reaches the right-end of // the window. To prevent unintended line-wraps, we use the width one // column larger than the desired value. if !t.preview.wrap && t.tui.DoesAutoWrap() { pwidth += 1 } t.pwindow = t.tui.NewWindow(y+1, x+2, pwidth, h-2, true, noBorder) } switch t.preview.position { case posUp: pheight := calculateSize(height, t.preview.size, minHeight, 3) t.window = t.tui.NewWindow( marginInt[0]+pheight, marginInt[3], width, height-pheight, false, noBorder) createPreviewWindow(marginInt[0], marginInt[3], width, pheight) case posDown: pheight := calculateSize(height, t.preview.size, minHeight, 3) t.window = t.tui.NewWindow( marginInt[0], marginInt[3], width, height-pheight, false, noBorder) createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight) case posLeft: pwidth := calculateSize(width, t.preview.size, minWidth, 5) t.window = t.tui.NewWindow( marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false, noBorder) createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) case posRight: pwidth := calculateSize(width, t.preview.size, minWidth, 5) t.window = t.tui.NewWindow( marginInt[0], marginInt[3], width-pwidth, height, false, noBorder) createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height) } } else { t.window = t.tui.NewWindow( marginInt[0], marginInt[3], width, height, false, noBorder) } for i := 0; i < t.window.Height(); i++ { t.window.MoveAndClear(i, 0) } } func (t *Terminal) move(y int, x int, clear bool) { h := t.window.Height() switch t.layout { case layoutDefault: y = h - y - 1 case layoutReverseList: n := 2 + len(t.header) if t.noInfoLine() { n-- } if y < n { y = h - y - 1 } else { y -= n } } if clear { t.window.MoveAndClear(y, x) } else { t.window.Move(y, x) } } func (t *Terminal) truncateQuery() { t.input, _ = t.trimRight(t.input, maxPatternLength) t.cx = util.Constrain(t.cx, 0, len(t.input)) } func (t *Terminal) updatePromptOffset() ([]rune, []rune) { maxWidth := util.Max(1, t.window.Width()-t.promptLen-1) _, overflow := t.trimLeft(t.input[:t.cx], maxWidth) minOffset := int(overflow) maxOffset := util.Min(util.Min(len(t.input), minOffset+maxWidth), t.cx) t.xoffset = util.Constrain(t.xoffset, minOffset, maxOffset) before, _ := t.trimLeft(t.input[t.xoffset:t.cx], maxWidth) beforeLen := t.displayWidth(before) after, _ := t.trimRight(t.input[t.cx:], maxWidth-beforeLen) afterLen := t.displayWidth(after) t.queryLen = [2]int{beforeLen, afterLen} return before, after } func (t *Terminal) placeCursor() { t.move(0, t.promptLen+t.queryLen[0], false) } func (t *Terminal) printPrompt() { t.move(0, 0, true) t.window.CPrint(tui.ColPrompt, t.strong, t.prompt) before, after := t.updatePromptOffset() t.window.CPrint(tui.ColNormal, t.strong, string(before)) t.window.CPrint(tui.ColNormal, t.strong, string(after)) } func (t *Terminal) printInfo() { pos := 0 switch t.infoStyle { case infoDefault: t.move(1, 0, true) if t.reading { duration := int64(spinnerDuration) idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration t.window.CPrint(tui.ColSpinner, t.strong, _spinner[idx]) } t.move(1, 2, false) pos = 2 case infoInline: pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1 if pos+len(" < ") > t.window.Width() { return } t.move(0, pos, true) if t.reading { t.window.CPrint(tui.ColSpinner, t.strong, " < ") } else { t.window.CPrint(tui.ColPrompt, t.strong, " < ") } pos += len(" < ") case infoHidden: return } found := t.merger.Length() total := util.Max(found, t.count) output := fmt.Sprintf("%d/%d", found, total) if t.toggleSort { if t.sort { output += " +S" } else { output += " -S" } } if len(t.selected) > 0 { if t.multi == maxMulti { output += fmt.Sprintf(" (%d)", len(t.selected)) } else { output += fmt.Sprintf(" (%d/%d)", len(t.selected), t.multi) } } if t.progress > 0 && t.progress < 100 { output += fmt.Sprintf(" (%d%%)", t.progress) } if t.failed != nil && t.count == 0 { output = fmt.Sprintf("[Command failed: %s]", *t.failed) } maxWidth := t.window.Width() - pos if len(output) > maxWidth { outputRunes, _ := t.trimRight([]rune(output), maxWidth-2) output = string(outputRunes) + ".." } t.window.CPrint(tui.ColInfo, 0, output) } func (t *Terminal) printHeader() { if len(t.header) == 0 { return } max := t.window.Height() var state *ansiState for idx, lineStr := range t.header { line := idx + 2 if t.noInfoLine() { line-- } if line >= max { continue } trimmed, colors, newState := extractColor(lineStr, state, nil) state = newState item := &Item{ text: util.ToChars([]byte(trimmed)), colors: colors} t.move(line, 2, true) t.printHighlighted(Result{item: item}, tui.AttrRegular, tui.ColHeader, tui.ColHeader, false, false) } } func (t *Terminal) printList() { t.constrain() maxy := t.maxItems() count := t.merger.Length() - t.offset for j := 0; j < maxy; j++ { i := j if t.layout == layoutDefault { i = maxy - 1 - j } line := i + 2 + len(t.header) if t.noInfoLine() { line-- } if i < count { t.printItem(t.merger.Get(i+t.offset), line, i, i == t.cy-t.offset) } else if t.prevLines[i] != emptyLine { t.prevLines[i] = emptyLine t.move(line, 0, true) } } } func (t *Terminal) printItem(result Result, line int, i int, current bool) { item := result.item _, selected := t.selected[item.Index()] label := " " if t.jumping != jumpDisabled { if i < len(t.jumpLabels) { // Striped current = i%2 == 0 label = t.jumpLabels[i : i+1] } } else if current { label = ">" } // Avoid unnecessary redraw newLine := itemLine{current: current, selected: selected, label: label, result: result, queryLen: len(t.input), width: 0} prevLine := t.prevLines[i] if prevLine.current == newLine.current && prevLine.selected == newLine.selected && prevLine.label == newLine.label && prevLine.queryLen == newLine.queryLen && prevLine.result == newLine.result { return } t.move(line, 0, false) if current { t.window.CPrint(tui.ColCurrentCursor, t.strong, label) if selected { t.window.CPrint(tui.ColCurrentSelected, t.strong, ">") } else { t.window.CPrint(tui.ColCurrentSelected, t.strong, " ") } newLine.width = t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true, true) } else { t.window.CPrint(tui.ColCursor, t.strong, label) if selected { t.window.CPrint(tui.ColSelected, t.strong, ">") } else { t.window.Print(" ") } newLine.width = t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false, true) } fillSpaces := prevLine.width - newLine.width if fillSpaces > 0 { t.window.Print(strings.Repeat(" ", fillSpaces)) } t.prevLines[i] = newLine } func (t *Terminal) trimRight(runes []rune, width int) ([]rune, int) { // We start from the beginning to handle tab characters l := 0 for idx, r := range runes { l += util.RuneWidth(r, l, t.tabstop) if l > width { return runes[:idx], len(runes) - idx } } return runes, 0 } func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int { l := 0 for _, r := range runes { l += util.RuneWidth(r, l+prefixWidth, t.tabstop) if l > limit { // Early exit return l } } return l } func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) { if len(runes) > maxDisplayWidthCalc && len(runes) > width { trimmed := len(runes) - width return runes[trimmed:], int32(trimmed) } currentWidth := t.displayWidth(runes) var trimmed int32 for currentWidth > width && len(runes) > 0 { runes = runes[1:] trimmed++ currentWidth = t.displayWidthWithLimit(runes, 2, width) } return runes, trimmed } func (t *Terminal) overflow(runes []rune, max int) bool { return t.displayWidthWithLimit(runes, 0, max) > max } func (t *Terminal) printHighlighted(result Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool, match bool) int { item := result.item // Overflow text := make([]rune, item.text.Length()) copy(text, item.text.ToRunes()) matchOffsets := []Offset{} var pos *[]int if match && t.merger.pattern != nil { _, matchOffsets, pos = t.merger.pattern.MatchItem(item, true, t.slab) } charOffsets := matchOffsets if pos != nil { charOffsets = make([]Offset, len(*pos)) for idx, p := range *pos { offset := Offset{int32(p), int32(p + 1)} charOffsets[idx] = offset } sort.Sort(ByOrder(charOffsets)) } var maxe int for _, offset := range charOffsets { maxe = util.Max(maxe, int(offset[1])) } offsets := result.colorOffsets(charOffsets, t.theme, col2, attr, current) maxWidth := t.window.Width() - 3 maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) displayWidth := t.displayWidthWithLimit(text, 0, maxWidth) if displayWidth > maxWidth { if t.hscroll { // Stri.. if !t.overflow(text[:maxe], maxWidth-2) { text, _ = t.trimRight(text, maxWidth-2) text = append(text, []rune("..")...) } else { // Stri.. if t.overflow(text[maxe:], 2) { text = append(text[:maxe], []rune("..")...) } // ..ri.. var diff int32 text, diff = t.trimLeft(text, maxWidth-2) // Transform offsets for idx, offset := range offsets { b, e := offset.offset[0], offset.offset[1] b += 2 - diff e += 2 - diff b = util.Max32(b, 2) offsets[idx].offset[0] = b offsets[idx].offset[1] = util.Max32(b, e) } text = append([]rune(".."), text...) } } else { text, _ = t.trimRight(text, maxWidth-2) text = append(text, []rune("..")...) for idx, offset := range offsets { offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-2)) offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth)) } } displayWidth = t.displayWidthWithLimit(text, 0, displayWidth) } var index int32 var substr string var prefixWidth int maxOffset := int32(len(text)) for _, offset := range offsets { b := util.Constrain32(offset.offset[0], index, maxOffset) e := util.Constrain32(offset.offset[1], index, maxOffset) substr, prefixWidth = t.processTabs(text[index:b], prefixWidth) t.window.CPrint(col1, attr, substr) if b < e { substr, prefixWidth = t.processTabs(text[b:e], prefixWidth) t.window.CPrint(offset.color, offset.attr, substr) } index = e if index >= maxOffset { break } } if index < maxOffset { substr, _ = t.processTabs(text[index:], prefixWidth) t.window.CPrint(col1, attr, substr) } return displayWidth } func (t *Terminal) printPreview() { if !t.hasPreviewWindow() { return } t.pwindow.Erase() maxWidth := t.pwindow.Width() if t.tui.DoesAutoWrap() { maxWidth -= 1 } reader := bufio.NewReader(strings.NewReader(t.previewer.text)) lineNo := -t.previewer.offset height := t.pwindow.Height() t.previewer.more = t.previewer.offset > 0 var ansi *ansiState for ; ; lineNo++ { line, err := reader.ReadString('\n') eof := err == io.EOF if !eof { line = line[:len(line)-1] } if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 { break } else if lineNo >= 0 { var fillRet tui.FillReturn prefixWidth := 0 _, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool { trimmed := []rune(str) if !t.preview.wrap { trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X()) } str, width := t.processTabs(trimmed, prefixWidth) prefixWidth += width if t.theme != nil && ansi != nil && ansi.colored() { fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str) } else { fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str) } return fillRet == tui.FillContinue }) t.previewer.more = t.previewer.more || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width() if fillRet == tui.FillNextLine { continue } else if fillRet == tui.FillSuspend { break } t.pwindow.Fill("\n") } if eof { break } } t.pwindow.FinishFill() if t.previewer.lines > height { t.previewer.more = true offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines) pos := t.pwindow.Width() - len(offset) if t.tui.DoesAutoWrap() { pos -= 1 } t.pwindow.Move(0, pos) t.pwindow.CPrint(tui.ColInfo, tui.Reverse, offset) } } func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) { var strbuf bytes.Buffer l := prefixWidth for _, r := range runes { w := util.RuneWidth(r, l, t.tabstop) l += w if r == '\t' { strbuf.WriteString(strings.Repeat(" ", w)) } else { strbuf.WriteRune(r) } } return strbuf.String(), l } func (t *Terminal) printAll() { t.resizeWindows() t.printList() t.printPrompt() t.printInfo() t.printHeader() t.printPreview() } func (t *Terminal) refresh() { t.placeCursor() if !t.suppress { windows := make([]tui.Window, 0, 4) if t.bordered { windows = append(windows, t.border) } if t.hasPreviewWindow() { windows = append(windows, t.pborder, t.pwindow) } windows = append(windows, t.window) t.tui.RefreshWindows(windows) } } func (t *Terminal) delChar() bool { if len(t.input) > 0 && t.cx < len(t.input) { t.input = append(t.input[:t.cx], t.input[t.cx+1:]...) return true } return false } func findLastMatch(pattern string, str string) int { rx, err := regexp.Compile(pattern) if err != nil { return -1 } locs := rx.FindAllStringIndex(str, -1) if locs == nil { return -1 } return locs[len(locs)-1][0] } func findFirstMatch(pattern string, str string) int { rx, err := regexp.Compile(pattern) if err != nil { return -1 } loc := rx.FindStringIndex(str) if loc == nil { return -1 } return loc[0] } func copySlice(slice []rune) []rune { ret := make([]rune, len(slice)) copy(ret, slice) return ret } func (t *Terminal) rubout(pattern string) { pcx := t.cx after := t.input[t.cx:] t.cx = findLastMatch(pattern, string(t.input[:t.cx])) + 1 t.yanked = copySlice(t.input[t.cx:pcx]) t.input = append(t.input[:t.cx], after...) } func keyMatch(key int, event tui.Event) bool { return event.Type == key || event.Type == tui.Rune && int(event.Char) == key-tui.AltZ || event.Type == tui.Mouse && key == tui.DoubleClick && event.MouseEvent.Double } func quoteEntryCmd(entry string) string { escaped := strings.Replace(entry, `\`, `\\`, -1) escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"` r, _ := regexp.Compile(`[&|<>()@^%!"]`) return r.ReplaceAllStringFunc(escaped, func(match string) string { return "^" + match }) } func quoteEntry(entry string) string { if util.IsWindows() { return quoteEntryCmd(entry) } return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" } func parsePlaceholder(match string) (bool, string, placeholderFlags) { flags := placeholderFlags{} if match[0] == '\\' { // Escaped placeholder pattern return true, match[1:], flags } skipChars := 1 for _, char := range match[1:] { switch char { case '+': flags.plus = true skipChars++ case 's': flags.preserveSpace = true skipChars++ case 'n': flags.number = true skipChars++ case 'f': flags.file = true skipChars++ case 'q': flags.query = true default: break } } matchWithoutFlags := "{" + match[skipChars:] return false, matchWithoutFlags, flags } func hasPreviewFlags(template string) (slot bool, plus bool, query bool) { for _, match := range placeholder.FindAllString(template, -1) { _, _, flags := parsePlaceholder(match) if flags.plus { plus = true } if flags.query { query = true } slot = true } return } func writeTemporaryFile(data []string, printSep string) string { f, err := ioutil.TempFile("", "fzf-preview-*") if err != nil { errorExit("Unable to create temporary file") } defer f.Close() f.WriteString(strings.Join(data, printSep)) f.WriteString(printSep) activeTempFiles = append(activeTempFiles, f.Name()) return f.Name() } func cleanTemporaryFiles() { for _, filename := range activeTempFiles { os.Remove(filename) } activeTempFiles = []string{} } func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string { current := allItems[:1] selected := allItems[1:] if current[0] == nil { current = []*Item{} } if selected[0] == nil { selected = []*Item{} } return placeholder.ReplaceAllStringFunc(template, func(match string) string { escaped, match, flags := parsePlaceholder(match) if escaped { return match } // Current query if match == "{q}" { return quoteEntry(query) } items := current if flags.plus || forcePlus { items = selected } replacements := make([]string, len(items)) if match == "{}" { for idx, item := range items { if flags.number { n := int(item.text.Index) if n < 0 { replacements[idx] = "" } else { replacements[idx] = strconv.Itoa(n) } } else if flags.file { replacements[idx] = item.AsString(stripAnsi) } else { replacements[idx] = quoteEntry(item.AsString(stripAnsi)) } } if flags.file { return writeTemporaryFile(replacements, printsep) } return strings.Join(replacements, " ") } tokens := strings.Split(match[1:len(match)-1], ",") ranges := make([]Range, len(tokens)) for idx, s := range tokens { r, ok := ParseRange(&s) if !ok { // Invalid expression, just return the original string in the template return match } ranges[idx] = r } for idx, item := range items { tokens := Tokenize(item.AsString(stripAnsi), delimiter) trans := Transform(tokens, ranges) str := joinTokens(trans) if delimiter.str != nil { str = strings.TrimSuffix(str, *delimiter.str) } else if delimiter.regex != nil { delims := delimiter.regex.FindAllStringIndex(str, -1) if len(delims) > 0 && delims[len(delims)-1][1] == len(str) { str = str[:delims[len(delims)-1][0]] } } if !flags.preserveSpace { str = strings.TrimSpace(str) } if !flags.file { str = quoteEntry(str) } replacements[idx] = str } if flags.file { return writeTemporaryFile(replacements, printsep) } return strings.Join(replacements, " ") }) } func (t *Terminal) redraw() { t.tui.Clear() t.tui.Refresh() t.printAll() } func (t *Terminal) executeCommand(template string, forcePlus bool, background bool) { valid, list := t.buildPlusList(template, forcePlus) if !valid { return } command := replacePlaceholder(template, t.ansi, t.delimiter, t.printsep, forcePlus, string(t.input), list) cmd := util.ExecCommand(command, false) if !background { cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr t.tui.Pause(true) cmd.Run() t.tui.Resume(true) t.redraw() t.refresh() } else { t.tui.Pause(false) cmd.Run() t.tui.Resume(false) } cleanTemporaryFiles() } func (t *Terminal) hasPreviewer() bool { return t.previewBox != nil } func (t *Terminal) isPreviewEnabled() bool { return t.hasPreviewer() && t.previewer.enabled } func (t *Terminal) hasPreviewWindow() bool { return t.pwindow != nil && t.isPreviewEnabled() } func (t *Terminal) currentItem() *Item { cnt := t.merger.Length() if t.cy >= 0 && cnt > 0 && cnt > t.cy { return t.merger.Get(t.cy).item } return nil } func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) { current := t.currentItem() _, plus, query := hasPreviewFlags(template) if !(query && len(t.input) > 0 || (forcePlus || plus) && len(t.selected) > 0) { return current != nil, []*Item{current, current} } // We would still want to update preview window even if there is no match if // 1. command template contains {q} and the query string is not empty // 2. or it contains {+} and we have more than one item already selected. // To do so, we pass an empty Item instead of nil to trigger an update. if current == nil { current = &minItem } var sels []*Item if len(t.selected) == 0 { sels = []*Item{current, current} } else { sels = make([]*Item, len(t.selected)+1) sels[0] = current for i, sel := range t.sortSelected() { sels[i+1] = sel.item } } return true, sels } func (t *Terminal) selectItem(item *Item) bool { if len(t.selected) >= t.multi { return false } if _, found := t.selected[item.Index()]; found { return true } t.selected[item.Index()] = selectedItem{time.Now(), item} t.version++ return true } func (t *Terminal) deselectItem(item *Item) { delete(t.selected, item.Index()) t.version++ } func (t *Terminal) toggleItem(item *Item) bool { if _, found := t.selected[item.Index()]; !found { return t.selectItem(item) } t.deselectItem(item) return true } func (t *Terminal) killPreview(code int) { select { case t.killChan <- code: default: if code != exitCancel { os.Exit(code) } } } func (t *Terminal) cancelPreview() { t.killPreview(exitCancel) } // Loop is called to start Terminal I/O func (t *Terminal) Loop() { // prof := profile.Start(profile.ProfilePath("/tmp/")) <-t.startChan { // Late initialization intChan := make(chan os.Signal, 1) signal.Notify(intChan, os.Interrupt, syscall.SIGTERM) go func() { <-intChan t.reqBox.Set(reqQuit, nil) }() contChan := make(chan os.Signal, 1) notifyOnCont(contChan) go func() { for { <-contChan t.reqBox.Set(reqReinit, nil) } }() resizeChan := make(chan os.Signal, 1) notifyOnResize(resizeChan) // Non-portable go func() { for { <-resizeChan t.reqBox.Set(reqRedraw, nil) } }() t.mutex.Lock() t.initFunc() t.resizeWindows() t.printPrompt() t.printInfo() t.printHeader() t.refresh() t.mutex.Unlock() go func() { timer := time.NewTimer(t.initDelay) <-timer.C t.reqBox.Set(reqRefresh, nil) }() // Keep the spinner spinning go func() { for { t.mutex.Lock() reading := t.reading t.mutex.Unlock() time.Sleep(spinnerDuration) if reading { t.reqBox.Set(reqInfo, nil) } } }() } if t.hasPreviewer() { go func() { for { var request []*Item t.previewBox.Wait(func(events *util.Events) { for req, value := range *events { switch req { case reqPreviewEnqueue: request = value.([]*Item) } } events.Clear() }) // We don't display preview window if no match if request[0] != nil { command := replacePlaceholder(t.preview.command, t.ansi, t.delimiter, t.printsep, false, string(t.Input()), request) cmd := util.ExecCommand(command, true) if t.pwindow != nil { env := os.Environ() lines := fmt.Sprintf("LINES=%d", t.pwindow.Height()) columns := fmt.Sprintf("COLUMNS=%d", t.pwindow.Width()) env = append(env, lines) env = append(env, "FZF_PREVIEW_"+lines) env = append(env, columns) env = append(env, "FZF_PREVIEW_"+columns) cmd.Env = env } var out bytes.Buffer cmd.Stdout = &out cmd.Stderr = &out cmd.Start() finishChan := make(chan bool, 1) updateChan := make(chan bool) go func() { select { case code := <-t.killChan: if code != exitCancel { util.KillCommand(cmd) os.Exit(code) } else { select { case <-time.After(previewCancelWait): util.KillCommand(cmd) updateChan <- true case <-finishChan: updateChan <- false } } case <-finishChan: updateChan <- false } }() cmd.Wait() finishChan <- true if out.Len() > 0 || !<-updateChan { t.reqBox.Set(reqPreviewDisplay, out.String()) } cleanTemporaryFiles() } else { t.reqBox.Set(reqPreviewDisplay, "") } } }() } exit := func(getCode func() int) { t.tui.Close() code := getCode() if code <= exitNoMatch && t.history != nil { t.history.append(string(t.input)) } // prof.Stop() t.killPreview(code) } go func() { var focusedIndex int32 = minItem.Index() var version int64 = -1 for { t.reqBox.Wait(func(events *util.Events) { defer events.Clear() t.mutex.Lock() for req, value := range *events { switch req { case reqPrompt: t.printPrompt() if t.noInfoLine() { t.printInfo() } case reqInfo: t.printInfo() case reqList: t.printList() var currentIndex int32 = minItem.Index() currentItem := t.currentItem() if currentItem != nil { currentIndex = currentItem.Index() } if focusedIndex != currentIndex || version != t.version { version = t.version focusedIndex = currentIndex if t.isPreviewEnabled() { _, list := t.buildPlusList(t.preview.command, false) t.cancelPreview() t.previewBox.Set(reqPreviewEnqueue, list) } } case reqJump: if t.merger.Length() == 0 { t.jumping = jumpDisabled } t.printList() case reqHeader: t.printHeader() case reqRefresh: t.suppress = false case reqReinit: t.tui.Resume(t.fullscreen) t.redraw() case reqRedraw: t.redraw() case reqClose: exit(func() int { if t.output() { return exitOk } return exitNoMatch }) case reqPreviewDisplay: t.previewer.text = value.(string) t.previewer.lines = strings.Count(t.previewer.text, "\n") t.previewer.offset = 0 t.printPreview() case reqPreviewRefresh: t.printPreview() case reqPrintQuery: exit(func() int { t.printer(string(t.input)) return exitOk }) case reqQuit: exit(func() int { return exitInterrupt }) } } t.refresh() t.mutex.Unlock() }) } }() looping := true for looping { var newCommand *string changed := false queryChanged := false event := t.tui.GetChar() t.mutex.Lock() previousInput := t.input previousCx := t.cx events := []util.EventType{} req := func(evts ...util.EventType) { for _, event := range evts { events = append(events, event) if event == reqClose || event == reqQuit { looping = false } } } toggle := func() bool { if t.cy < t.merger.Length() && t.toggleItem(t.merger.Get(t.cy).item) { req(reqInfo) return true } return false } scrollPreview := func(amount int) { if !t.previewer.more { return } newOffset := util.Constrain( t.previewer.offset+amount, 0, t.previewer.lines-1) if t.previewer.offset != newOffset { t.previewer.offset = newOffset req(reqPreviewRefresh) } } for key, ret := range t.expect { if keyMatch(key, event) { t.pressed = ret t.reqBox.Set(reqClose, nil) t.mutex.Unlock() return } } var doAction func(action, int) bool doActions := func(actions []action, mapkey int) bool { for _, action := range actions { if !doAction(action, mapkey) { return false } } return true } doAction = func(a action, mapkey int) bool { switch a.t { case actIgnore: case actExecute, actExecuteSilent: t.executeCommand(a.a, false, a.t == actExecuteSilent) case actExecuteMulti: t.executeCommand(a.a, true, false) case actInvalid: t.mutex.Unlock() return false case actTogglePreview: if t.hasPreviewer() { t.previewer.enabled = !t.previewer.enabled t.tui.Clear() t.resizeWindows() if t.previewer.enabled { valid, list := t.buildPlusList(t.preview.command, false) if valid { t.cancelPreview() t.previewBox.Set(reqPreviewEnqueue, list) } } req(reqPrompt, reqList, reqInfo, reqHeader) } case actTogglePreviewWrap: if t.hasPreviewWindow() { t.preview.wrap = !t.preview.wrap req(reqPreviewRefresh) } case actToggleSort: t.sort = !t.sort changed = true case actPreviewUp: if t.hasPreviewWindow() { scrollPreview(-1) } case actPreviewDown: if t.hasPreviewWindow() { scrollPreview(1) } case actPreviewPageUp: if t.hasPreviewWindow() { scrollPreview(-t.pwindow.Height()) } case actPreviewPageDown: if t.hasPreviewWindow() { scrollPreview(t.pwindow.Height()) } case actBeginningOfLine: t.cx = 0 case actBackwardChar: if t.cx > 0 { t.cx-- } case actPrintQuery: req(reqPrintQuery) case actReplaceQuery: if t.cy >= 0 && t.cy < t.merger.Length() { t.input = t.merger.Get(t.cy).item.text.ToRunes() t.cx = len(t.input) } case actAbort: req(reqQuit) case actDeleteChar: t.delChar() case actDeleteCharEOF: if !t.delChar() && t.cx == 0 { req(reqQuit) } case actEndOfLine: t.cx = len(t.input) case actCancel: if len(t.input) == 0 { req(reqQuit) } else { t.yanked = t.input t.input = []rune{} t.cx = 0 } case actForwardChar: if t.cx < len(t.input) { t.cx++ } case actBackwardDeleteChar: if t.cx > 0 { t.input = append(t.input[:t.cx-1], t.input[t.cx:]...) t.cx-- } case actSelectAll: if t.multi > 0 { for i := 0; i < t.merger.Length(); i++ { if !t.selectItem(t.merger.Get(i).item) { break } } req(reqList, reqInfo) } case actDeselectAll: if t.multi > 0 { for i := 0; i < t.merger.Length() && len(t.selected) > 0; i++ { t.deselectItem(t.merger.Get(i).item) } req(reqList, reqInfo) } case actToggle: if t.multi > 0 && t.merger.Length() > 0 && toggle() { req(reqList) } case actToggleAll: if t.multi > 0 { prevIndexes := make(map[int]struct{}) for i := 0; i < t.merger.Length() && len(t.selected) > 0; i++ { item := t.merger.Get(i).item if _, found := t.selected[item.Index()]; found { prevIndexes[i] = struct{}{} t.deselectItem(item) } } for i := 0; i < t.merger.Length(); i++ { if _, found := prevIndexes[i]; !found { item := t.merger.Get(i).item if !t.selectItem(item) { break } } } req(reqList, reqInfo) } case actToggleIn: if t.layout != layoutDefault { return doAction(action{t: actToggleUp}, mapkey) } return doAction(action{t: actToggleDown}, mapkey) case actToggleOut: if t.layout != layoutDefault { return doAction(action{t: actToggleDown}, mapkey) } return doAction(action{t: actToggleUp}, mapkey) case actToggleDown: if t.multi > 0 && t.merger.Length() > 0 && toggle() { t.vmove(-1, true) req(reqList) } case actToggleUp: if t.multi > 0 && t.merger.Length() > 0 && toggle() { t.vmove(1, true) req(reqList) } case actDown: t.vmove(-1, true) req(reqList) case actUp: t.vmove(1, true) req(reqList) case actAccept: req(reqClose) case actAcceptNonEmpty: if len(t.selected) > 0 || t.merger.Length() > 0 || !t.reading && t.count == 0 { req(reqClose) } case actClearScreen: req(reqRedraw) case actClearQuery: t.input = []rune{} t.cx = 0 case actClearSelection: if t.multi > 0 { t.selected = make(map[int32]selectedItem) t.version++ req(reqList, reqInfo) } case actTop: t.vset(0) req(reqList) case actUnixLineDiscard: if t.cx > 0 { t.yanked = copySlice(t.input[:t.cx]) t.input = t.input[t.cx:] t.cx = 0 } case actUnixWordRubout: if t.cx > 0 { t.rubout("\\s\\S") } case actBackwardKillWord: if t.cx > 0 { t.rubout(t.wordRubout) } case actYank: suffix := copySlice(t.input[t.cx:]) t.input = append(append(t.input[:t.cx], t.yanked...), suffix...) t.cx += len(t.yanked) case actPageUp: t.vmove(t.maxItems()-1, false) req(reqList) case actPageDown: t.vmove(-(t.maxItems() - 1), false) req(reqList) case actHalfPageUp: t.vmove(t.maxItems()/2, false) req(reqList) case actHalfPageDown: t.vmove(-(t.maxItems() / 2), false) req(reqList) case actJump: t.jumping = jumpEnabled req(reqJump) case actJumpAccept: t.jumping = jumpAcceptEnabled req(reqJump) case actBackwardWord: t.cx = findLastMatch(t.wordRubout, string(t.input[:t.cx])) + 1 case actForwardWord: t.cx += findFirstMatch(t.wordNext, string(t.input[t.cx:])) + 1 case actKillWord: ncx := t.cx + findFirstMatch(t.wordNext, string(t.input[t.cx:])) + 1 if ncx > t.cx { t.yanked = copySlice(t.input[t.cx:ncx]) t.input = append(t.input[:t.cx], t.input[ncx:]...) } case actKillLine: if t.cx < len(t.input) { t.yanked = copySlice(t.input[t.cx:]) t.input = t.input[:t.cx] } case actRune: prefix := copySlice(t.input[:t.cx]) t.input = append(append(prefix, event.Char), t.input[t.cx:]...) t.cx++ case actPreviousHistory: if t.history != nil { t.history.override(string(t.input)) t.input = trimQuery(t.history.previous()) t.cx = len(t.input) } case actNextHistory: if t.history != nil { t.history.override(string(t.input)) t.input = trimQuery(t.history.next()) t.cx = len(t.input) } case actSigStop: p, err := os.FindProcess(os.Getpid()) if err == nil { t.tui.Clear() t.tui.Pause(t.fullscreen) notifyStop(p) t.mutex.Unlock() return false } case actMouse: me := event.MouseEvent mx, my := me.X, me.Y if me.S != 0 { // Scroll if t.window.Enclose(my, mx) && t.merger.Length() > 0 { if t.multi > 0 && me.Mod { toggle() } t.vmove(me.S, true) req(reqList) } else if t.hasPreviewWindow() && t.pwindow.Enclose(my, mx) { scrollPreview(-me.S) } } else if t.window.Enclose(my, mx) { mx -= t.window.Left() my -= t.window.Top() mx = util.Constrain(mx-t.promptLen, 0, len(t.input)) min := 2 + len(t.header) if t.noInfoLine() { min-- } h := t.window.Height() switch t.layout { case layoutDefault: my = h - my - 1 case layoutReverseList: if my < h-min { my += min } else { my = h - my - 1 } } if me.Double { // Double-click if my >= min { if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { return doActions(t.keymap[tui.DoubleClick], tui.DoubleClick) } } } else if me.Down { if my == 0 && mx >= 0 { // Prompt t.cx = mx + t.xoffset } else if my >= min { // List if t.vset(t.offset+my-min) && t.multi > 0 && me.Mod { toggle() } req(reqList) if me.Left { return doActions(t.keymap[tui.LeftClick], tui.LeftClick) } return doActions(t.keymap[tui.RightClick], tui.RightClick) } } } case actReload: t.failed = nil valid, list := t.buildPlusList(a.a, false) if !valid { // We run the command even when there's no match // 1. If the template doesn't have any slots // 2. If the template has {q} slot, _, query := hasPreviewFlags(a.a) valid = !slot || query } if valid { command := replacePlaceholder(a.a, t.ansi, t.delimiter, t.printsep, false, string(t.input), list) newCommand = &command } } return true } mapkey := event.Type if t.jumping == jumpDisabled { actions := t.keymap[mapkey] if mapkey == tui.Rune { mapkey = int(event.Char) + int(tui.AltZ) if act, prs := t.keymap[mapkey]; prs { actions = act } } if !doActions(actions, mapkey) { continue } t.truncateQuery() queryChanged = string(previousInput) != string(t.input) changed = changed || queryChanged if onChanges, prs := t.keymap[tui.Change]; queryChanged && prs { if !doActions(onChanges, tui.Change) { continue } } } else { if mapkey == tui.Rune { if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() { t.cy = idx + t.offset if t.jumping == jumpAcceptEnabled { req(reqClose) } } } t.jumping = jumpDisabled req(reqList) } if queryChanged { if t.isPreviewEnabled() { _, _, q := hasPreviewFlags(t.preview.command) if q { t.version++ } } } if queryChanged || t.cx != previousCx { req(reqPrompt) } t.mutex.Unlock() // Must be unlocked before touching reqBox if changed || newCommand != nil { t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, command: newCommand}) } for _, event := range events { t.reqBox.Set(event, nil) } } } func (t *Terminal) constrain() { count := t.merger.Length() height := t.maxItems() diffpos := t.cy - t.offset t.cy = util.Constrain(t.cy, 0, count-1) t.offset = util.Constrain(t.offset, t.cy-height+1, t.cy) // Adjustment if count-t.offset < height { t.offset = util.Max(0, count-height) t.cy = util.Constrain(t.offset+diffpos, 0, count-1) } t.offset = util.Max(0, t.offset) } func (t *Terminal) vmove(o int, allowCycle bool) { if t.layout != layoutDefault { o *= -1 } dest := t.cy + o if t.cycle && allowCycle { max := t.merger.Length() - 1 if dest > max { if t.cy == max { dest = 0 } } else if dest < 0 { if t.cy == 0 { dest = max } } } t.vset(dest) } func (t *Terminal) vset(o int) bool { t.cy = util.Constrain(o, 0, t.merger.Length()-1) return t.cy == o } func (t *Terminal) maxItems() int { max := t.window.Height() - 2 - len(t.header) if t.noInfoLine() { max++ } return util.Max(max, 0) } fzf-0.20.0/src/terminal_test.go000066400000000000000000000130751357617647500164010ustar00rootroot00000000000000package fzf import ( "regexp" "testing" "github.com/junegunn/fzf/src/util" ) func newItem(str string) *Item { bytes := []byte(str) trimmed, _, _ := extractColor(str, nil, nil) return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))} } func TestReplacePlaceholder(t *testing.T) { item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m") items1 := []*Item{item1, item1} items2 := []*Item{ newItem("foo'bar \x1b[31mbaz\x1b[m"), newItem("foo'bar \x1b[31mbaz\x1b[m"), newItem("FOO'BAR \x1b[31mBAZ\x1b[m")} delim := "'" var regex *regexp.Regexp var result string check := func(expected string) { if result != expected { t.Errorf("expected: %s, actual: %s", expected, result) } } printsep := "\n" // {}, preserve ansi result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1) check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'") // {}, strip ansi result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1) check("echo ' foo'\\''bar baz'") // {}, with multiple items result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2) check("echo 'foo'\\''bar baz'") // {..}, strip leading whitespaces, preserve ansi result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1) check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'") // {..}, strip leading whitespaces, strip ansi result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1) check("echo 'foo'\\''bar baz'") // {q} result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1) check("echo ' foo'\\''bar baz' 'query'") // {q}, multiple items result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2) check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'") result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2) check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'") result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1) check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''") result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2) check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''") result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2) check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''") // forcePlus result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2) check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''") // Whitespace preserving flag with "'" delimiter result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1) check("echo ' foo'") result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) check("echo 'bar baz'") result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1) check("echo ' foo'\\''bar baz'") result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1) check("echo ' foo'\\''bar baz'") // Whitespace preserving flag with regex delimiter regex = regexp.MustCompile(`\w+`) result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1) check("echo ' '") result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1) check("echo ''\\'''") result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) check("echo ' '") // No match result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil}) check("echo /") // No match, but with selections result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1}) check("echo /' foo'\\''bar baz'") // String delimiter result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) check("echo ' foo'\\''bar baz'/'foo'/'bar baz'") // Regex delimiter regex = regexp.MustCompile("[oa]+") // foo'bar baz result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'") } func TestQuoteEntryCmd(t *testing.T) { tests := map[string]string{ `"`: `^"\^"^"`, `\`: `^"\\^"`, `\"`: `^"\\\^"^"`, `"\\\"`: `^"\^"\\\\\\\^"^"`, `&|<>()@^%!`: `^"^&^|^<^>^(^)^@^^^%^!^"`, `%USERPROFILE%`: `^"^%USERPROFILE^%^"`, `C:\Program Files (x86)\`: `^"C:\\Program Files ^(x86^)\\^"`, } for input, expected := range tests { escaped := quoteEntryCmd(input) if escaped != expected { t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped) } } } fzf-0.20.0/src/terminal_unix.go000066400000000000000000000005141357617647500163770ustar00rootroot00000000000000// +build !windows package fzf import ( "os" "os/signal" "syscall" ) func notifyOnResize(resizeChan chan<- os.Signal) { signal.Notify(resizeChan, syscall.SIGWINCH) } func notifyStop(p *os.Process) { p.Signal(syscall.SIGSTOP) } func notifyOnCont(resizeChan chan<- os.Signal) { signal.Notify(resizeChan, syscall.SIGCONT) } fzf-0.20.0/src/terminal_windows.go000066400000000000000000000003321357617647500171040ustar00rootroot00000000000000// +build windows package fzf import ( "os" ) func notifyOnResize(resizeChan chan<- os.Signal) { // TODO } func notifyStop(p *os.Process) { // NOOP } func notifyOnCont(resizeChan chan<- os.Signal) { // NOOP } fzf-0.20.0/src/tokenizer.go000066400000000000000000000130061357617647500155330ustar00rootroot00000000000000package fzf import ( "bytes" "fmt" "regexp" "strconv" "strings" "github.com/junegunn/fzf/src/util" ) const rangeEllipsis = 0 // Range represents nth-expression type Range struct { begin int end int } // Token contains the tokenized part of the strings and its prefix length type Token struct { text *util.Chars prefixLength int32 } // String returns the string representation of a Token. func (t Token) String() string { return fmt.Sprintf("Token{text: %s, prefixLength: %d}", t.text, t.prefixLength) } // Delimiter for tokenizing the input type Delimiter struct { regex *regexp.Regexp str *string } // String returns the string representation of a Delimeter. func (d Delimiter) String() string { return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str) } func newRange(begin int, end int) Range { if begin == 1 { begin = rangeEllipsis } if end == -1 { end = rangeEllipsis } return Range{begin, end} } // ParseRange parses nth-expression and returns the corresponding Range object func ParseRange(str *string) (Range, bool) { if (*str) == ".." { return newRange(rangeEllipsis, rangeEllipsis), true } else if strings.HasPrefix(*str, "..") { end, err := strconv.Atoi((*str)[2:]) if err != nil || end == 0 { return Range{}, false } return newRange(rangeEllipsis, end), true } else if strings.HasSuffix(*str, "..") { begin, err := strconv.Atoi((*str)[:len(*str)-2]) if err != nil || begin == 0 { return Range{}, false } return newRange(begin, rangeEllipsis), true } else if strings.Contains(*str, "..") { ns := strings.Split(*str, "..") if len(ns) != 2 { return Range{}, false } begin, err1 := strconv.Atoi(ns[0]) end, err2 := strconv.Atoi(ns[1]) if err1 != nil || err2 != nil || begin == 0 || end == 0 { return Range{}, false } return newRange(begin, end), true } n, err := strconv.Atoi(*str) if err != nil || n == 0 { return Range{}, false } return newRange(n, n), true } func withPrefixLengths(tokens []string, begin int) []Token { ret := make([]Token, len(tokens)) prefixLength := begin for idx := range tokens { chars := util.ToChars([]byte(tokens[idx])) ret[idx] = Token{&chars, int32(prefixLength)} prefixLength += chars.Length() } return ret } const ( awkNil = iota awkBlack awkWhite ) func awkTokenizer(input string) ([]string, int) { // 9, 32 ret := []string{} prefixLength := 0 state := awkNil begin := 0 end := 0 for idx := 0; idx < len(input); idx++ { r := input[idx] white := r == 9 || r == 32 switch state { case awkNil: if white { prefixLength++ } else { state, begin, end = awkBlack, idx, idx+1 } case awkBlack: end = idx + 1 if white { state = awkWhite } case awkWhite: if white { end = idx + 1 } else { ret = append(ret, input[begin:end]) state, begin, end = awkBlack, idx, idx+1 } } } if begin < end { ret = append(ret, input[begin:end]) } return ret, prefixLength } // Tokenize tokenizes the given string with the delimiter func Tokenize(text string, delimiter Delimiter) []Token { if delimiter.str == nil && delimiter.regex == nil { // AWK-style (\S+\s*) tokens, prefixLength := awkTokenizer(text) return withPrefixLengths(tokens, prefixLength) } if delimiter.str != nil { return withPrefixLengths(strings.SplitAfter(text, *delimiter.str), 0) } // FIXME performance var tokens []string if delimiter.regex != nil { for len(text) > 0 { loc := delimiter.regex.FindStringIndex(text) if len(loc) < 2 { loc = []int{0, len(text)} } last := util.Max(loc[1], 1) tokens = append(tokens, text[:last]) text = text[last:] } } return withPrefixLengths(tokens, 0) } func joinTokens(tokens []Token) string { var output bytes.Buffer for _, token := range tokens { output.WriteString(token.text.ToString()) } return output.String() } // Transform is used to transform the input when --with-nth option is given func Transform(tokens []Token, withNth []Range) []Token { transTokens := make([]Token, len(withNth)) numTokens := len(tokens) for idx, r := range withNth { parts := []*util.Chars{} minIdx := 0 if r.begin == r.end { idx := r.begin if idx == rangeEllipsis { chars := util.ToChars([]byte(joinTokens(tokens))) parts = append(parts, &chars) } else { if idx < 0 { idx += numTokens + 1 } if idx >= 1 && idx <= numTokens { minIdx = idx - 1 parts = append(parts, tokens[idx-1].text) } } } else { var begin, end int if r.begin == rangeEllipsis { // ..N begin, end = 1, r.end if end < 0 { end += numTokens + 1 } } else if r.end == rangeEllipsis { // N.. begin, end = r.begin, numTokens if begin < 0 { begin += numTokens + 1 } } else { begin, end = r.begin, r.end if begin < 0 { begin += numTokens + 1 } if end < 0 { end += numTokens + 1 } } minIdx = util.Max(0, begin-1) for idx := begin; idx <= end; idx++ { if idx >= 1 && idx <= numTokens { parts = append(parts, tokens[idx-1].text) } } } // Merge multiple parts var merged util.Chars switch len(parts) { case 0: merged = util.ToChars([]byte{}) case 1: merged = *parts[0] default: var output bytes.Buffer for _, part := range parts { output.WriteString(part.ToString()) } merged = util.ToChars(output.Bytes()) } var prefixLength int32 if minIdx < numTokens { prefixLength = tokens[minIdx].prefixLength } else { prefixLength = 0 } transTokens[idx] = Token{&merged, prefixLength} } return transTokens } fzf-0.20.0/src/tokenizer_test.go000066400000000000000000000054521357617647500166000ustar00rootroot00000000000000package fzf import ( "testing" ) func TestParseRange(t *testing.T) { { i := ".." r, _ := ParseRange(&i) if r.begin != rangeEllipsis || r.end != rangeEllipsis { t.Errorf("%v", r) } } { i := "3.." r, _ := ParseRange(&i) if r.begin != 3 || r.end != rangeEllipsis { t.Errorf("%v", r) } } { i := "3..5" r, _ := ParseRange(&i) if r.begin != 3 || r.end != 5 { t.Errorf("%v", r) } } { i := "-3..-5" r, _ := ParseRange(&i) if r.begin != -3 || r.end != -5 { t.Errorf("%v", r) } } { i := "3" r, _ := ParseRange(&i) if r.begin != 3 || r.end != 3 { t.Errorf("%v", r) } } } func TestTokenize(t *testing.T) { // AWK-style input := " abc: def: ghi " tokens := Tokenize(input, Delimiter{}) if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 { t.Errorf("%s", tokens) } // With delimiter tokens = Tokenize(input, delimiterRegexp(":")) if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 { t.Error(tokens[0].text.ToString(), tokens[0].prefixLength) } // With delimiter regex tokens = Tokenize(input, delimiterRegexp("\\s+")) if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 || tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 || tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 || tokens[3].text.ToString() != "ghi " || tokens[3].prefixLength != 14 { t.Errorf("%s", tokens) } } func TestTransform(t *testing.T) { input := " abc: def: ghi: jkl" { tokens := Tokenize(input, Delimiter{}) { ranges := splitNth("1,2,3") tx := Transform(tokens, ranges) if joinTokens(tx) != "abc: def: ghi: " { t.Errorf("%s", tx) } } { ranges := splitNth("1..2,3,2..,1") tx := Transform(tokens, ranges) if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " || len(tx) != 4 || tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 || tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 || tx[2].text.ToString() != "def: ghi: jkl" || tx[2].prefixLength != 8 || tx[3].text.ToString() != "abc: " || tx[3].prefixLength != 2 { t.Errorf("%s", tx) } } } { tokens := Tokenize(input, delimiterRegexp(":")) { ranges := splitNth("1..2,3,2..,1") tx := Transform(tokens, ranges) if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" || len(tx) != 4 || tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 || tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 || tx[2].text.ToString() != " def: ghi: jkl" || tx[2].prefixLength != 6 || tx[3].text.ToString() != " abc:" || tx[3].prefixLength != 0 { t.Errorf("%s", tx) } } } } func TestTransformIndexOutOfBounds(t *testing.T) { Transform([]Token{}, splitNth("1")) } fzf-0.20.0/src/tui/000077500000000000000000000000001357617647500137735ustar00rootroot00000000000000fzf-0.20.0/src/tui/dummy.go000066400000000000000000000022511357617647500154550ustar00rootroot00000000000000// +build !ncurses // +build !tcell // +build !windows package tui type Attr int func HasFullscreenRenderer() bool { return false } func (a Attr) Merge(b Attr) Attr { return a | b } const ( AttrRegular Attr = Attr(0) Bold = Attr(1) Dim = Attr(1 << 1) Italic = Attr(1 << 2) Underline = Attr(1 << 3) Blink = Attr(1 << 4) Blink2 = Attr(1 << 5) Reverse = Attr(1 << 6) ) func (r *FullscreenRenderer) Init() {} func (r *FullscreenRenderer) Pause(bool) {} func (r *FullscreenRenderer) Resume(bool) {} func (r *FullscreenRenderer) Clear() {} func (r *FullscreenRenderer) Refresh() {} func (r *FullscreenRenderer) Close() {} func (r *FullscreenRenderer) DoesAutoWrap() bool { return false } func (r *FullscreenRenderer) GetChar() Event { return Event{} } func (r *FullscreenRenderer) MaxX() int { return 0 } func (r *FullscreenRenderer) MaxY() int { return 0 } func (r *FullscreenRenderer) RefreshWindows(windows []Window) {} func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window { return nil } fzf-0.20.0/src/tui/light.go000066400000000000000000000502421357617647500154340ustar00rootroot00000000000000package tui import ( "fmt" "os" "os/exec" "regexp" "strconv" "strings" "syscall" "time" "unicode/utf8" "github.com/junegunn/fzf/src/util" "golang.org/x/crypto/ssh/terminal" ) const ( defaultWidth = 80 defaultHeight = 24 defaultEscDelay = 100 escPollInterval = 5 offsetPollTries = 10 ) const consoleDevice string = "/dev/tty" var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R") func openTtyIn() *os.File { in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) if err != nil { tty := ttyname() if len(tty) > 0 { if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil { return in } } fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice) os.Exit(2) } return in } func (r *LightRenderer) stderr(str string) { r.stderrInternal(str, true) } // FIXME: Need better handling of non-displayable characters func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) { bytes := []byte(str) runes := []rune{} for len(bytes) > 0 { r, sz := utf8.DecodeRune(bytes) nlcr := r == '\n' || r == '\r' if r >= 32 || r == '\x1b' || nlcr { if r == utf8.RuneError || nlcr && !allowNLCR { runes = append(runes, ' ') } else { runes = append(runes, r) } } bytes = bytes[sz:] } r.queued += string(runes) } func (r *LightRenderer) csi(code string) { r.stderr("\x1b[" + code) } func (r *LightRenderer) flush() { if len(r.queued) > 0 { fmt.Fprint(os.Stderr, r.queued) r.queued = "" } } // Light renderer type LightRenderer struct { theme *ColorTheme mouse bool forceBlack bool clearOnExit bool prevDownTime time.Time clickY []int ttyin *os.File buffer []byte origState *terminal.State width int height int yoffset int tabstop int escDelay int fullscreen bool upOneLine bool queued string y int x int maxHeightFunc func(int) int } type LightWindow struct { renderer *LightRenderer colored bool border BorderStyle top int left int width int height int posx int posy int tabstop int fg Color bg Color } func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) Renderer { r := LightRenderer{ theme: theme, forceBlack: forceBlack, mouse: mouse, clearOnExit: clearOnExit, ttyin: openTtyIn(), yoffset: 0, tabstop: tabstop, fullscreen: fullscreen, upOneLine: false, maxHeightFunc: maxHeightFunc} return &r } func (r *LightRenderer) fd() int { return int(r.ttyin.Fd()) } func (r *LightRenderer) defaultTheme() *ColorTheme { if strings.Contains(os.Getenv("TERM"), "256") { return Dark256 } colors, err := exec.Command("tput", "colors").Output() if err == nil && atoi(strings.TrimSpace(string(colors)), 16) > 16 { return Dark256 } return Default16 } func (r *LightRenderer) findOffset() (row int, col int) { r.csi("6n") r.flush() bytes := []byte{} for tries := 0; tries < offsetPollTries; tries++ { bytes = r.getBytesInternal(bytes, tries > 0) offsets := offsetRegexp.FindSubmatch(bytes) if len(offsets) > 3 { // add anything we skipped over to the input buffer r.buffer = append(r.buffer, offsets[1]...) return atoi(string(offsets[2]), 0) - 1, atoi(string(offsets[3]), 0) - 1 } } return -1, -1 } func repeat(r rune, times int) string { if times > 0 { return strings.Repeat(string(r), times) } return "" } func atoi(s string, defaultValue int) int { value, err := strconv.Atoi(s) if err != nil { return defaultValue } return value } func (r *LightRenderer) Init() { r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay) fd := r.fd() origState, err := terminal.GetState(fd) if err != nil { errorExit(err.Error()) } r.origState = origState terminal.MakeRaw(fd) r.updateTerminalSize() initTheme(r.theme, r.defaultTheme(), r.forceBlack) if r.fullscreen { r.smcup() } else { // We assume that --no-clear is used for repetitive relaunching of fzf. // So we do not clear the lower bottom of the screen. if r.clearOnExit { r.csi("J") } y, x := r.findOffset() r.mouse = r.mouse && y >= 0 // When --no-clear is used for repetitive relaunching, there is a small // time frame between fzf processes where the user keystrokes are not // captured by either of fzf process which can cause x offset to be // increased and we're left with unwanted extra new line. if x > 0 && r.clearOnExit { r.upOneLine = true r.makeSpace() } for i := 1; i < r.MaxY(); i++ { r.makeSpace() } } if r.mouse { r.csi("?1000h") } r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) r.csi("G") r.csi("K") if !r.clearOnExit && !r.fullscreen { r.csi("s") } if !r.fullscreen && r.mouse { r.yoffset, _ = r.findOffset() } } func (r *LightRenderer) makeSpace() { r.stderr("\n") r.csi("G") } func (r *LightRenderer) move(y int, x int) { // w.csi("u") if r.y < y { r.csi(fmt.Sprintf("%dB", y-r.y)) } else if r.y > y { r.csi(fmt.Sprintf("%dA", r.y-y)) } r.stderr("\r") if x > 0 { r.csi(fmt.Sprintf("%dC", x)) } r.y = y r.x = x } func (r *LightRenderer) origin() { r.move(0, 0) } func getEnv(name string, defaultValue int) int { env := os.Getenv(name) if len(env) == 0 { return defaultValue } return atoi(env, defaultValue) } func (r *LightRenderer) updateTerminalSize() { width, height, err := terminal.GetSize(r.fd()) if err == nil { r.width = width r.height = r.maxHeightFunc(height) } else { r.width = getEnv("COLUMNS", defaultWidth) r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight)) } } func (r *LightRenderer) getch(nonblock bool) (int, bool) { b := make([]byte, 1) fd := r.fd() util.SetNonblock(r.ttyin, nonblock) _, err := util.Read(fd, b) if err != nil { return 0, false } return int(b[0]), true } func (r *LightRenderer) getBytes() []byte { return r.getBytesInternal(r.buffer, false) } func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte { c, ok := r.getch(nonblock) if !nonblock && !ok { r.Close() errorExit("Failed to read " + consoleDevice) } retries := 0 if c == ESC || nonblock { retries = r.escDelay / escPollInterval } buffer = append(buffer, byte(c)) pc := c for { c, ok = r.getch(true) if !ok { if retries > 0 { retries-- time.Sleep(escPollInterval * time.Millisecond) continue } break } else if c == ESC && pc != c { retries = r.escDelay / escPollInterval } else { retries = 0 } buffer = append(buffer, byte(c)) pc = c } return buffer } func (r *LightRenderer) GetChar() Event { if len(r.buffer) == 0 { r.buffer = r.getBytes() } if len(r.buffer) == 0 { panic("Empty buffer") } sz := 1 defer func() { r.buffer = r.buffer[sz:] }() switch r.buffer[0] { case CtrlC: return Event{CtrlC, 0, nil} case CtrlG: return Event{CtrlG, 0, nil} case CtrlQ: return Event{CtrlQ, 0, nil} case 127: return Event{BSpace, 0, nil} case 0: return Event{CtrlSpace, 0, nil} case 28: return Event{CtrlBackSlash, 0, nil} case 29: return Event{CtrlRightBracket, 0, nil} case 30: return Event{CtrlCaret, 0, nil} case 31: return Event{CtrlSlash, 0, nil} case ESC: ev := r.escSequence(&sz) // Second chance if ev.Type == Invalid { r.buffer = r.getBytes() ev = r.escSequence(&sz) } return ev } // CTRL-A ~ CTRL-Z if r.buffer[0] <= CtrlZ { return Event{int(r.buffer[0]), 0, nil} } char, rsz := utf8.DecodeRune(r.buffer) if char == utf8.RuneError { return Event{ESC, 0, nil} } sz = rsz return Event{Rune, char, nil} } func (r *LightRenderer) escSequence(sz *int) Event { if len(r.buffer) < 2 { return Event{ESC, 0, nil} } *sz = 2 if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 { return Event{int(CtrlAltA + r.buffer[1] - 1), 0, nil} } alt := false if len(r.buffer) > 2 && r.buffer[1] == ESC { r.buffer = r.buffer[1:] alt = true } switch r.buffer[1] { case ESC: return Event{ESC, 0, nil} case 32: return Event{AltSpace, 0, nil} case 47: return Event{AltSlash, 0, nil} case 98: return Event{AltB, 0, nil} case 100: return Event{AltD, 0, nil} case 102: return Event{AltF, 0, nil} case 127: return Event{AltBS, 0, nil} case 91, 79: if len(r.buffer) < 3 { return Event{Invalid, 0, nil} } *sz = 3 switch r.buffer[2] { case 68: if alt { return Event{AltLeft, 0, nil} } return Event{Left, 0, nil} case 67: if alt { // Ugh.. return Event{AltRight, 0, nil} } return Event{Right, 0, nil} case 66: if alt { return Event{AltDown, 0, nil} } return Event{Down, 0, nil} case 65: if alt { return Event{AltUp, 0, nil} } return Event{Up, 0, nil} case 90: return Event{BTab, 0, nil} case 72: return Event{Home, 0, nil} case 70: return Event{End, 0, nil} case 77: return r.mouseSequence(sz) case 80: return Event{F1, 0, nil} case 81: return Event{F2, 0, nil} case 82: return Event{F3, 0, nil} case 83: return Event{F4, 0, nil} case 49, 50, 51, 52, 53, 54: if len(r.buffer) < 4 { return Event{Invalid, 0, nil} } *sz = 4 switch r.buffer[2] { case 50: if len(r.buffer) == 5 && r.buffer[4] == 126 { *sz = 5 switch r.buffer[3] { case 48: return Event{F9, 0, nil} case 49: return Event{F10, 0, nil} case 51: return Event{F11, 0, nil} case 52: return Event{F12, 0, nil} } } // Bracketed paste mode: \e[200~ ... \e[201~ if r.buffer[3] == '0' && (r.buffer[4] == '0' || r.buffer[4] == '1') && r.buffer[5] == '~' { // Immediately discard the sequence from the buffer and reread input r.buffer = r.buffer[6:] *sz = 0 return r.GetChar() } return Event{Invalid, 0, nil} // INS case 51: return Event{Del, 0, nil} case 52: return Event{End, 0, nil} case 53: return Event{PgUp, 0, nil} case 54: return Event{PgDn, 0, nil} case 49: switch r.buffer[3] { case 126: return Event{Home, 0, nil} case 53, 55, 56, 57: if len(r.buffer) == 5 && r.buffer[4] == 126 { *sz = 5 switch r.buffer[3] { case 53: return Event{F5, 0, nil} case 55: return Event{F6, 0, nil} case 56: return Event{F7, 0, nil} case 57: return Event{F8, 0, nil} } } return Event{Invalid, 0, nil} case ';': if len(r.buffer) != 6 { return Event{Invalid, 0, nil} } *sz = 6 switch r.buffer[4] { case '2', '5': switch r.buffer[5] { case 'A': return Event{SUp, 0, nil} case 'B': return Event{SDown, 0, nil} case 'C': return Event{SRight, 0, nil} case 'D': return Event{SLeft, 0, nil} } } // r.buffer[4] } // r.buffer[3] } // r.buffer[2] } // r.buffer[2] } // r.buffer[1] if r.buffer[1] >= 'a' && r.buffer[1] <= 'z' { return Event{AltA + int(r.buffer[1]) - 'a', 0, nil} } if r.buffer[1] >= '0' && r.buffer[1] <= '9' { return Event{Alt0 + int(r.buffer[1]) - '0', 0, nil} } return Event{Invalid, 0, nil} } func (r *LightRenderer) mouseSequence(sz *int) Event { if len(r.buffer) < 6 || !r.mouse { return Event{Invalid, 0, nil} } *sz = 6 switch r.buffer[3] { case 32, 34, 36, 40, 48, // mouse-down / shift / cmd / ctrl 35, 39, 43, 51: // mouse-up / shift / cmd / ctrl mod := r.buffer[3] >= 36 left := r.buffer[3] == 32 down := r.buffer[3]%2 == 0 x := int(r.buffer[4] - 33) y := int(r.buffer[5]-33) - r.yoffset double := false if down { now := time.Now() if !left { // Right double click is not allowed r.clickY = []int{} } else if now.Sub(r.prevDownTime) < doubleClickDuration { r.clickY = append(r.clickY, y) } else { r.clickY = []int{y} } r.prevDownTime = now } else { if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && time.Since(r.prevDownTime) < doubleClickDuration { double = true } } return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}} case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl mod := r.buffer[3] >= 100 s := 1 - int(r.buffer[3]%2)*2 x := int(r.buffer[4] - 33) y := int(r.buffer[5]-33) - r.yoffset return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, false, mod}} } return Event{Invalid, 0, nil} } func (r *LightRenderer) smcup() { r.csi("?1049h") } func (r *LightRenderer) rmcup() { r.csi("?1049l") } func (r *LightRenderer) Pause(clear bool) { terminal.Restore(r.fd(), r.origState) if clear { if r.fullscreen { r.rmcup() } else { r.smcup() r.csi("H") } r.flush() } } func (r *LightRenderer) Resume(clear bool) { terminal.MakeRaw(r.fd()) if clear { if r.fullscreen { r.smcup() } else { r.rmcup() } r.flush() } else if !r.fullscreen && r.mouse { // NOTE: Resume(false) is only called on SIGCONT after SIGSTOP. // And It's highly likely that the offset we obtained at the beginning will // no longer be correct, so we simply disable mouse input. r.csi("?1000l") r.mouse = false } } func (r *LightRenderer) Clear() { if r.fullscreen { r.csi("H") } // r.csi("u") r.origin() r.csi("J") r.flush() } func (r *LightRenderer) RefreshWindows(windows []Window) { r.flush() } func (r *LightRenderer) Refresh() { r.updateTerminalSize() } func (r *LightRenderer) Close() { // r.csi("u") if r.clearOnExit { if r.fullscreen { r.rmcup() } else { r.origin() if r.upOneLine { r.csi("A") } r.csi("J") } } else if !r.fullscreen { r.csi("u") } if r.mouse { r.csi("?1000l") } r.flush() terminal.Restore(r.fd(), r.origState) } func (r *LightRenderer) MaxX() int { return r.width } func (r *LightRenderer) MaxY() int { return r.height } func (r *LightRenderer) DoesAutoWrap() bool { return false } func (r *LightRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window { w := &LightWindow{ renderer: r, colored: r.theme != nil, border: borderStyle, top: top, left: left, width: width, height: height, tabstop: r.tabstop, fg: colDefault, bg: colDefault} if r.theme != nil { if preview { w.fg = r.theme.PreviewFg w.bg = r.theme.PreviewBg } else { w.fg = r.theme.Fg w.bg = r.theme.Bg } } w.drawBorder() return w } func (w *LightWindow) drawBorder() { switch w.border.shape { case BorderAround: w.drawBorderAround() case BorderHorizontal: w.drawBorderHorizontal() } } func (w *LightWindow) drawBorderHorizontal() { w.Move(0, 0) w.CPrint(ColBorder, AttrRegular, repeat(w.border.horizontal, w.width)) w.Move(w.height-1, 0) w.CPrint(ColBorder, AttrRegular, repeat(w.border.horizontal, w.width)) } func (w *LightWindow) drawBorderAround() { w.Move(0, 0) w.CPrint(ColPreviewBorder, AttrRegular, string(w.border.topLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.topRight)) for y := 1; y < w.height-1; y++ { w.Move(y, 0) w.CPrint(ColPreviewBorder, AttrRegular, string(w.border.vertical)) w.CPrint(ColPreviewBorder, AttrRegular, repeat(' ', w.width-2)) w.CPrint(ColPreviewBorder, AttrRegular, string(w.border.vertical)) } w.Move(w.height-1, 0) w.CPrint(ColPreviewBorder, AttrRegular, string(w.border.bottomLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.bottomRight)) } func (w *LightWindow) csi(code string) { w.renderer.csi(code) } func (w *LightWindow) stderrInternal(str string, allowNLCR bool) { w.renderer.stderrInternal(str, allowNLCR) } func (w *LightWindow) Top() int { return w.top } func (w *LightWindow) Left() int { return w.left } func (w *LightWindow) Width() int { return w.width } func (w *LightWindow) Height() int { return w.height } func (w *LightWindow) Refresh() { } func (w *LightWindow) Close() { } func (w *LightWindow) X() int { return w.posx } func (w *LightWindow) Y() int { return w.posy } func (w *LightWindow) Enclose(y int, x int) bool { return x >= w.left && x < (w.left+w.width) && y >= w.top && y < (w.top+w.height) } func (w *LightWindow) Move(y int, x int) { w.posx = x w.posy = y w.renderer.move(w.Top()+y, w.Left()+x) } func (w *LightWindow) MoveAndClear(y int, x int) { w.Move(y, x) // We should not delete preview window on the right // csi("K") w.Print(repeat(' ', w.width-x)) w.Move(y, x) } func attrCodes(attr Attr) []string { codes := []string{} if (attr & Bold) > 0 { codes = append(codes, "1") } if (attr & Dim) > 0 { codes = append(codes, "2") } if (attr & Italic) > 0 { codes = append(codes, "3") } if (attr & Underline) > 0 { codes = append(codes, "4") } if (attr & Blink) > 0 { codes = append(codes, "5") } if (attr & Reverse) > 0 { codes = append(codes, "7") } return codes } func colorCodes(fg Color, bg Color) []string { codes := []string{} appendCode := func(c Color, offset int) { if c == colDefault { return } if c.is24() { r := (c >> 16) & 0xff g := (c >> 8) & 0xff b := (c) & 0xff codes = append(codes, fmt.Sprintf("%d;2;%d;%d;%d", 38+offset, r, g, b)) } else if c >= colBlack && c <= colWhite { codes = append(codes, fmt.Sprintf("%d", int(c)+30+offset)) } else if c > colWhite && c < 16 { codes = append(codes, fmt.Sprintf("%d", int(c)+90+offset-8)) } else if c >= 16 && c < 256 { codes = append(codes, fmt.Sprintf("%d;5;%d", 38+offset, c)) } } appendCode(fg, 0) appendCode(bg, 10) return codes } func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) bool { codes := append(attrCodes(attr), colorCodes(fg, bg)...) w.csi(";" + strings.Join(codes, ";") + "m") return len(codes) > 0 } func (w *LightWindow) Print(text string) { w.cprint2(colDefault, w.bg, AttrRegular, text) } func cleanse(str string) string { return strings.Replace(str, "\x1b", "", -1) } func (w *LightWindow) CPrint(pair ColorPair, attr Attr, text string) { if !w.colored { w.csiColor(colDefault, colDefault, attrFor(pair, attr)) } else { w.csiColor(pair.Fg(), pair.Bg(), attr) } w.stderrInternal(cleanse(text), false) w.csi("m") } func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) { if w.csiColor(fg, bg, attr) { defer w.csi("m") } w.stderrInternal(cleanse(text), false) } type wrappedLine struct { text string displayWidth int } func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLine { lines := []wrappedLine{} width := 0 line := "" for _, r := range input { w := util.Max(util.RuneWidth(r, prefixLength+width, 8), 1) width += w str := string(r) if r == '\t' { str = repeat(' ', w) } if prefixLength+width <= max { line += str } else { lines = append(lines, wrappedLine{string(line), width - w}) line = str prefixLength = 0 width = util.RuneWidth(r, prefixLength, 8) } } lines = append(lines, wrappedLine{string(line), width}) return lines } func (w *LightWindow) fill(str string, onMove func()) FillReturn { allLines := strings.Split(str, "\n") for i, line := range allLines { lines := wrapLine(line, w.posx, w.width, w.tabstop) for j, wl := range lines { if w.posx >= w.Width()-1 && wl.displayWidth == 0 { if w.posy < w.height-1 { w.Move(w.posy+1, 0) } return FillNextLine } w.stderrInternal(wl.text, false) w.posx += wl.displayWidth // Wrap line if j < len(lines)-1 || i < len(allLines)-1 { if w.posy+1 >= w.height { return FillSuspend } w.MoveAndClear(w.posy, w.posx) w.Move(w.posy+1, 0) onMove() } } } return FillContinue } func (w *LightWindow) setBg() { if w.bg != colDefault { w.csiColor(colDefault, w.bg, AttrRegular) } } func (w *LightWindow) Fill(text string) FillReturn { w.Move(w.posy, w.posx) w.setBg() return w.fill(text, w.setBg) } func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn { w.Move(w.posy, w.posx) if fg == colDefault { fg = w.fg } if bg == colDefault { bg = w.bg } if w.csiColor(fg, bg, attr) { defer w.csi("m") return w.fill(text, func() { w.csiColor(fg, bg, attr) }) } return w.fill(text, w.setBg) } func (w *LightWindow) FinishFill() { w.MoveAndClear(w.posy, w.posx) for y := w.posy + 1; y < w.height; y++ { w.MoveAndClear(y, 0) } } func (w *LightWindow) Erase() { w.drawBorder() // We don't erase the window here to avoid flickering during scroll w.Move(0, 0) } fzf-0.20.0/src/tui/tcell.go000066400000000000000000000316621357617647500154350ustar00rootroot00000000000000// +build tcell windows package tui import ( "os" "time" "unicode/utf8" "runtime" "github.com/gdamore/tcell" "github.com/gdamore/tcell/encoding" "github.com/mattn/go-runewidth" ) func HasFullscreenRenderer() bool { return true } func (p ColorPair) style() tcell.Style { style := tcell.StyleDefault return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg())) } type Attr tcell.Style type TcellWindow struct { color bool top int left int width int height int normal ColorPair lastX int lastY int moveCursor bool borderStyle BorderStyle } func (w *TcellWindow) Top() int { return w.top } func (w *TcellWindow) Left() int { return w.left } func (w *TcellWindow) Width() int { return w.width } func (w *TcellWindow) Height() int { return w.height } func (w *TcellWindow) Refresh() { if w.moveCursor { _screen.ShowCursor(w.left+w.lastX, w.top+w.lastY) w.moveCursor = false } w.lastX = 0 w.lastY = 0 w.drawBorder() } func (w *TcellWindow) FinishFill() { // NO-OP } const ( Bold Attr = Attr(tcell.AttrBold) Dim = Attr(tcell.AttrDim) Blink = Attr(tcell.AttrBlink) Reverse = Attr(tcell.AttrReverse) Underline = Attr(tcell.AttrUnderline) Italic = Attr(tcell.AttrNone) // Not supported ) const ( AttrRegular Attr = 0 ) func (r *FullscreenRenderer) defaultTheme() *ColorTheme { if _screen.Colors() >= 256 { return Dark256 } return Default16 } var ( _colorToAttribute = []tcell.Color{ tcell.ColorBlack, tcell.ColorRed, tcell.ColorGreen, tcell.ColorYellow, tcell.ColorBlue, tcell.ColorDarkMagenta, tcell.ColorLightCyan, tcell.ColorWhite, } ) func (c Color) Style() tcell.Color { if c <= colDefault { return tcell.ColorDefault } else if c >= colBlack && c <= colWhite { return _colorToAttribute[int(c)] } else { return tcell.Color(c) } } func (a Attr) Merge(b Attr) Attr { return a | b } var ( _screen tcell.Screen ) func (r *FullscreenRenderer) initScreen() { s, e := tcell.NewScreen() if e != nil { errorExit(e.Error()) } if e = s.Init(); e != nil { errorExit(e.Error()) } if r.mouse { s.EnableMouse() } else { s.DisableMouse() } _screen = s } func (r *FullscreenRenderer) Init() { if os.Getenv("TERM") == "cygwin" { os.Setenv("TERM", "") } encoding.Register() r.initScreen() initTheme(r.theme, r.defaultTheme(), r.forceBlack) } func (r *FullscreenRenderer) MaxX() int { ncols, _ := _screen.Size() return int(ncols) } func (r *FullscreenRenderer) MaxY() int { _, nlines := _screen.Size() return int(nlines) } func (w *TcellWindow) X() int { return w.lastX } func (w *TcellWindow) Y() int { return w.lastY } func (r *FullscreenRenderer) DoesAutoWrap() bool { return false } func (r *FullscreenRenderer) Clear() { _screen.Sync() _screen.Clear() } func (r *FullscreenRenderer) Refresh() { // noop } func (r *FullscreenRenderer) GetChar() Event { ev := _screen.PollEvent() switch ev := ev.(type) { case *tcell.EventResize: return Event{Resize, 0, nil} // process mouse events: case *tcell.EventMouse: x, y := ev.Position() button := ev.Buttons() mod := ev.Modifiers() != 0 if button&tcell.WheelDown != 0 { return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}} } else if button&tcell.WheelUp != 0 { return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}} } else if runtime.GOOS != "windows" { // double and single taps on Windows don't quite work due to // the console acting on the events and not allowing us // to consume them. left := button&tcell.Button1 != 0 down := left || button&tcell.Button3 != 0 double := false if down { now := time.Now() if !left { r.clickY = []int{} } else if now.Sub(r.prevDownTime) < doubleClickDuration { r.clickY = append(r.clickY, x) } else { r.clickY = []int{x} r.prevDownTime = now } } else { if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && time.Now().Sub(r.prevDownTime) < doubleClickDuration { double = true } } return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}} } // process keyboard: case *tcell.EventKey: alt := (ev.Modifiers() & tcell.ModAlt) > 0 keyfn := func(r rune) int { if alt { return CtrlAltA - 'a' + int(r) } return CtrlA - 'a' + int(r) } switch ev.Key() { case tcell.KeyCtrlA: return Event{keyfn('a'), 0, nil} case tcell.KeyCtrlB: return Event{keyfn('b'), 0, nil} case tcell.KeyCtrlC: return Event{keyfn('c'), 0, nil} case tcell.KeyCtrlD: return Event{keyfn('d'), 0, nil} case tcell.KeyCtrlE: return Event{keyfn('e'), 0, nil} case tcell.KeyCtrlF: return Event{keyfn('f'), 0, nil} case tcell.KeyCtrlG: return Event{keyfn('g'), 0, nil} case tcell.KeyCtrlH: return Event{keyfn('h'), 0, nil} case tcell.KeyCtrlI: return Event{keyfn('i'), 0, nil} case tcell.KeyCtrlJ: return Event{keyfn('j'), 0, nil} case tcell.KeyCtrlK: return Event{keyfn('k'), 0, nil} case tcell.KeyCtrlL: return Event{keyfn('l'), 0, nil} case tcell.KeyCtrlM: return Event{keyfn('m'), 0, nil} case tcell.KeyCtrlN: return Event{keyfn('n'), 0, nil} case tcell.KeyCtrlO: return Event{keyfn('o'), 0, nil} case tcell.KeyCtrlP: return Event{keyfn('p'), 0, nil} case tcell.KeyCtrlQ: return Event{keyfn('q'), 0, nil} case tcell.KeyCtrlR: return Event{keyfn('r'), 0, nil} case tcell.KeyCtrlS: return Event{keyfn('s'), 0, nil} case tcell.KeyCtrlT: return Event{keyfn('t'), 0, nil} case tcell.KeyCtrlU: return Event{keyfn('u'), 0, nil} case tcell.KeyCtrlV: return Event{keyfn('v'), 0, nil} case tcell.KeyCtrlW: return Event{keyfn('w'), 0, nil} case tcell.KeyCtrlX: return Event{keyfn('x'), 0, nil} case tcell.KeyCtrlY: return Event{keyfn('y'), 0, nil} case tcell.KeyCtrlZ: return Event{keyfn('z'), 0, nil} case tcell.KeyCtrlSpace: return Event{CtrlSpace, 0, nil} case tcell.KeyCtrlBackslash: return Event{CtrlBackSlash, 0, nil} case tcell.KeyCtrlRightSq: return Event{CtrlRightBracket, 0, nil} case tcell.KeyCtrlUnderscore: return Event{CtrlSlash, 0, nil} case tcell.KeyBackspace2: if alt { return Event{AltBS, 0, nil} } return Event{BSpace, 0, nil} case tcell.KeyUp: if alt { return Event{AltUp, 0, nil} } return Event{Up, 0, nil} case tcell.KeyDown: if alt { return Event{AltDown, 0, nil} } return Event{Down, 0, nil} case tcell.KeyLeft: if alt { return Event{AltLeft, 0, nil} } return Event{Left, 0, nil} case tcell.KeyRight: if alt { return Event{AltRight, 0, nil} } return Event{Right, 0, nil} case tcell.KeyHome: return Event{Home, 0, nil} case tcell.KeyDelete: return Event{Del, 0, nil} case tcell.KeyEnd: return Event{End, 0, nil} case tcell.KeyPgUp: return Event{PgUp, 0, nil} case tcell.KeyPgDn: return Event{PgDn, 0, nil} case tcell.KeyBacktab: return Event{BTab, 0, nil} case tcell.KeyF1: return Event{F1, 0, nil} case tcell.KeyF2: return Event{F2, 0, nil} case tcell.KeyF3: return Event{F3, 0, nil} case tcell.KeyF4: return Event{F4, 0, nil} case tcell.KeyF5: return Event{F5, 0, nil} case tcell.KeyF6: return Event{F6, 0, nil} case tcell.KeyF7: return Event{F7, 0, nil} case tcell.KeyF8: return Event{F8, 0, nil} case tcell.KeyF9: return Event{F9, 0, nil} case tcell.KeyF10: return Event{F10, 0, nil} case tcell.KeyF11: return Event{F11, 0, nil} case tcell.KeyF12: return Event{F12, 0, nil} // ev.Ch doesn't work for some reason for space: case tcell.KeyRune: r := ev.Rune() if alt { switch r { case ' ': return Event{AltSpace, 0, nil} case '/': return Event{AltSlash, 0, nil} } if r >= 'a' && r <= 'z' { return Event{AltA + int(r) - 'a', 0, nil} } if r >= '0' && r <= '9' { return Event{Alt0 + int(r) - '0', 0, nil} } } return Event{Rune, r, nil} case tcell.KeyEsc: return Event{ESC, 0, nil} } } return Event{Invalid, 0, nil} } func (r *FullscreenRenderer) Pause(clear bool) { if clear { _screen.Fini() } } func (r *FullscreenRenderer) Resume(clear bool) { if clear { r.initScreen() } } func (r *FullscreenRenderer) Close() { _screen.Fini() } func (r *FullscreenRenderer) RefreshWindows(windows []Window) { // TODO for _, w := range windows { w.Refresh() } _screen.Show() } func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window { normal := ColNormal if preview { normal = ColPreview } return &TcellWindow{ color: r.theme != nil, top: top, left: left, width: width, height: height, normal: normal, borderStyle: borderStyle} } func (w *TcellWindow) Close() { // TODO } func fill(x, y, w, h int, n ColorPair, r rune) { for ly := 0; ly <= h; ly++ { for lx := 0; lx <= w; lx++ { _screen.SetContent(x+lx, y+ly, r, nil, n.style()) } } } func (w *TcellWindow) Erase() { fill(w.left-1, w.top, w.width+1, w.height, w.normal, ' ') } func (w *TcellWindow) Enclose(y int, x int) bool { return x >= w.left && x < (w.left+w.width) && y >= w.top && y < (w.top+w.height) } func (w *TcellWindow) Move(y int, x int) { w.lastX = x w.lastY = y w.moveCursor = true } func (w *TcellWindow) MoveAndClear(y int, x int) { w.Move(y, x) for i := w.lastX; i < w.width; i++ { _screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, w.normal.style()) } w.lastX = x } func (w *TcellWindow) Print(text string) { w.printString(text, w.normal, 0) } func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) { t := text lx := 0 var style tcell.Style if w.color { style = pair.style(). Reverse(a&Attr(tcell.AttrReverse) != 0). Underline(a&Attr(tcell.AttrUnderline) != 0) } else { style = w.normal.style(). Reverse(a&Attr(tcell.AttrReverse) != 0 || pair == ColCurrent || pair == ColCurrentMatch). Underline(a&Attr(tcell.AttrUnderline) != 0 || pair == ColMatch || pair == ColCurrentMatch) } style = style. Blink(a&Attr(tcell.AttrBlink) != 0). Bold(a&Attr(tcell.AttrBold) != 0). Dim(a&Attr(tcell.AttrDim) != 0) for { if len(t) == 0 { break } r, size := utf8.DecodeRuneInString(t) t = t[size:] if r < rune(' ') { // ignore control characters continue } if r == '\n' { w.lastY++ lx = 0 } else { if r == '\u000D' { // skip carriage return continue } var xPos = w.left + w.lastX + lx var yPos = w.top + w.lastY if xPos < (w.left+w.width) && yPos < (w.top+w.height) { _screen.SetContent(xPos, yPos, r, nil, style) } lx += runewidth.RuneWidth(r) } } w.lastX += lx } func (w *TcellWindow) CPrint(pair ColorPair, attr Attr, text string) { w.printString(text, pair, attr) } func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) FillReturn { lx := 0 var style tcell.Style if w.color { style = pair.style() } else { style = w.normal.style() } style = style. Blink(a&Attr(tcell.AttrBlink) != 0). Bold(a&Attr(tcell.AttrBold) != 0). Dim(a&Attr(tcell.AttrDim) != 0). Reverse(a&Attr(tcell.AttrReverse) != 0). Underline(a&Attr(tcell.AttrUnderline) != 0) for _, r := range text { if r == '\n' { w.lastY++ w.lastX = 0 lx = 0 } else { var xPos = w.left + w.lastX + lx // word wrap: if xPos >= (w.left + w.width) { w.lastY++ w.lastX = 0 lx = 0 xPos = w.left } var yPos = w.top + w.lastY if yPos >= (w.top + w.height) { return FillSuspend } _screen.SetContent(xPos, yPos, r, nil, style) lx += runewidth.RuneWidth(r) } } w.lastX += lx return FillContinue } func (w *TcellWindow) Fill(str string) FillReturn { return w.fillString(str, w.normal, 0) } func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn { if fg == colDefault { fg = w.normal.Fg() } if bg == colDefault { bg = w.normal.Bg() } return w.fillString(str, NewColorPair(fg, bg), a) } func (w *TcellWindow) drawBorder() { if w.borderStyle.shape == BorderNone { return } left := w.left right := left + w.width top := w.top bot := top + w.height var style tcell.Style if w.color { if w.borderStyle.shape == BorderAround { style = ColPreviewBorder.style() } else { style = ColBorder.style() } } else { style = w.normal.style() } for x := left; x < right; x++ { _screen.SetContent(x, top, w.borderStyle.horizontal, nil, style) _screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style) } if w.borderStyle.shape == BorderAround { for y := top; y < bot; y++ { _screen.SetContent(left, y, w.borderStyle.vertical, nil, style) _screen.SetContent(right-1, y, w.borderStyle.vertical, nil, style) } _screen.SetContent(left, top, w.borderStyle.topLeft, nil, style) _screen.SetContent(right-1, top, w.borderStyle.topRight, nil, style) _screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style) _screen.SetContent(right-1, bot-1, w.borderStyle.bottomRight, nil, style) } } fzf-0.20.0/src/tui/ttyname_unix.go000066400000000000000000000007751357617647500170570ustar00rootroot00000000000000// +build !windows package tui import ( "io/ioutil" "syscall" ) var devPrefixes = [...]string{"/dev/pts/", "/dev/"} func ttyname() string { var stderr syscall.Stat_t if syscall.Fstat(2, &stderr) != nil { return "" } for _, prefix := range devPrefixes { files, err := ioutil.ReadDir(prefix) if err != nil { continue } for _, file := range files { if stat, ok := file.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev { return prefix + file.Name() } } } return "" } fzf-0.20.0/src/tui/ttyname_windows.go000066400000000000000000000001051357617647500175510ustar00rootroot00000000000000// +build windows package tui func ttyname() string { return "" } fzf-0.20.0/src/tui/tui.go000066400000000000000000000227511357617647500151320ustar00rootroot00000000000000package tui import ( "fmt" "os" "strconv" "time" ) // Types of user action const ( Rune = iota CtrlA CtrlB CtrlC CtrlD CtrlE CtrlF CtrlG CtrlH Tab CtrlJ CtrlK CtrlL CtrlM CtrlN CtrlO CtrlP CtrlQ CtrlR CtrlS CtrlT CtrlU CtrlV CtrlW CtrlX CtrlY CtrlZ ESC CtrlSpace // https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal CtrlBackSlash CtrlRightBracket CtrlCaret CtrlSlash Invalid Resize Mouse DoubleClick LeftClick RightClick BTab BSpace Del PgUp PgDn Up Down Left Right Home End SUp SDown SLeft SRight F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 Change AltSpace AltSlash AltBS AltUp AltDown AltLeft AltRight Alt0 ) const ( // Reset iota AltA = Alt0 + 'a' - '0' + iota AltB AltC AltD AltE AltF AltZ = AltA + 'z' - 'a' CtrlAltA = AltZ + 1 CtrlAltM = CtrlAltA + 'm' - 'a' ) const ( doubleClickDuration = 500 * time.Millisecond ) type Color int32 func (c Color) is24() bool { return c > 0 && (c&(1<<24)) > 0 } const ( colUndefined Color = -2 colDefault Color = -1 ) const ( colBlack Color = iota colRed colGreen colYellow colBlue colMagenta colCyan colWhite ) type FillReturn int const ( FillContinue FillReturn = iota FillNextLine FillSuspend ) type ColorPair struct { fg Color bg Color id int } func HexToColor(rrggbb string) Color { r, _ := strconv.ParseInt(rrggbb[1:3], 16, 0) g, _ := strconv.ParseInt(rrggbb[3:5], 16, 0) b, _ := strconv.ParseInt(rrggbb[5:7], 16, 0) return Color((1 << 24) + (r << 16) + (g << 8) + b) } func NewColorPair(fg Color, bg Color) ColorPair { return ColorPair{fg, bg, -1} } func (p ColorPair) Fg() Color { return p.fg } func (p ColorPair) Bg() Color { return p.bg } type ColorTheme struct { Fg Color Bg Color PreviewFg Color PreviewBg Color DarkBg Color Gutter Color Prompt Color Match Color Current Color CurrentMatch Color Spinner Color Info Color Cursor Color Selected Color Header Color Border Color } type Event struct { Type int Char rune MouseEvent *MouseEvent } type MouseEvent struct { Y int X int S int Left bool Down bool Double bool Mod bool } type BorderShape int const ( BorderNone BorderShape = iota BorderAround BorderHorizontal ) type BorderStyle struct { shape BorderShape horizontal rune vertical rune topLeft rune topRight rune bottomLeft rune bottomRight rune } type BorderCharacter int func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle { if unicode { return BorderStyle{ shape: shape, horizontal: '─', vertical: '│', topLeft: '┌', topRight: '┐', bottomLeft: '└', bottomRight: '┘', } } return BorderStyle{ shape: shape, horizontal: '-', vertical: '|', topLeft: '+', topRight: '+', bottomLeft: '+', bottomRight: '+', } } func MakeTransparentBorder() BorderStyle { return BorderStyle{ shape: BorderAround, horizontal: ' ', vertical: ' ', topLeft: ' ', topRight: ' ', bottomLeft: ' ', bottomRight: ' '} } type Renderer interface { Init() Pause(clear bool) Resume(clear bool) Clear() RefreshWindows(windows []Window) Refresh() Close() GetChar() Event MaxX() int MaxY() int DoesAutoWrap() bool NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window } type Window interface { Top() int Left() int Width() int Height() int Refresh() FinishFill() Close() X() int Y() int Enclose(y int, x int) bool Move(y int, x int) MoveAndClear(y int, x int) Print(text string) CPrint(color ColorPair, attr Attr, text string) Fill(text string) FillReturn CFill(fg Color, bg Color, attr Attr, text string) FillReturn Erase() } type FullscreenRenderer struct { theme *ColorTheme mouse bool forceBlack bool prevDownTime time.Time clickY []int } func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer { r := &FullscreenRenderer{ theme: theme, mouse: mouse, forceBlack: forceBlack, prevDownTime: time.Unix(0, 0), clickY: []int{}} return r } var ( Default16 *ColorTheme Dark256 *ColorTheme Light256 *ColorTheme ColPrompt ColorPair ColNormal ColorPair ColMatch ColorPair ColCursor ColorPair ColSelected ColorPair ColCurrent ColorPair ColCurrentMatch ColorPair ColCurrentCursor ColorPair ColCurrentSelected ColorPair ColSpinner ColorPair ColInfo ColorPair ColHeader ColorPair ColBorder ColorPair ColPreview ColorPair ColPreviewBorder ColorPair ) func EmptyTheme() *ColorTheme { return &ColorTheme{ Fg: colUndefined, Bg: colUndefined, PreviewFg: colUndefined, PreviewBg: colUndefined, DarkBg: colUndefined, Gutter: colUndefined, Prompt: colUndefined, Match: colUndefined, Current: colUndefined, CurrentMatch: colUndefined, Spinner: colUndefined, Info: colUndefined, Cursor: colUndefined, Selected: colUndefined, Header: colUndefined, Border: colUndefined} } func errorExit(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(2) } func init() { Default16 = &ColorTheme{ Fg: colDefault, Bg: colDefault, PreviewFg: colUndefined, PreviewBg: colUndefined, DarkBg: colBlack, Gutter: colUndefined, Prompt: colBlue, Match: colGreen, Current: colYellow, CurrentMatch: colGreen, Spinner: colGreen, Info: colWhite, Cursor: colRed, Selected: colMagenta, Header: colCyan, Border: colBlack} Dark256 = &ColorTheme{ Fg: colDefault, Bg: colDefault, PreviewFg: colUndefined, PreviewBg: colUndefined, DarkBg: 236, Gutter: colUndefined, Prompt: 110, Match: 108, Current: 254, CurrentMatch: 151, Spinner: 148, Info: 144, Cursor: 161, Selected: 168, Header: 109, Border: 59} Light256 = &ColorTheme{ Fg: colDefault, Bg: colDefault, PreviewFg: colUndefined, PreviewBg: colUndefined, DarkBg: 251, Gutter: colUndefined, Prompt: 25, Match: 66, Current: 237, CurrentMatch: 23, Spinner: 65, Info: 101, Cursor: 161, Selected: 168, Header: 31, Border: 145} } func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) { if theme == nil { initPalette(theme) return } if forceBlack { theme.Bg = colBlack } o := func(a Color, b Color) Color { if b == colUndefined { return a } return b } theme.Fg = o(baseTheme.Fg, theme.Fg) theme.Bg = o(baseTheme.Bg, theme.Bg) theme.PreviewFg = o(theme.Fg, o(baseTheme.PreviewFg, theme.PreviewFg)) theme.PreviewBg = o(theme.Bg, o(baseTheme.PreviewBg, theme.PreviewBg)) theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg) theme.Gutter = o(theme.DarkBg, o(baseTheme.Gutter, theme.Gutter)) theme.Prompt = o(baseTheme.Prompt, theme.Prompt) theme.Match = o(baseTheme.Match, theme.Match) theme.Current = o(baseTheme.Current, theme.Current) theme.CurrentMatch = o(baseTheme.CurrentMatch, theme.CurrentMatch) theme.Spinner = o(baseTheme.Spinner, theme.Spinner) theme.Info = o(baseTheme.Info, theme.Info) theme.Cursor = o(baseTheme.Cursor, theme.Cursor) theme.Selected = o(baseTheme.Selected, theme.Selected) theme.Header = o(baseTheme.Header, theme.Header) theme.Border = o(baseTheme.Border, theme.Border) initPalette(theme) } func initPalette(theme *ColorTheme) { idx := 0 pair := func(fg, bg Color) ColorPair { idx++ return ColorPair{fg, bg, idx} } if theme != nil { ColPrompt = pair(theme.Prompt, theme.Bg) ColNormal = pair(theme.Fg, theme.Bg) ColMatch = pair(theme.Match, theme.Bg) ColCursor = pair(theme.Cursor, theme.Gutter) ColSelected = pair(theme.Selected, theme.Gutter) ColCurrent = pair(theme.Current, theme.DarkBg) ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg) ColCurrentCursor = pair(theme.Cursor, theme.DarkBg) ColCurrentSelected = pair(theme.Selected, theme.DarkBg) ColSpinner = pair(theme.Spinner, theme.Bg) ColInfo = pair(theme.Info, theme.Bg) ColHeader = pair(theme.Header, theme.Bg) ColBorder = pair(theme.Border, theme.Bg) ColPreview = pair(theme.PreviewFg, theme.PreviewBg) ColPreviewBorder = pair(theme.Border, theme.PreviewBg) } else { ColPrompt = pair(colDefault, colDefault) ColNormal = pair(colDefault, colDefault) ColMatch = pair(colDefault, colDefault) ColCursor = pair(colDefault, colDefault) ColSelected = pair(colDefault, colDefault) ColCurrent = pair(colDefault, colDefault) ColCurrentMatch = pair(colDefault, colDefault) ColCurrentCursor = pair(colDefault, colDefault) ColCurrentSelected = pair(colDefault, colDefault) ColSpinner = pair(colDefault, colDefault) ColInfo = pair(colDefault, colDefault) ColHeader = pair(colDefault, colDefault) ColBorder = pair(colDefault, colDefault) ColPreview = pair(colDefault, colDefault) ColPreviewBorder = pair(colDefault, colDefault) } } func attrFor(color ColorPair, attr Attr) Attr { switch color { case ColCurrent: return attr | Reverse case ColMatch: return attr | Underline case ColCurrentMatch: return attr | Underline | Reverse } return attr } fzf-0.20.0/src/tui/tui_test.go000066400000000000000000000006171357617647500161660ustar00rootroot00000000000000package tui import "testing" func TestHexToColor(t *testing.T) { assert := func(expr string, r, g, b int) { color := HexToColor(expr) if !color.is24() || int((color>>16)&0xff) != r || int((color>>8)&0xff) != g || int((color)&0xff) != b { t.Fail() } } assert("#ff0000", 255, 0, 0) assert("#010203", 1, 2, 3) assert("#102030", 16, 32, 48) assert("#ffffff", 255, 255, 255) } fzf-0.20.0/src/update_assets.rb000077500000000000000000000022271357617647500163710ustar00rootroot00000000000000#!/usr/bin/env ruby # http://www.rubydoc.info/github/rest-client/rest-client/RestClient require 'rest_client' require 'json' if ARGV.length < 3 puts "usage: #$0 " exit 1 end token, version, *files = ARGV base = "https://api.github.com/repos/junegunn/fzf-bin/releases" # List releases rels = JSON.parse(RestClient.get(base, :authorization => "token #{token}")) rel = rels.find { |r| r['tag_name'] == version } unless rel puts "#{version} not found" exit 1 end # List assets assets = Hash[rel['assets'].map { |a| a.values_at *%w[name id] }] files.select { |f| File.exists? f }.map do |file| Thread.new do name = File.basename file if asset_id = assets[name] puts "#{name} found. Deleting asset id #{asset_id}." RestClient.delete "#{base}/assets/#{asset_id}", :authorization => "token #{token}" else puts "#{name} not found" end puts "Uploading #{name}" RestClient.post( "#{base.sub 'api', 'uploads'}/#{rel['id']}/assets?name=#{name}", File.read(file), :authorization => "token #{token}", :content_type => "application/octet-stream") end end.each(&:join) fzf-0.20.0/src/util/000077500000000000000000000000001357617647500141475ustar00rootroot00000000000000fzf-0.20.0/src/util/atomicbool.go000066400000000000000000000012371357617647500166310ustar00rootroot00000000000000package util import "sync" // AtomicBool is a boxed-class that provides synchronized access to the // underlying boolean value type AtomicBool struct { mutex sync.Mutex state bool } // NewAtomicBool returns a new AtomicBool func NewAtomicBool(initialState bool) *AtomicBool { return &AtomicBool{ mutex: sync.Mutex{}, state: initialState} } // Get returns the current boolean value synchronously func (a *AtomicBool) Get() bool { a.mutex.Lock() defer a.mutex.Unlock() return a.state } // Set updates the boolean value synchronously func (a *AtomicBool) Set(newState bool) bool { a.mutex.Lock() defer a.mutex.Unlock() a.state = newState return a.state } fzf-0.20.0/src/util/atomicbool_test.go000066400000000000000000000004551357617647500176710ustar00rootroot00000000000000package util import "testing" func TestAtomicBool(t *testing.T) { if !NewAtomicBool(true).Get() || NewAtomicBool(false).Get() { t.Error("Invalid initial value") } ab := NewAtomicBool(true) if ab.Set(false) { t.Error("Invalid return value") } if ab.Get() { t.Error("Invalid state") } } fzf-0.20.0/src/util/chars.go000066400000000000000000000076751357617647500156150ustar00rootroot00000000000000package util import ( "fmt" "unicode" "unicode/utf8" "unsafe" ) const ( overflow64 uint64 = 0x8080808080808080 overflow32 uint32 = 0x80808080 ) type Chars struct { slice []byte // or []rune inBytes bool trimLengthKnown bool trimLength uint16 // XXX Piggybacking item index here is a horrible idea. But I'm trying to // minimize the memory footprint by not wasting padded spaces. Index int32 } func checkAscii(bytes []byte) (bool, int) { i := 0 for ; i <= len(bytes)-8; i += 8 { if (overflow64 & *(*uint64)(unsafe.Pointer(&bytes[i]))) > 0 { return false, i } } for ; i <= len(bytes)-4; i += 4 { if (overflow32 & *(*uint32)(unsafe.Pointer(&bytes[i]))) > 0 { return false, i } } for ; i < len(bytes); i++ { if bytes[i] >= utf8.RuneSelf { return false, i } } return true, 0 } // ToChars converts byte array into rune array func ToChars(bytes []byte) Chars { inBytes, bytesUntil := checkAscii(bytes) if inBytes { return Chars{slice: bytes, inBytes: inBytes} } runes := make([]rune, bytesUntil, len(bytes)) for i := 0; i < bytesUntil; i++ { runes[i] = rune(bytes[i]) } for i := bytesUntil; i < len(bytes); { r, sz := utf8.DecodeRune(bytes[i:]) i += sz runes = append(runes, r) } return RunesToChars(runes) } func RunesToChars(runes []rune) Chars { return Chars{slice: *(*[]byte)(unsafe.Pointer(&runes)), inBytes: false} } func (chars *Chars) IsBytes() bool { return chars.inBytes } func (chars *Chars) Bytes() []byte { return chars.slice } func (chars *Chars) optionalRunes() []rune { if chars.inBytes { return nil } return *(*[]rune)(unsafe.Pointer(&chars.slice)) } func (chars *Chars) Get(i int) rune { if runes := chars.optionalRunes(); runes != nil { return runes[i] } return rune(chars.slice[i]) } func (chars *Chars) Length() int { if runes := chars.optionalRunes(); runes != nil { return len(runes) } return len(chars.slice) } // String returns the string representation of a Chars object. func (chars *Chars) String() string { return fmt.Sprintf("Chars{slice: []byte(%q), inBytes: %v, trimLengthKnown: %v, trimLength: %d, Index: %d}", chars.slice, chars.inBytes, chars.trimLengthKnown, chars.trimLength, chars.Index) } // TrimLength returns the length after trimming leading and trailing whitespaces func (chars *Chars) TrimLength() uint16 { if chars.trimLengthKnown { return chars.trimLength } chars.trimLengthKnown = true var i int len := chars.Length() for i = len - 1; i >= 0; i-- { char := chars.Get(i) if !unicode.IsSpace(char) { break } } // Completely empty if i < 0 { return 0 } var j int for j = 0; j < len; j++ { char := chars.Get(j) if !unicode.IsSpace(char) { break } } chars.trimLength = AsUint16(i - j + 1) return chars.trimLength } func (chars *Chars) TrailingWhitespaces() int { whitespaces := 0 for i := chars.Length() - 1; i >= 0; i-- { char := chars.Get(i) if !unicode.IsSpace(char) { break } whitespaces++ } return whitespaces } func (chars *Chars) TrimTrailingWhitespaces() { whitespaces := chars.TrailingWhitespaces() chars.slice = chars.slice[0 : len(chars.slice)-whitespaces] } func (chars *Chars) ToString() string { if runes := chars.optionalRunes(); runes != nil { return string(runes) } return string(chars.slice) } func (chars *Chars) ToRunes() []rune { if runes := chars.optionalRunes(); runes != nil { return runes } bytes := chars.slice runes := make([]rune, len(bytes)) for idx, b := range bytes { runes[idx] = rune(b) } return runes } func (chars *Chars) CopyRunes(dest []rune) { if runes := chars.optionalRunes(); runes != nil { copy(dest, runes) return } for idx, b := range chars.slice[:len(dest)] { dest[idx] = rune(b) } } func (chars *Chars) Prepend(prefix string) { if runes := chars.optionalRunes(); runes != nil { runes = append([]rune(prefix), runes...) chars.slice = *(*[]byte)(unsafe.Pointer(&runes)) } else { chars.slice = append([]byte(prefix), chars.slice...) } } fzf-0.20.0/src/util/chars_test.go000066400000000000000000000017251357617647500166420ustar00rootroot00000000000000package util import "testing" func TestToCharsAscii(t *testing.T) { chars := ToChars([]byte("foobar")) if !chars.inBytes || chars.ToString() != "foobar" || !chars.inBytes { t.Error() } } func TestCharsLength(t *testing.T) { chars := ToChars([]byte("\tabc한글 ")) if chars.inBytes || chars.Length() != 8 || chars.TrimLength() != 5 { t.Error() } } func TestCharsToString(t *testing.T) { text := "\tabc한글 " chars := ToChars([]byte(text)) if chars.ToString() != text { t.Error() } } func TestTrimLength(t *testing.T) { check := func(str string, exp uint16) { chars := ToChars([]byte(str)) trimmed := chars.TrimLength() if trimmed != exp { t.Errorf("Invalid TrimLength result for '%s': %d (expected %d)", str, trimmed, exp) } } check("hello", 5) check("hello ", 5) check("hello ", 5) check(" hello", 5) check(" hello", 5) check(" hello ", 5) check(" hello ", 5) check("h o", 5) check(" h o ", 5) check(" ", 0) } fzf-0.20.0/src/util/eventbox.go000066400000000000000000000036701357617647500163360ustar00rootroot00000000000000package util import "sync" // EventType is the type for fzf events type EventType int // Events is a type that associates EventType to any data type Events map[EventType]interface{} // EventBox is used for coordinating events type EventBox struct { events Events cond *sync.Cond ignore map[EventType]bool } // NewEventBox returns a new EventBox func NewEventBox() *EventBox { return &EventBox{ events: make(Events), cond: sync.NewCond(&sync.Mutex{}), ignore: make(map[EventType]bool)} } // Wait blocks the goroutine until signaled func (b *EventBox) Wait(callback func(*Events)) { b.cond.L.Lock() if len(b.events) == 0 { b.cond.Wait() } callback(&b.events) b.cond.L.Unlock() } // Set turns on the event type on the box func (b *EventBox) Set(event EventType, value interface{}) { b.cond.L.Lock() b.events[event] = value if _, found := b.ignore[event]; !found { b.cond.Broadcast() } b.cond.L.Unlock() } // Clear clears the events // Unsynchronized; should be called within Wait routine func (events *Events) Clear() { for event := range *events { delete(*events, event) } } // Peek peeks at the event box if the given event is set func (b *EventBox) Peek(event EventType) bool { b.cond.L.Lock() _, ok := b.events[event] b.cond.L.Unlock() return ok } // Watch deletes the events from the ignore list func (b *EventBox) Watch(events ...EventType) { b.cond.L.Lock() for _, event := range events { delete(b.ignore, event) } b.cond.L.Unlock() } // Unwatch adds the events to the ignore list func (b *EventBox) Unwatch(events ...EventType) { b.cond.L.Lock() for _, event := range events { b.ignore[event] = true } b.cond.L.Unlock() } // WaitFor blocks the execution until the event is received func (b *EventBox) WaitFor(event EventType) { looping := true for looping { b.Wait(func(events *Events) { for evt := range *events { switch evt { case event: looping = false return } } }) } } fzf-0.20.0/src/util/eventbox_test.go000066400000000000000000000016151357617647500173720ustar00rootroot00000000000000package util import "testing" // fzf events const ( EvtReadNew EventType = iota EvtReadFin EvtSearchNew EvtSearchProgress EvtSearchFin EvtClose ) func TestEventBox(t *testing.T) { eb := NewEventBox() // Wait should return immediately ch := make(chan bool) go func() { eb.Set(EvtReadNew, 10) ch <- true <-ch eb.Set(EvtSearchNew, 10) eb.Set(EvtSearchNew, 15) eb.Set(EvtSearchNew, 20) eb.Set(EvtSearchProgress, 30) ch <- true <-ch eb.Set(EvtSearchFin, 40) ch <- true <-ch }() count := 0 sum := 0 looping := true for looping { <-ch eb.Wait(func(events *Events) { for _, value := range *events { switch val := value.(type) { case int: sum += val looping = sum < 100 } } events.Clear() }) ch <- true count++ } if count != 3 { t.Error("Invalid number of events", count) } if sum != 100 { t.Error("Invalid sum", sum) } } fzf-0.20.0/src/util/slab.go000066400000000000000000000002711357617647500154170ustar00rootroot00000000000000package util type Slab struct { I16 []int16 I32 []int32 } func MakeSlab(size16 int, size32 int) *Slab { return &Slab{ I16: make([]int16, size16), I32: make([]int32, size32)} } fzf-0.20.0/src/util/util.go000066400000000000000000000043751357617647500154640ustar00rootroot00000000000000package util import ( "math" "os" "time" "github.com/mattn/go-isatty" "github.com/mattn/go-runewidth" ) var _runeWidths = make(map[rune]int) // RuneWidth returns rune width func RuneWidth(r rune, prefixWidth int, tabstop int) int { if r == '\t' { return tabstop - prefixWidth%tabstop } else if w, found := _runeWidths[r]; found { return w } else if r == '\n' || r == '\r' { return 1 } w := runewidth.RuneWidth(r) _runeWidths[r] = w return w } // Max returns the largest integer func Max(first int, second int) int { if first >= second { return first } return second } // Max16 returns the largest integer func Max16(first int16, second int16) int16 { if first >= second { return first } return second } // Max32 returns the largest 32-bit integer func Max32(first int32, second int32) int32 { if first > second { return first } return second } // Min returns the smallest integer func Min(first int, second int) int { if first <= second { return first } return second } // Min32 returns the smallest 32-bit integer func Min32(first int32, second int32) int32 { if first <= second { return first } return second } // Constrain32 limits the given 32-bit integer with the upper and lower bounds func Constrain32(val int32, min int32, max int32) int32 { if val < min { return min } if val > max { return max } return val } // Constrain limits the given integer with the upper and lower bounds func Constrain(val int, min int, max int) int { if val < min { return min } if val > max { return max } return val } func AsUint16(val int) uint16 { if val > math.MaxUint16 { return math.MaxUint16 } else if val < 0 { return 0 } return uint16(val) } // DurWithin limits the given time.Duration with the upper and lower bounds func DurWithin( val time.Duration, min time.Duration, max time.Duration) time.Duration { if val < min { return min } if val > max { return max } return val } // IsTty returns true is stdin is a terminal func IsTty() bool { return isatty.IsTerminal(os.Stdin.Fd()) } // Once returns a function that returns the specified boolean value only once func Once(nextResponse bool) func() bool { state := nextResponse return func() bool { prevState := state state = false return prevState } } fzf-0.20.0/src/util/util_test.go000066400000000000000000000010721357617647500165120ustar00rootroot00000000000000package util import "testing" func TestMax(t *testing.T) { if Max(-2, 5) != 5 { t.Error("Invalid result") } } func TestContrain(t *testing.T) { if Constrain(-3, -1, 3) != -1 { t.Error("Expected", -1) } if Constrain(2, -1, 3) != 2 { t.Error("Expected", 2) } if Constrain(5, -1, 3) != 3 { t.Error("Expected", 3) } } func TestOnce(t *testing.T) { o := Once(false) if o() { t.Error("Expected: false") } if o() { t.Error("Expected: false") } o = Once(true) if !o() { t.Error("Expected: true") } if o() { t.Error("Expected: false") } } fzf-0.20.0/src/util/util_unix.go000066400000000000000000000021241357617647500165150ustar00rootroot00000000000000// +build !windows package util import ( "os" "os/exec" "syscall" ) // ExecCommand executes the given command with $SHELL func ExecCommand(command string, setpgid bool) *exec.Cmd { shell := os.Getenv("SHELL") if len(shell) == 0 { shell = "sh" } return ExecCommandWith(shell, command, setpgid) } // ExecCommandWith executes the given command with the specified shell func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd { cmd := exec.Command(shell, "-c", command) if setpgid { cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} } return cmd } // KillCommand kills the process for the given command func KillCommand(cmd *exec.Cmd) error { return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) } // IsWindows returns true on Windows func IsWindows() bool { return false } // SetNonblock executes syscall.SetNonblock on file descriptor func SetNonblock(file *os.File, nonblock bool) { syscall.SetNonblock(int(file.Fd()), nonblock) } // Read executes syscall.Read on file descriptor func Read(fd int, b []byte) (int, error) { return syscall.Read(int(fd), b) } fzf-0.20.0/src/util/util_windows.go000066400000000000000000000023541357617647500172310ustar00rootroot00000000000000// +build windows package util import ( "fmt" "os" "os/exec" "syscall" ) // ExecCommand executes the given command with cmd func ExecCommand(command string, setpgid bool) *exec.Cmd { return ExecCommandWith("cmd", command, setpgid) } // ExecCommandWith executes the given command with cmd. _shell parameter is // ignored on Windows. // FIXME: setpgid is unused. We set it in the Unix implementation so that we // can kill preview process with its child processes at once. func ExecCommandWith(_shell string, command string, setpgid bool) *exec.Cmd { cmd := exec.Command("cmd") cmd.SysProcAttr = &syscall.SysProcAttr{ HideWindow: false, CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command), CreationFlags: 0, } return cmd } // KillCommand kills the process for the given command func KillCommand(cmd *exec.Cmd) error { return cmd.Process.Kill() } // IsWindows returns true on Windows func IsWindows() bool { return true } // SetNonblock executes syscall.SetNonblock on file descriptor func SetNonblock(file *os.File, nonblock bool) { syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock) } // Read executes syscall.Read on file descriptor func Read(fd int, b []byte) (int, error) { return syscall.Read(syscall.Handle(fd), b) } fzf-0.20.0/test/000077500000000000000000000000001357617647500133625ustar00rootroot00000000000000fzf-0.20.0/test/fzf.vader000066400000000000000000000133441357617647500151770ustar00rootroot00000000000000Execute (Setup): let g:dir = fnamemodify(g:vader_file, ':p:h') unlet! g:fzf_layout g:fzf_action g:fzf_history_dir Log 'Test directory: ' . g:dir Save &acd Execute (fzf#run with dir option): let cwd = getcwd() let result = fzf#run({ 'source': 'git ls-files', 'options': '--filter=vdr', 'dir': g:dir }) AssertEqual ['fzf.vader'], result AssertEqual 0, haslocaldir() AssertEqual getcwd(), cwd execute 'lcd' fnameescape(cwd) let result = sort(fzf#run({ 'source': 'git ls-files', 'options': '--filter e', 'dir': g:dir })) AssertEqual ['fzf.vader', 'test_go.rb'], result AssertEqual 1, haslocaldir() AssertEqual getcwd(), cwd Execute (fzf#run with Funcref command): let g:ret = [] function! g:FzfTest(e) call add(g:ret, a:e) endfunction let result = sort(fzf#run({ 'source': 'git ls-files', 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir })) AssertEqual ['fzf.vader', 'test_go.rb'], result AssertEqual ['fzf.vader', 'test_go.rb'], sort(g:ret) Execute (fzf#run with string source): let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' })) AssertEqual ['hi'], result Execute (fzf#run with list source): let result = sort(fzf#run({ 'source': ['hello', 'world'], 'options': '-f e' })) AssertEqual ['hello'], result let result = sort(fzf#run({ 'source': ['hello', 'world'], 'options': '-f o' })) AssertEqual ['hello', 'world'], result Execute (fzf#run with string source): let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' })) AssertEqual ['hi'], result Execute (fzf#run with dir option and noautochdir): set noacd let cwd = getcwd() call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'}) " No change in working directory AssertEqual cwd, getcwd() call fzf#run({'source': ['/foobar'], 'sink': 'tabe', 'dir': '/tmp', 'options': '-1'}) AssertEqual cwd, getcwd() tabclose AssertEqual cwd, getcwd() Execute (Incomplete fzf#run with dir option and autochdir): set acd let cwd = getcwd() call fzf#run({'source': [], 'sink': 'e', 'dir': '/tmp', 'options': '-0'}) " No change in working directory even if &acd is set AssertEqual cwd, getcwd() Execute (FIXME: fzf#run with dir option and autochdir): set acd call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'}) " Working directory changed due to &acd AssertEqual '/foobar', expand('%') AssertEqual '/', getcwd() Execute (fzf#run with dir option and autochdir when final cwd is same as dir): set acd cd /tmp call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/', 'options': '-1'}) " Working directory changed due to &acd AssertEqual '/', getcwd() Execute (fzf#wrap): AssertThrows fzf#wrap({'foo': 'bar'}) let opts = fzf#wrap('foobar') Log opts AssertEqual '~40%', opts.down Assert opts.options =~ '--expect=' Assert !has_key(opts, 'sink') Assert has_key(opts, 'sink*') let opts = fzf#wrap('foobar', {}, 0) Log opts AssertEqual '~40%', opts.down let opts = fzf#wrap('foobar', {}, 1) Log opts Assert !has_key(opts, 'down') let opts = fzf#wrap('foobar', {'down': '50%'}) Log opts AssertEqual '50%', opts.down let opts = fzf#wrap('foobar', {'down': '50%'}, 1) Log opts Assert !has_key(opts, 'down') let opts = fzf#wrap('foobar', {'sink': 'e'}) Log opts AssertEqual 'e', opts.sink Assert !has_key(opts, 'sink*') let opts = fzf#wrap('foobar', {'options': '--reverse'}) Log opts Assert opts.options =~ '--expect=' Assert opts.options =~ '--reverse' let g:fzf_layout = {'window': 'enew'} let opts = fzf#wrap('foobar') Log opts AssertEqual 'enew', opts.window let opts = fzf#wrap('foobar', {}, 1) Log opts Assert !has_key(opts, 'window') let opts = fzf#wrap('foobar', {'right': '50%'}) Log opts Assert !has_key(opts, 'window') AssertEqual '50%', opts.right let opts = fzf#wrap('foobar', {'right': '50%'}, 1) Log opts Assert !has_key(opts, 'window') Assert !has_key(opts, 'right') let g:fzf_action = {'a': 'tabe'} let opts = fzf#wrap('foobar') Log opts Assert opts.options =~ '--expect=a' Assert !has_key(opts, 'sink') Assert has_key(opts, 'sink*') let opts = fzf#wrap('foobar', {'sink': 'e'}) Log opts AssertEqual 'e', opts.sink Assert !has_key(opts, 'sink*') let g:fzf_history_dir = '/tmp' let opts = fzf#wrap('foobar', {'options': '--color light'}) Log opts Assert opts.options =~ "--history '/tmp/foobar'" Assert opts.options =~ '--color light' let g:fzf_colors = { 'fg': ['fg', 'Error'] } let opts = fzf#wrap({}) Assert opts.options =~ '^--color=fg:' Execute (fzf#shellescape with sh): AssertEqual '''''', fzf#shellescape('', 'sh') AssertEqual '''\''', fzf#shellescape('\', 'sh') AssertEqual '''""''', fzf#shellescape('""', 'sh') AssertEqual '''foobar>''', fzf#shellescape('foobar>', 'sh') AssertEqual '''\\\"\\\''', fzf#shellescape('\\\"\\\', 'sh') AssertEqual '''echo ''\''''a''\'''' && echo ''\''''b''\''''''', fzf#shellescape('echo ''a'' && echo ''b''', 'sh') Execute (fzf#shellescape with cmd.exe): AssertEqual '^"^"', fzf#shellescape('', 'cmd.exe') AssertEqual '^"\\^"', fzf#shellescape('\', 'cmd.exe') AssertEqual '^"\^"\^"^"', fzf#shellescape('""', 'cmd.exe') AssertEqual '^"foobar^>^"', fzf#shellescape('foobar>', 'cmd.exe') AssertEqual '^"\\\\\\\^"\\\\\\^"', fzf#shellescape('\\\"\\\', 'cmd.exe') AssertEqual '^"echo ''a'' ^&^& echo ''b''^"', fzf#shellescape('echo ''a'' && echo ''b''', 'cmd.exe') AssertEqual '^"C:\Program Files ^(x86^)\\^"', fzf#shellescape('C:\Program Files (x86)\', 'cmd.exe') AssertEqual '^"C:/Program Files ^(x86^)/^"', fzf#shellescape('C:/Program Files (x86)/', 'cmd.exe') AssertEqual '^"%%USERPROFILE%%^"', fzf#shellescape('%USERPROFILE%', 'cmd.exe') Execute (Cleanup): unlet g:dir Restore fzf-0.20.0/test/test_go.rb000077500000000000000000001741521357617647500153700ustar00rootroot00000000000000#!/usr/bin/env ruby # encoding: utf-8 # frozen_string_literal: true # rubocop:disable Metrics/LineLength # rubocop:disable Metrics/MethodLength require 'minitest/autorun' require 'fileutils' require 'English' require 'shellwords' DEFAULT_TIMEOUT = 20 FILE = File.expand_path(__FILE__) base = File.expand_path('../../', __FILE__) Dir.chdir base FZF = "FZF_DEFAULT_OPTS= FZF_DEFAULT_COMMAND= #{base}/bin/fzf" class NilClass def include?(_str) false end def start_with?(_str) false end def end_with?(_str) false end end def wait since = Time.now while Time.now - since < DEFAULT_TIMEOUT return if yield sleep 0.05 end raise 'timeout' end class Shell class << self def unsets 'unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS FZF_ALT_C_COMMAND FZF_ALT_C_OPTS FZF_CTRL_R_OPTS;' end def bash 'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.bash' end def zsh FileUtils.mkdir_p '/tmp/fzf-zsh' FileUtils.cp File.expand_path('~/.fzf.zsh'), '/tmp/fzf-zsh/.zshrc' 'PS1= PROMPT_COMMAND= HISTSIZE=100 ZDOTDIR=/tmp/fzf-zsh zsh' end def fish 'fish' end end end class Tmux TEMPNAME = '/tmp/fzf-test.txt' attr_reader :win def initialize(shell = :bash) @win = case shell when :bash go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.bash}'").first when :zsh go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.zsh}'").first when :fish go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.fish}'").first else raise "Unknown shell: #{shell}" end go("set-window-option -t #{@win} pane-base-index 0") @lines = `tput lines`.chomp.to_i return unless shell == :fish send_keys('function fish_prompt; end; clear', :Enter) self.until(&:empty?) end def kill go("kill-window -t #{win} 2> /dev/null") end def send_keys(*args) target = if args.last.is_a?(Hash) hash = args.pop go("select-window -t #{win}") "#{win}.#{hash[:pane]}" else win end enum = (args + [nil]).each_cons(2) loop do pair = enum.next if pair.first == :Escape arg = pair.compact.map { |key| %("#{key}") }.join(' ') go(%(send-keys -t #{target} #{arg})) enum.next if pair.last else go(%(send-keys -t #{target} "#{pair.first}")) end break unless pair.last end end def paste(str) `tmux setb '#{str.gsub("'", "'\\''")}' \\; pasteb -t #{win} \\; send-keys -t #{win} Enter` end def capture(pane = 0) File.unlink TEMPNAME while File.exist? TEMPNAME wait do go("capture-pane -t #{win}.#{pane} \\; save-buffer #{TEMPNAME} 2> /dev/null") $CHILD_STATUS.exitstatus.zero? end File.read(TEMPNAME).split($INPUT_RECORD_SEPARATOR)[0, @lines].reverse.drop_while(&:empty?).reverse end def until(refresh = false, pane = 0) lines = nil begin wait do lines = capture(pane) class << lines def counts lazy .map { |l| l.scan %r{^. ([0-9]+)\/([0-9]+)( \(([0-9]+)\))?} } .reject(&:empty?) .first&.first&.map(&:to_i)&.values_at(0, 1, 3) || [0, 0, 0] end def match_count counts[0] end def item_count counts[1] end def select_count counts[2] end def any_include?(val) method = val.is_a?(Regexp) ? :match : :include? select { |line| line.send method, val }.first end end yield(lines).tap do |ok| send_keys 'C-l' if refresh && !ok end end rescue StandardError puts $ERROR_INFO.backtrace puts '>' * 80 puts lines puts '<' * 80 raise end lines end def prepare tries = 0 begin self.until do |lines| send_keys 'C-u', 'hello' lines[-1].end_with?('hello') end rescue StandardError (tries += 1) < 5 ? retry : raise end send_keys 'C-u' end private def go(*args) `tmux #{args.join ' '}`.split($INPUT_RECORD_SEPARATOR) end end class TestBase < Minitest::Test TEMPNAME = '/tmp/output' attr_reader :tmux def tempname @temp_suffix ||= 0 [TEMPNAME, caller_locations.map(&:label).find { |l| l =~ /^test_/ }, @temp_suffix].join '-' end def writelines(path, lines) File.unlink path while File.exist? path File.open(path, 'w') { |f| f << lines.join($INPUT_RECORD_SEPARATOR) + $INPUT_RECORD_SEPARATOR } end def readonce wait { File.exist?(tempname) } File.read(tempname) ensure File.unlink tempname while File.exist?(tempname) @temp_suffix += 1 tmux.prepare end def fzf(*opts) fzf!(*opts) + " > #{tempname}.tmp; mv #{tempname}.tmp #{tempname}" end def fzf!(*opts) opts = opts.map do |o| case o when Symbol o = o.to_s o.length > 1 ? "--#{o.tr('_', '-')}" : "-#{o}" when String, Numeric o.to_s end end.compact "#{FZF} #{opts.join ' '}" end end class TestGoFZF < TestBase def setup super @tmux = Tmux.new end def teardown @tmux.kill end def test_vanilla tmux.send_keys "seq 1 100000 | #{fzf}", :Enter tmux.until { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ } lines = tmux.capture assert_equal ' 2', lines[-4] assert_equal '> 1', lines[-3] assert_equal ' 100000/100000', lines[-2] assert_equal '>', lines[-1] # Testing basic key bindings tmux.send_keys '99', 'C-a', '1', 'C-f', '3', 'C-b', 'C-h', 'C-u', 'C-e', 'C-y', 'C-k', 'Tab', 'BTab' tmux.until do |lines| '> 3910' == lines[-4] && ' 391' == lines[-3] && ' 856/100000' == lines[-2] && '> 391' == lines[-1] end tmux.send_keys :Enter assert_equal '3910', readonce.chomp end def test_fzf_default_command tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND='echo hello'"), :Enter tmux.until { |lines| lines.last =~ /^>/ } tmux.send_keys :Enter assert_equal 'hello', readonce.chomp end def test_fzf_default_command_failure tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', 'FZF_DEFAULT_COMMAND=false'), :Enter tmux.until { |lines| lines[-2].include?('Command failed: false') } tmux.send_keys :Enter end def test_key_bindings tmux.send_keys "#{FZF} -q 'foo bar foo-bar'", :Enter tmux.until { |lines| lines.last =~ /^>/ } # CTRL-A tmux.send_keys 'C-A', '(' tmux.until { |lines| lines.last == '> (foo bar foo-bar' } # META-F tmux.send_keys :Escape, :f, ')' tmux.until { |lines| lines.last == '> (foo) bar foo-bar' } # CTRL-B tmux.send_keys 'C-B', 'var' tmux.until { |lines| lines.last == '> (foovar) bar foo-bar' } # Left, CTRL-D tmux.send_keys :Left, :Left, 'C-D' tmux.until { |lines| lines.last == '> (foovr) bar foo-bar' } # META-BS tmux.send_keys :Escape, :BSpace tmux.until { |lines| lines.last == '> (r) bar foo-bar' } # CTRL-Y tmux.send_keys 'C-Y', 'C-Y' tmux.until { |lines| lines.last == '> (foovfoovr) bar foo-bar' } # META-B tmux.send_keys :Escape, :b, :Space, :Space tmux.until { |lines| lines.last == '> ( foovfoovr) bar foo-bar' } # CTRL-F / Right tmux.send_keys 'C-F', :Right, '/' tmux.until { |lines| lines.last == '> ( fo/ovfoovr) bar foo-bar' } # CTRL-H / BS tmux.send_keys 'C-H', :BSpace tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-bar' } # CTRL-E tmux.send_keys 'C-E', 'baz' tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-barbaz' } # CTRL-U tmux.send_keys 'C-U' tmux.until { |lines| lines.last == '>' } # CTRL-Y tmux.send_keys 'C-Y' tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-barbaz' } # CTRL-W tmux.send_keys 'C-W', 'bar-foo' tmux.until { |lines| lines.last == '> ( fovfoovr) bar bar-foo' } # META-D tmux.send_keys :Escape, :b, :Escape, :b, :Escape, :d, 'C-A', 'C-Y' tmux.until { |lines| lines.last == '> bar( fovfoovr) bar -foo' } # CTRL-M tmux.send_keys 'C-M' tmux.until { |lines| lines.last !~ /^>/ } end def test_file_word tmux.send_keys "#{FZF} -q '--/foo bar/foo-bar/baz' --filepath-word", :Enter tmux.until { |lines| lines.last =~ /^>/ } tmux.send_keys :Escape, :b tmux.send_keys :Escape, :b tmux.send_keys :Escape, :b tmux.send_keys :Escape, :d tmux.send_keys :Escape, :f tmux.send_keys :Escape, :BSpace tmux.until { |lines| lines.last == '> --///baz' } end def test_multi_order tmux.send_keys "seq 1 10 | #{fzf :multi}", :Enter tmux.until { |lines| lines.last =~ /^>/ } tmux.send_keys :Tab, :Up, :Up, :Tab, :Tab, :Tab, # 3, 2 'C-K', 'C-K', 'C-K', 'C-K', :BTab, :BTab, # 5, 6 :PgUp, 'C-J', :Down, :Tab, :Tab # 8, 7 tmux.until { |lines| lines[-2].include? '(6)' } tmux.send_keys 'C-M' assert_equal %w[3 2 5 6 8 7], readonce.split($INPUT_RECORD_SEPARATOR) end def test_multi_max tmux.send_keys "seq 1 10 | #{FZF} -m 3 --bind A:select-all,T:toggle-all --preview 'echo [{+}]/{}'", :Enter tmux.until { |lines| lines.item_count == 10 } tmux.send_keys '1' tmux.until do |lines| lines[1].include?('[1]/1') && lines[-2].include?('2/10') end tmux.send_keys 'A' tmux.until do |lines| lines[1].include?('[1 10]/1') && lines[-2].include?('2/10 (2/3)') end tmux.send_keys :BSpace tmux.until { |lines| lines[-2].include?('10/10 (2/3)') } tmux.send_keys 'T' tmux.until do |lines| lines[1].include?('[2 3 4]/1') && lines[-2].include?('10/10 (3/3)') end %w[T A].each do |key| tmux.send_keys key tmux.until do |lines| lines[1].include?('[1 5 6]/1') && lines[-2].include?('10/10 (3/3)') end end tmux.send_keys :BTab tmux.until do |lines| lines[1].include?('[5 6]/2') && lines[-2].include?('10/10 (2/3)') end [:BTab, :BTab, 'A'].each do |key| tmux.send_keys key tmux.until do |lines| lines[1].include?('[5 6 2]/3') && lines[-2].include?('10/10 (3/3)') end end tmux.send_keys '2' tmux.until { |lines| lines[-2].include?('1/10 (3/3)') } tmux.send_keys 'T' tmux.until do |lines| lines[1].include?('[5 6]/2') && lines[-2].include?('1/10 (2/3)') end tmux.send_keys :BSpace tmux.until { |lines| lines[-2].include?('10/10 (2/3)') } tmux.send_keys 'A' tmux.until do |lines| lines[1].include?('[5 6 1]/1') && lines[-2].include?('10/10 (3/3)') end end def test_with_nth [true, false].each do |multi| tmux.send_keys "(echo ' 1st 2nd 3rd/'; echo ' first second third/') | #{fzf multi && :multi, :x, :nth, 2, :with_nth, '2,-1,1'}", :Enter tmux.until { |lines| lines[-2].include?('2/2') } # Transformed list lines = tmux.capture assert_equal ' second third/first', lines[-4] assert_equal '> 2nd 3rd/1st', lines[-3] # However, the output must not be transformed if multi tmux.send_keys :BTab, :BTab tmux.until { |lines| lines[-2].include?('(2)') } tmux.send_keys :Enter assert_equal [' 1st 2nd 3rd/', ' first second third/'], readonce.split($INPUT_RECORD_SEPARATOR) else tmux.send_keys '^', '3' tmux.until { |lines| lines[-2].include?('1/2') } tmux.send_keys :Enter assert_equal [' 1st 2nd 3rd/'], readonce.split($INPUT_RECORD_SEPARATOR) end end end def test_scroll [true, false].each do |rev| tmux.send_keys "seq 1 100 | #{fzf rev && :reverse}", :Enter tmux.until { |lines| lines.include? ' 100/100' } tmux.send_keys(*Array.new(110) { rev ? :Down : :Up }) tmux.until { |lines| lines.include? '> 100' } tmux.send_keys :Enter assert_equal '100', readonce.chomp end end def test_select_1 tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 5555, :'1'}", :Enter assert_equal %w[5555 55], readonce.split($INPUT_RECORD_SEPARATOR) end def test_exit_0 tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 555_555, :'0'}", :Enter assert_equal ['555555'], readonce.split($INPUT_RECORD_SEPARATOR) end def test_select_1_exit_0_fail [:'0', :'1', %i[1 0]].each do |opt| tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter tmux.until { |lines| lines.last =~ /^> 5/ } tmux.send_keys :BTab, :BTab, :BTab tmux.until { |lines| lines[-2].include?('(3)') } tmux.send_keys :Enter assert_equal %w[5 5 50 51], readonce.split($INPUT_RECORD_SEPARATOR) end end def test_query_unicode tmux.paste "(echo abc; echo 가나다) | #{fzf :query, '가다'}" tmux.until { |lines| lines[-2].include? '1/2' } tmux.send_keys :Enter assert_equal ['가나다'], readonce.split($INPUT_RECORD_SEPARATOR) end def test_sync tmux.send_keys "seq 1 100 | #{fzf! :multi} | awk '{print \\$1 \\$1}' | #{fzf :sync}", :Enter tmux.until { |lines| lines[-1] == '>' } tmux.send_keys 9 tmux.until { |lines| lines[-2] == ' 19/100' } tmux.send_keys :BTab, :BTab, :BTab tmux.until { |lines| lines[-2].include?('(3)') } tmux.send_keys :Enter tmux.until { |lines| lines[-1] == '>' } tmux.send_keys 'C-K', :Enter assert_equal ['9090'], readonce.split($INPUT_RECORD_SEPARATOR) end def test_tac tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.send_keys :BTab, :BTab, :BTab tmux.until { |lines| lines[-2].include?('(3)') } tmux.send_keys :Enter assert_equal %w[1000 999 998], readonce.split($INPUT_RECORD_SEPARATOR) end def test_tac_sort tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.send_keys '99' tmux.until { |lines| lines[-2].include? '28/1000' } tmux.send_keys :BTab, :BTab, :BTab tmux.until { |lines| lines[-2].include?('(3)') } tmux.send_keys :Enter assert_equal %w[99 999 998], readonce.split($INPUT_RECORD_SEPARATOR) end def test_tac_nosort tmux.send_keys "seq 1 1000 | #{fzf :tac, :no_sort, :multi}", :Enter tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.send_keys '00' tmux.until { |lines| lines[-2].include? '10/1000' } tmux.send_keys :BTab, :BTab, :BTab tmux.until { |lines| lines[-2].include?('(3)') } tmux.send_keys :Enter assert_equal %w[1000 900 800], readonce.split($INPUT_RECORD_SEPARATOR) end def test_expect test = lambda do |key, feed, expected = key| tmux.send_keys "seq 1 100 | #{fzf :expect, key}; sync", :Enter tmux.until { |lines| lines[-2].include? '100/100' } tmux.send_keys '55' tmux.until { |lines| lines[-2].include? '1/100' } tmux.send_keys(*feed) tmux.prepare assert_equal [expected, '55'], readonce.split($INPUT_RECORD_SEPARATOR) end test.call 'ctrl-t', 'C-T' test.call 'ctrl-t', 'Enter', '' test.call 'alt-c', %i[Escape c] test.call 'f1', 'f1' test.call 'f2', 'f2' test.call 'f3', 'f3' test.call 'f2,f4', 'f2', 'f2' test.call 'f2,f4', 'f4', 'f4' test.call 'alt-/', %i[Escape /] %w[f5 f6 f7 f8 f9 f10].each do |key| test.call 'f5,f6,f7,f8,f9,f10', key, key end test.call '@', '@' end def test_expect_print_query tmux.send_keys "seq 1 100 | #{fzf '--expect=alt-z', :print_query}", :Enter tmux.until { |lines| lines[-2].include? '100/100' } tmux.send_keys '55' tmux.until { |lines| lines[-2].include? '1/100' } tmux.send_keys :Escape, :z assert_equal ['55', 'alt-z', '55'], readonce.split($INPUT_RECORD_SEPARATOR) end def test_expect_printable_character_print_query tmux.send_keys "seq 1 100 | #{fzf '--expect=z --print-query'}", :Enter tmux.until { |lines| lines[-2].include? '100/100' } tmux.send_keys '55' tmux.until { |lines| lines[-2].include? '1/100' } tmux.send_keys 'z' assert_equal %w[55 z 55], readonce.split($INPUT_RECORD_SEPARATOR) end def test_expect_print_query_select_1 tmux.send_keys "seq 1 100 | #{fzf '-q55 -1 --expect=alt-z --print-query'}", :Enter assert_equal ['55', '', '55'], readonce.split($INPUT_RECORD_SEPARATOR) end def test_toggle_sort ['--toggle-sort=ctrl-r', '--bind=ctrl-r:toggle-sort'].each do |opt| tmux.send_keys "seq 1 111 | #{fzf "-m +s --tac #{opt} -q11"}", :Enter tmux.until { |lines| lines[-3].include? '> 111' } tmux.send_keys :Tab tmux.until { |lines| lines[-2].include? '4/111 -S (1)' } tmux.send_keys 'C-R' tmux.until { |lines| lines[-3].include? '> 11' } tmux.send_keys :Tab tmux.until { |lines| lines[-2].include? '4/111 +S (2)' } tmux.send_keys :Enter assert_equal %w[111 11], readonce.split($INPUT_RECORD_SEPARATOR) end end def test_unicode_case writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4] assert_equal %w[СТРОКА2 Строка4], `#{FZF} -fС < #{tempname}`.split($INPUT_RECORD_SEPARATOR) assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `#{FZF} -fс < #{tempname}`.split($INPUT_RECORD_SEPARATOR) end def test_tiebreak input = %w[ --foobar-------- -----foobar--- ----foobar-- -------foobar- ] writelines tempname, input assert_equal input, `#{FZF} -ffoobar --tiebreak=index < #{tempname}`.split($INPUT_RECORD_SEPARATOR) by_length = %w[ ----foobar-- -----foobar--- -------foobar- --foobar-------- ] assert_equal by_length, `#{FZF} -ffoobar < #{tempname}`.split($INPUT_RECORD_SEPARATOR) assert_equal by_length, `#{FZF} -ffoobar --tiebreak=length < #{tempname}`.split($INPUT_RECORD_SEPARATOR) by_begin = %w[ --foobar-------- ----foobar-- -----foobar--- -------foobar- ] assert_equal by_begin, `#{FZF} -ffoobar --tiebreak=begin < #{tempname}`.split($INPUT_RECORD_SEPARATOR) assert_equal by_begin, `#{FZF} -f"!z foobar" -x --tiebreak begin < #{tempname}`.split($INPUT_RECORD_SEPARATOR) assert_equal %w[ -------foobar- ----foobar-- -----foobar--- --foobar-------- ], `#{FZF} -ffoobar --tiebreak end < #{tempname}`.split($INPUT_RECORD_SEPARATOR) assert_equal input, `#{FZF} -f"!z" -x --tiebreak end < #{tempname}`.split($INPUT_RECORD_SEPARATOR) end def test_tiebreak_index_begin writelines tempname, [ 'xoxxxxxoxx', 'xoxxxxxox', 'xxoxxxoxx', 'xxxoxoxxx', 'xxxxoxox', ' xxoxoxxx' ] assert_equal [ 'xxxxoxox', ' xxoxoxxx', 'xxxoxoxxx', 'xxoxxxoxx', 'xoxxxxxox', 'xoxxxxxoxx' ], `#{FZF} -foo < #{tempname}`.split($INPUT_RECORD_SEPARATOR) assert_equal [ 'xxxoxoxxx', 'xxxxoxox', ' xxoxoxxx', 'xxoxxxoxx', 'xoxxxxxoxx', 'xoxxxxxox' ], `#{FZF} -foo --tiebreak=index < #{tempname}`.split($INPUT_RECORD_SEPARATOR) # Note that --tiebreak=begin is now based on the first occurrence of the # first character on the pattern assert_equal [ ' xxoxoxxx', 'xxxoxoxxx', 'xxxxoxox', 'xxoxxxoxx', 'xoxxxxxoxx', 'xoxxxxxox' ], `#{FZF} -foo --tiebreak=begin < #{tempname}`.split($INPUT_RECORD_SEPARATOR) assert_equal [ ' xxoxoxxx', 'xxxoxoxxx', 'xxxxoxox', 'xxoxxxoxx', 'xoxxxxxox', 'xoxxxxxoxx' ], `#{FZF} -foo --tiebreak=begin,length < #{tempname}`.split($INPUT_RECORD_SEPARATOR) end def test_tiebreak_begin_algo_v2 writelines tempname, [ 'baz foo bar', 'foo bar baz' ] assert_equal [ 'foo bar baz', 'baz foo bar' ], `#{FZF} -fbar --tiebreak=begin --algo=v2 < #{tempname}`.split($INPUT_RECORD_SEPARATOR) end def test_tiebreak_end writelines tempname, [ 'xoxxxxxxxx', 'xxoxxxxxxx', 'xxxoxxxxxx', 'xxxxoxxxx', 'xxxxxoxxx', ' xxxxoxxx' ] assert_equal [ ' xxxxoxxx', 'xxxxoxxxx', 'xxxxxoxxx', 'xoxxxxxxxx', 'xxoxxxxxxx', 'xxxoxxxxxx' ], `#{FZF} -fo < #{tempname}`.split($INPUT_RECORD_SEPARATOR) assert_equal [ 'xxxxxoxxx', ' xxxxoxxx', 'xxxxoxxxx', 'xxxoxxxxxx', 'xxoxxxxxxx', 'xoxxxxxxxx' ], `#{FZF} -fo --tiebreak=end < #{tempname}`.split($INPUT_RECORD_SEPARATOR) assert_equal [ 'xxxxxoxxx', ' xxxxoxxx', 'xxxxoxxxx', 'xxxoxxxxxx', 'xxoxxxxxxx', 'xoxxxxxxxx' ], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.split($INPUT_RECORD_SEPARATOR) end def test_tiebreak_length_with_nth input = %w[ 1:hell 123:hello 12345:he 1234567:h ] writelines tempname, input output = %w[ 1:hell 12345:he 123:hello 1234567:h ] assert_equal output, `#{FZF} -fh < #{tempname}`.split($INPUT_RECORD_SEPARATOR) # Since 0.16.8, --nth doesn't affect --tiebreak assert_equal output, `#{FZF} -fh -n2 -d: < #{tempname}`.split($INPUT_RECORD_SEPARATOR) end def test_invalid_cache tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter tmux.until { |lines| lines[-2].include? '2/3' } tmux.send_keys :BSpace tmux.until { |lines| lines[-2].include? '3/3' } tmux.send_keys :D tmux.until { |lines| lines[-2].include? '1/3' } tmux.send_keys :Enter end def test_invalid_cache_query_type command = %[(echo 'foo\\$bar'; echo 'barfoo'; echo 'foo^bar'; echo \\"foo'1-2\\"; seq 100) | #{fzf}] # Suffix match tmux.send_keys command, :Enter tmux.until { |lines| lines.match_count == 104 } tmux.send_keys 'foo$' tmux.until { |lines| lines.match_count == 1 } tmux.send_keys 'bar' tmux.until { |lines| lines.match_count == 1 } tmux.send_keys :Enter # Prefix match tmux.prepare tmux.send_keys command, :Enter tmux.until { |lines| lines.match_count == 104 } tmux.send_keys '^bar' tmux.until { |lines| lines.match_count == 1 } tmux.send_keys 'C-a', 'foo' tmux.until { |lines| lines.match_count == 1 } tmux.send_keys :Enter # Exact match tmux.prepare tmux.send_keys command, :Enter tmux.until { |lines| lines.match_count == 104 } tmux.send_keys "'12" tmux.until { |lines| lines.match_count == 1 } tmux.send_keys 'C-a', 'foo' tmux.until { |lines| lines.match_count == 1 } end def test_smart_case_for_each_term assert_equal 1, `echo Foo bar | #{FZF} -x -f "foo Fbar" | wc -l`.to_i end def test_bind tmux.send_keys "seq 1 1000 | #{fzf '-m --bind=ctrl-j:accept,u:up,T:toggle-up,t:toggle'}", :Enter tmux.until { |lines| lines[-2].end_with? '/1000' } tmux.send_keys 'uuu', 'TTT', 'tt', 'uu', 'ttt', 'C-j' assert_equal %w[4 5 6 9], readonce.split($INPUT_RECORD_SEPARATOR) end def test_bind_print_query tmux.send_keys "seq 1 1000 | #{fzf '-m --bind=ctrl-j:print-query'}", :Enter tmux.until { |lines| lines[-2].end_with? '/1000' } tmux.send_keys 'print-my-query', 'C-j' assert_equal %w[print-my-query], readonce.split($INPUT_RECORD_SEPARATOR) end def test_bind_replace_query tmux.send_keys "seq 1 1000 | #{fzf '--print-query --bind=ctrl-j:replace-query'}", :Enter tmux.send_keys '1' tmux.until { |lines| lines[-2].end_with? '272/1000' } tmux.send_keys 'C-k', 'C-j' tmux.until { |lines| lines[-2].end_with? '29/1000' } tmux.until { |lines| lines[-1].end_with? '> 10' } end def test_long_line data = '.' * 256 * 1024 File.open(tempname, 'w') do |f| f << data end assert_equal data, `#{FZF} -f . < #{tempname}`.chomp end def test_read0 lines = `find .`.split($INPUT_RECORD_SEPARATOR) assert_equal lines.last, `find . | #{FZF} -e -f "^#{lines.last}$"`.chomp assert_equal( lines.last, `find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp ) end def test_select_all_deselect_all_toggle_all tmux.send_keys "seq 100 | #{fzf '--bind ctrl-a:select-all,ctrl-d:deselect-all,ctrl-t:toggle-all --multi'}", :Enter tmux.until { |lines| lines[-2].include? '100/100' } tmux.send_keys :BTab, :BTab, :BTab tmux.until { |lines| lines[-2].include? '(3)' } tmux.send_keys 'C-t' tmux.until { |lines| lines[-2].include? '(97)' } tmux.send_keys 'C-a' tmux.until { |lines| lines[-2].include? '(100)' } tmux.send_keys :Tab, :Tab tmux.until { |lines| lines[-2].include? '(98)' } tmux.send_keys '100' tmux.until { |lines| lines.match_count == 1 } tmux.send_keys 'C-d' tmux.until { |lines| lines[-2].include? '(97)' } tmux.send_keys 'C-u' tmux.until { |lines| lines.match_count == 100 } tmux.send_keys 'C-d' tmux.until { |lines| !lines[-2].include? '(' } tmux.send_keys :BTab, :BTab tmux.until { |lines| lines[-2].include? '(2)' } tmux.send_keys 0 tmux.until { |lines| lines[-2].include? '10/100' } tmux.send_keys 'C-a' tmux.until { |lines| lines[-2].include? '(12)' } tmux.send_keys :Enter assert_equal %w[1 2 10 20 30 40 50 60 70 80 90 100], readonce.split($INPUT_RECORD_SEPARATOR) end def test_history history_file = '/tmp/fzf-test-history' # History with limited number of entries begin File.unlink history_file rescue nil end opts = "--history=#{history_file} --history-size=4" input = %w[00 11 22 33 44].map { |e| e + $INPUT_RECORD_SEPARATOR } input.each do |keys| tmux.send_keys "seq 100 | #{fzf opts}", :Enter tmux.until { |lines| lines[-2].include? '100/100' } tmux.send_keys keys tmux.until { |lines| lines[-2].include? '1/100' } tmux.send_keys :Enter readonce end assert_equal input[1..-1], File.readlines(history_file) # Update history entries (not changed on disk) tmux.send_keys "seq 100 | #{fzf opts}", :Enter tmux.until { |lines| lines[-2].include? '100/100' } tmux.send_keys 'C-p' tmux.until { |lines| lines[-1].end_with? '> 44' } tmux.send_keys 'C-p' tmux.until { |lines| lines[-1].end_with? '> 33' } tmux.send_keys :BSpace tmux.until { |lines| lines[-1].end_with? '> 3' } tmux.send_keys 1 tmux.until { |lines| lines[-1].end_with? '> 31' } tmux.send_keys 'C-p' tmux.until { |lines| lines[-1].end_with? '> 22' } tmux.send_keys 'C-n' tmux.until { |lines| lines[-1].end_with? '> 31' } tmux.send_keys 0 tmux.until { |lines| lines[-1].end_with? '> 310' } tmux.send_keys :Enter readonce assert_equal %w[22 33 44 310].map { |e| e + $INPUT_RECORD_SEPARATOR }, File.readlines(history_file) # Respect --bind option tmux.send_keys "seq 100 | #{fzf opts + ' --bind ctrl-p:next-history,ctrl-n:previous-history'}", :Enter tmux.until { |lines| lines[-2].include? '100/100' } tmux.send_keys 'C-n', 'C-n', 'C-n', 'C-n', 'C-p' tmux.until { |lines| lines[-1].end_with?('33') } tmux.send_keys :Enter ensure File.unlink history_file end def test_execute output = '/tmp/fzf-test-execute' opts = %[--bind \\"alt-a:execute(echo /{}/ >> #{output}),alt-b:execute[echo /{}{}/ >> #{output}],C:execute:echo /{}{}{}/ >> #{output}\\"] wait = ->(exp) { tmux.until { |lines| lines[-2].include? exp } } writelines tempname, %w[foo'bar foo"bar foo$bar] tmux.send_keys "cat #{tempname} | #{fzf opts}; sync", :Enter wait['3/3'] tmux.send_keys :Escape, :a wait['/3'] tmux.send_keys :Escape, :a wait['/3'] tmux.send_keys :Up tmux.send_keys :Escape, :b wait['/3'] tmux.send_keys :Escape, :b wait['/3'] tmux.send_keys :Up tmux.send_keys :C wait['3/3'] tmux.send_keys 'barfoo' wait['0/3'] tmux.send_keys :Escape, :a wait['/3'] tmux.send_keys :Escape, :b wait['/3'] tmux.send_keys :Enter readonce assert_equal %w[/foo'bar/ /foo'bar/ /foo"barfoo"bar/ /foo"barfoo"bar/ /foo$barfoo$barfoo$bar/], File.readlines(output).map(&:chomp) ensure begin File.unlink output rescue nil end end def test_execute_multi output = '/tmp/fzf-test-execute-multi' opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{+} >> #{output}; sync)\\"] writelines tempname, %w[foo'bar foo"bar foo$bar foobar] tmux.send_keys "cat #{tempname} | #{fzf opts}", :Enter tmux.until { |lines| lines[-2].include? '4/4' } tmux.send_keys :Escape, :a tmux.until { |lines| lines[-2].include? '/4' } tmux.send_keys :BTab, :BTab, :BTab tmux.send_keys :Escape, :a tmux.until { |lines| lines[-2].include? '/4' } tmux.send_keys :Tab, :Tab tmux.send_keys :Escape, :a tmux.until { |lines| lines[-2].include? '/4' } tmux.send_keys :Enter tmux.prepare readonce assert_equal [%(foo'bar/foo'bar), %(foo'bar foo"bar foo$bar/foo'bar foo"bar foo$bar), %(foo'bar foo"bar foobar/foo'bar foo"bar foobar)], File.readlines(output).map(&:chomp) ensure begin File.unlink output rescue nil end end def test_execute_plus_flag output = tempname + '.tmp' begin File.unlink output rescue nil end writelines tempname, ['foo bar', '123 456'] tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter execute = lambda do tmux.send_keys 'x', 'y' tmux.until { |lines| lines[-2].include? '0/2' } tmux.send_keys :BSpace tmux.until { |lines| lines[-2].include? '2/2' } end tmux.until { |lines| lines[-2].include? '2/2' } execute.call tmux.send_keys :Up tmux.send_keys :Tab execute.call tmux.send_keys :Tab execute.call tmux.send_keys :Enter tmux.prepare readonce assert_equal [ %(foo bar/foo bar/bar/bar), %(123 456/foo bar/456/bar), %(123 456 foo bar/foo bar/456 bar/bar) ], File.readlines(output).map(&:chomp) rescue begin File.unlink output rescue nil end end def test_execute_shell # Custom script to use as $SHELL output = tempname + '.out' begin File.unlink output rescue nil end writelines tempname, ['#!/usr/bin/env bash', "echo $1 / $2 > #{output}", 'sync'] system "chmod +x #{tempname}" tmux.send_keys "echo foo | SHELL=#{tempname} fzf --bind 'enter:execute:{}bar'", :Enter tmux.until { |lines| lines[-2].include? '1/1' } tmux.send_keys :Enter tmux.until { |lines| lines[-2].include? '1/1' } tmux.send_keys 'C-c' tmux.prepare assert_equal ["-c / 'foo'bar"], File.readlines(output).map(&:chomp) ensure begin File.unlink output rescue nil end end def test_cycle tmux.send_keys "seq 8 | #{fzf :cycle}", :Enter tmux.until { |lines| lines[-2].include? '8/8' } tmux.send_keys :Down tmux.until { |lines| lines[-10].start_with? '>' } tmux.send_keys :Down tmux.until { |lines| lines[-9].start_with? '>' } tmux.send_keys :Up tmux.until { |lines| lines[-10].start_with? '>' } tmux.send_keys :PgUp tmux.until { |lines| lines[-10].start_with? '>' } tmux.send_keys :Up tmux.until { |lines| lines[-3].start_with? '>' } tmux.send_keys :PgDn tmux.until { |lines| lines[-3].start_with? '>' } tmux.send_keys :Down tmux.until { |lines| lines[-10].start_with? '>' } end def test_header_lines tmux.send_keys "seq 100 | #{fzf '--header-lines=10 -q 5'}", :Enter 2.times do tmux.until do |lines| lines[-2].include?('/90') && lines[-3] == ' 1' && lines[-4] == ' 2' && lines[-13] == '> 50' end tmux.send_keys :Down end tmux.send_keys :Enter assert_equal '50', readonce.chomp end def test_header_lines_reverse tmux.send_keys "seq 100 | #{fzf '--header-lines=10 -q 5 --reverse'}", :Enter 2.times do tmux.until do |lines| lines[1].include?('/90') && lines[2] == ' 1' && lines[3] == ' 2' && lines[12] == '> 50' end tmux.send_keys :Up end tmux.send_keys :Enter assert_equal '50', readonce.chomp end def test_header_lines_reverse_list tmux.send_keys "seq 100 | #{fzf '--header-lines=10 -q 5 --layout=reverse-list'}", :Enter 2.times do tmux.until do |lines| lines[0] == '> 50' && lines[-4] == ' 2' && lines[-3] == ' 1' && lines[-2].include?('/90') end tmux.send_keys :Up end tmux.send_keys :Enter assert_equal '50', readonce.chomp end def test_header_lines_overflow tmux.send_keys "seq 100 | #{fzf '--header-lines=200'}", :Enter tmux.until do |lines| lines[-2].include?('0/0') && lines[-3].include?(' 1') end tmux.send_keys :Enter assert_equal '', readonce.chomp end def test_header_lines_with_nth tmux.send_keys "seq 100 | #{fzf '--header-lines 5 --with-nth 1,1,1,1,1'}", :Enter tmux.until do |lines| lines[-2].include?('95/95') && lines[-3] == ' 11111' && lines[-7] == ' 55555' && lines[-8] == '> 66666' end tmux.send_keys :Enter assert_equal '6', readonce.chomp end def test_header tmux.send_keys "seq 100 | #{fzf "--header \\\"\\$(head -5 #{FILE})\\\""}", :Enter header = File.readlines(FILE).take(5).map(&:strip) tmux.until do |lines| lines[-2].include?('100/100') && lines[-7..-3].map(&:strip) == header && lines[-8] == '> 1' end end def test_header_reverse tmux.send_keys "seq 100 | #{fzf "--header=\\\"\\$(head -5 #{FILE})\\\" --reverse"}", :Enter header = File.readlines(FILE).take(5).map(&:strip) tmux.until do |lines| lines[1].include?('100/100') && lines[2..6].map(&:strip) == header && lines[7] == '> 1' end end def test_header_reverse_list tmux.send_keys "seq 100 | #{fzf "--header=\\\"\\$(head -5 #{FILE})\\\" --layout=reverse-list"}", :Enter header = File.readlines(FILE).take(5).map(&:strip) tmux.until do |lines| lines[-2].include?('100/100') && lines[-7..-3].map(&:strip) == header && lines[0] == '> 1' end end def test_header_and_header_lines tmux.send_keys "seq 100 | #{fzf "--header-lines 10 --header \\\"\\$(head -5 #{FILE})\\\""}", :Enter header = File.readlines(FILE).take(5).map(&:strip) tmux.until do |lines| lines[-2].include?('90/90') && lines[-7...-2].map(&:strip) == header && lines[-17...-7].map(&:strip) == (1..10).map(&:to_s).reverse end end def test_header_and_header_lines_reverse tmux.send_keys "seq 100 | #{fzf "--reverse --header-lines 10 --header \\\"\\$(head -5 #{FILE})\\\""}", :Enter header = File.readlines(FILE).take(5).map(&:strip) tmux.until do |lines| lines[1].include?('90/90') && lines[2...7].map(&:strip) == header && lines[7...17].map(&:strip) == (1..10).map(&:to_s) end end def test_header_and_header_lines_reverse_list tmux.send_keys "seq 100 | #{fzf "--layout=reverse-list --header-lines 10 --header \\\"\\$(head -5 #{FILE})\\\""}", :Enter header = File.readlines(FILE).take(5).map(&:strip) tmux.until do |lines| lines[-2].include?('90/90') && lines[-7...-2].map(&:strip) == header && lines[-17...-7].map(&:strip) == (1..10).map(&:to_s).reverse end end def test_cancel tmux.send_keys "seq 10 | #{fzf '--bind 2:cancel'}", :Enter tmux.until { |lines| lines[-2].include?('10/10') } tmux.send_keys '123' tmux.until { |lines| lines[-1] == '> 3' && lines[-2].include?('1/10') } tmux.send_keys 'C-y', 'C-y' tmux.until { |lines| lines[-1] == '> 311' } tmux.send_keys 2 tmux.until { |lines| lines[-1] == '>' } tmux.send_keys 2 tmux.prepare end def test_margin tmux.send_keys "yes | head -1000 | #{fzf '--margin 5,3'}", :Enter tmux.until { |lines| lines[4] == '' && lines[5] == ' y' } tmux.send_keys :Enter end def test_margin_reverse tmux.send_keys "seq 1000 | #{fzf '--margin 7,5 --reverse'}", :Enter tmux.until { |lines| lines[1 + 7] == ' 1000/1000' } tmux.send_keys :Enter end def test_margin_reverse_list tmux.send_keys "yes | head -1000 | #{fzf '--margin 5,3 --layout=reverse-list'}", :Enter tmux.until { |lines| lines[4] == '' && lines[5] == ' > y' } tmux.send_keys :Enter end def test_tabstop writelines tempname, ["f\too\tba\tr\tbaz\tbarfooq\tux"] { 1 => '> f oo ba r baz barfooq ux', 2 => '> f oo ba r baz barfooq ux', 3 => '> f oo ba r baz barfooq ux', 4 => '> f oo ba r baz barfooq ux', 5 => '> f oo ba r baz barfooq ux', 6 => '> f oo ba r baz barfooq ux', 7 => '> f oo ba r baz barfooq ux', 8 => '> f oo ba r baz barfooq ux', 9 => '> f oo ba r baz barfooq ux' }.each do |ts, exp| tmux.prepare tmux.send_keys %(cat #{tempname} | fzf --tabstop=#{ts}), :Enter tmux.until(true) do |lines| exp.start_with? lines[-3].to_s.strip.sub(/\.\.$/, '') end tmux.send_keys :Enter end end def test_with_nth_basic writelines tempname, ['hello world ', 'byebye'] assert_equal( 'hello world ', `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp ) end def test_with_nth_ansi writelines tempname, ["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'] assert_equal( 'hello world ', `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi < #{tempname}`.chomp ) end def test_with_nth_no_ansi src = "\x1b[33mhello \x1b[34;1mworld\x1b[m " writelines tempname, [src, 'byebye'] assert_equal( src, `#{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi < #{tempname}`.chomp ) end def test_exit_0_exit_code `echo foo | #{FZF} -q bar -0` assert_equal 1, $CHILD_STATUS.exitstatus end def test_invalid_option lines = `#{FZF} --foobar 2>&1` assert_equal 2, $CHILD_STATUS.exitstatus assert lines.include?('unknown option: --foobar'), lines end def test_filter_exitstatus # filter / streaming filter ['', '--no-sort'].each do |opts| assert `echo foo | #{FZF} -f foo #{opts}`.include?('foo') assert_equal 0, $CHILD_STATUS.exitstatus assert `echo foo | #{FZF} -f bar #{opts}`.empty? assert_equal 1, $CHILD_STATUS.exitstatus end end def test_exitstatus_empty { '99' => '0', '999' => '1' }.each do |query, status| tmux.send_keys "seq 100 | #{FZF} -q #{query}; echo --\\$?--", :Enter tmux.until { |lines| lines[-2] =~ %r{ [10]/100} } tmux.send_keys :Enter tmux.until { |lines| lines.last.include? "--#{status}--" } end end def test_default_extended assert_equal '100', `seq 100 | #{FZF} -f "1 00$"`.chomp assert_equal '', `seq 100 | #{FZF} -f "1 00$" +x`.chomp end def test_exact assert_equal 4, `seq 123 | #{FZF} -f 13`.lines.length assert_equal 2, `seq 123 | #{FZF} -f 13 -e`.lines.length assert_equal 4, `seq 123 | #{FZF} -f 13 +e`.lines.length end def test_or_operator assert_equal %w[1 5 10], `seq 10 | #{FZF} -f "1 | 5"`.lines.map(&:chomp) assert_equal %w[1 10 2 3 4 5 6 7 8 9], `seq 10 | #{FZF} -f '1 | !1'`.lines.map(&:chomp) end def test_hscroll_off writelines tempname, ['=' * 10_000 + '0123456789'] [0, 3, 6].each do |off| tmux.prepare tmux.send_keys "#{FZF} --hscroll-off=#{off} -q 0 < #{tempname}", :Enter tmux.until { |lines| lines[-3].end_with?((0..off).to_a.join + '..') } tmux.send_keys '9' tmux.until { |lines| lines[-3].end_with? '789' } tmux.send_keys :Enter end end def test_partial_caching tmux.send_keys 'seq 1000 | fzf -e', :Enter tmux.until { |lines| lines[-2] == ' 1000/1000' } tmux.send_keys 11 tmux.until { |lines| lines[-2] == ' 19/1000' } tmux.send_keys 'C-a', "'" tmux.until { |lines| lines[-2] == ' 28/1000' } tmux.send_keys :Enter end def test_jump tmux.send_keys "seq 1000 | #{fzf "--multi --jump-labels 12345 --bind 'ctrl-j:jump'"}", :Enter tmux.until { |lines| lines[-2] == ' 1000/1000' } tmux.send_keys 'C-j' tmux.until { |lines| lines[-7] == '5 5' } tmux.until { |lines| lines[-8] == ' 6' } tmux.send_keys '5' tmux.until { |lines| lines[-7] == '> 5' } tmux.send_keys :Tab tmux.until { |lines| lines[-7] == ' >5' } tmux.send_keys 'C-j' tmux.until { |lines| lines[-7] == '5>5' } tmux.send_keys '2' tmux.until { |lines| lines[-4] == '> 2' } tmux.send_keys :Tab tmux.until { |lines| lines[-4] == ' >2' } tmux.send_keys 'C-j' tmux.until { |lines| lines[-7] == '5>5' } # Press any key other than jump labels to cancel jump tmux.send_keys '6' tmux.until { |lines| lines[-3] == '> 1' } tmux.send_keys :Tab tmux.until { |lines| lines[-3] == '>>1' } tmux.send_keys :Enter assert_equal %w[5 2 1], readonce.split($INPUT_RECORD_SEPARATOR) end def test_jump_accept tmux.send_keys "seq 1000 | #{fzf "--multi --jump-labels 12345 --bind 'ctrl-j:jump-accept'"}", :Enter tmux.until { |lines| lines[-2] == ' 1000/1000' } tmux.send_keys 'C-j' tmux.until { |lines| lines[-7] == '5 5' } tmux.send_keys '3' assert_equal '3', readonce.chomp end def test_preview tmux.send_keys %(seq 1000 | sed s/^2$// | #{FZF} -m --preview 'sleep 0.2; echo {{}-{+}}' --bind ?:toggle-preview), :Enter tmux.until { |lines| lines[1].include?(' {1-1}') } tmux.send_keys :Up tmux.until { |lines| lines[1].include?(' {-}') } tmux.send_keys '555' tmux.until { |lines| lines[1].include?(' {555-555}') } tmux.send_keys '?' tmux.until { |lines| !lines[1].include?(' {555-555}') } tmux.send_keys '?' tmux.until { |lines| lines[1].include?(' {555-555}') } tmux.send_keys :BSpace tmux.until { |lines| lines[-2].start_with? ' 28/1000' } tmux.send_keys 'foobar' tmux.until { |lines| !lines[1].include?('{') } tmux.send_keys 'C-u' tmux.until { |lines| lines.match_count == 1000 } tmux.until { |lines| lines[1].include?(' {1-1}') } tmux.send_keys :BTab tmux.until { |lines| lines[1].include?(' {-1}') } tmux.send_keys :BTab tmux.until { |lines| lines[1].include?(' {3-1 }') } tmux.send_keys :BTab tmux.until { |lines| lines[1].include?(' {4-1 3}') } tmux.send_keys :BTab tmux.until { |lines| lines[1].include?(' {5-1 3 4}') } end def test_preview_hidden tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-\\$FZF_PREVIEW_LINES-\\$FZF_PREVIEW_COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter tmux.until { |lines| lines[-1] == '>' } tmux.send_keys '?' tmux.until { |lines| lines[-2] =~ / {1-1-1-[0-9]+}/ } tmux.send_keys '555' tmux.until { |lines| lines[-2] =~ / {555-555-1-[0-9]+}/ } tmux.send_keys '?' tmux.until { |lines| lines[-1] == '> 555' } end def test_preview_size_0 begin File.unlink tempname rescue nil end tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0), :Enter tmux.until { |lines| lines.item_count == 100 && lines[1] == ' 100/100' && lines[2] == '> 1' } tmux.until { |_| %w[1] == File.readlines(tempname).map(&:chomp) } tmux.send_keys :Down tmux.until { |lines| lines[3] == '> 2' } tmux.until { |_| %w[1 2] == File.readlines(tempname).map(&:chomp) } tmux.send_keys :Down tmux.until { |lines| lines[4] == '> 3' } tmux.until { |_| %w[1 2 3] == File.readlines(tempname).map(&:chomp) } end def test_preview_flags tmux.send_keys %(seq 10 | sed 's/^/:: /; s/$/ /' | #{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}/{n}/{+n}}'), :Enter tmux.until { |lines| lines[1].include?('{1/1 /1/1 //0/0}') } tmux.send_keys '123' tmux.until { |lines| lines[1].include?('{////123//}') } tmux.send_keys 'C-u', '1' tmux.until { |lines| lines.match_count == 2 } tmux.until { |lines| lines[1].include?('{1/1 /1/1 /1/0/0}') } tmux.send_keys :BTab tmux.until { |lines| lines[1].include?('{10/10 /1/1 /1/9/0}') } tmux.send_keys :BTab tmux.until { |lines| lines[1].include?('{10/10 /1 10/1 10 /1/9/0 9}') } tmux.send_keys '2' tmux.until { |lines| lines[1].include?('{//1 10/1 10 /12//0 9}') } tmux.send_keys '3' tmux.until { |lines| lines[1].include?('{//1 10/1 10 /123//0 9}') } end def test_preview_file tmux.send_keys %[(echo foo bar; echo bar foo) | #{FZF} --multi --preview 'cat {+f} {+f2} {+nf} {+fn}' --print0], :Enter tmux.until { |lines| lines[1].include?('foo barbar00') } tmux.send_keys :BTab tmux.until { |lines| lines[1].include?('foo barbar00') } tmux.send_keys :BTab tmux.until { |lines| lines[1].include?('foo barbar foobarfoo0101') } end def test_preview_q_no_match tmux.send_keys %(: | #{FZF} --preview 'echo foo {q}'), :Enter tmux.until { |lines| lines.match_count == 0 } tmux.until { |lines| !lines[1].include?('foo') } tmux.send_keys 'bar' tmux.until { |lines| lines[1].include?('foo bar') } tmux.send_keys 'C-u' tmux.until { |lines| !lines[1].include?('foo') } end def test_preview_q_no_match_with_initial_query tmux.send_keys %(: | #{FZF} --preview 'echo foo {q}{q}' --query foo), :Enter tmux.until { |lines| lines.match_count == 0 } tmux.until { |lines| lines[1].include?('foofoo') } end def test_no_clear tmux.send_keys "seq 10 | fzf --no-clear --inline-info --height 5 > #{tempname}", :Enter prompt = '> < 10/10' tmux.until { |lines| lines[-1] == prompt } tmux.send_keys :Enter tmux.until { |_| %w[1] == File.readlines(tempname).map(&:chomp) } tmux.until { |lines| lines[-1] == prompt } end def test_info_hidden tmux.send_keys 'seq 10 | fzf --info=hidden', :Enter tmux.until { |lines| lines[-2] == '> 1' } end def test_change_top tmux.send_keys %(seq 1000 | #{FZF} --bind change:top), :Enter tmux.until { |lines| lines.match_count == 1000 } tmux.send_keys :Up tmux.until { |lines| lines[-4] == '> 2' } tmux.send_keys 1 tmux.until { |lines| lines[-3] == '> 1' } tmux.send_keys :Up tmux.until { |lines| lines[-4] == '> 10' } tmux.send_keys 1 tmux.until { |lines| lines[-3] == '> 11' } tmux.send_keys :Enter end def test_accept_non_empty tmux.send_keys %(seq 1000 | #{fzf '--print-query --bind enter:accept-non-empty'}), :Enter tmux.until { |lines| lines.match_count == 1000 } tmux.send_keys 'foo' tmux.until { |lines| lines[-2].include? '0/1000' } # fzf doesn't exit since there's no selection tmux.send_keys :Enter tmux.until { |lines| lines[-2].include? '0/1000' } tmux.send_keys 'C-u' tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.send_keys '999' tmux.until { |lines| lines[-2].include? '1/1000' } tmux.send_keys :Enter assert_equal %w[999 999], readonce.split($INPUT_RECORD_SEPARATOR) end def test_accept_non_empty_with_multi_selection tmux.send_keys %(seq 1000 | #{fzf '-m --print-query --bind enter:accept-non-empty'}), :Enter tmux.until { |lines| lines.match_count == 1000 } tmux.send_keys :Tab tmux.until { |lines| lines[-2].include? '1000/1000 (1)' } tmux.send_keys 'foo' tmux.until { |lines| lines[-2].include? '0/1000' } # fzf will exit in this case even though there's no match for the current query tmux.send_keys :Enter assert_equal %w[foo 1], readonce.split($INPUT_RECORD_SEPARATOR) end def test_accept_non_empty_with_empty_list tmux.send_keys %(: | #{fzf '-q foo --print-query --bind enter:accept-non-empty'}), :Enter tmux.until { |lines| lines[-2].strip == '0/0' } tmux.send_keys :Enter # fzf will exit anyway since input list is empty assert_equal %w[foo], readonce.split($INPUT_RECORD_SEPARATOR) end def test_preview_update_on_select tmux.send_keys(%(seq 10 | fzf -m --preview 'echo {+}' --bind a:toggle-all), :Enter) tmux.until { |lines| lines.item_count == 10 } tmux.send_keys 'a' tmux.until { |lines| lines.any? { |line| line.include? '1 2 3 4 5' } } tmux.send_keys 'a' tmux.until { |lines| lines.none? { |line| line.include? '1 2 3 4 5' } } end def test_escaped_meta_characters input = <<~EOF foo^bar foo$bar foo!bar foo'bar foo bar bar foo EOF writelines tempname, input.lines.map(&:chomp) assert_equal input.lines.count, `#{FZF} -f'foo bar' < #{tempname}`.lines.count assert_equal input.lines.count - 1, `#{FZF} -f'^foo bar$' < #{tempname}`.lines.count assert_equal ['foo bar'], `#{FZF} -f'foo\\ bar' < #{tempname}`.lines.map(&:chomp) assert_equal ['foo bar'], `#{FZF} -f'^foo\\ bar$' < #{tempname}`.lines.map(&:chomp) assert_equal input.lines.count - 1, `#{FZF} -f'!^foo\\ bar$' < #{tempname}`.lines.count end def test_inverse_only_search_should_not_sort_the_result # Filter assert_equal(%w[aaaaa b ccc], `printf '%s\n' aaaaa b ccc BAD | #{FZF} -f '!bad'`.lines.map(&:chomp)) # Interactive tmux.send_keys(%[printf '%s\n' aaaaa b ccc BAD | #{FZF} -q '!bad'], :Enter) tmux.until { |lines| lines.item_count == 4 && lines.match_count == 3 } tmux.until { |lines| lines[-3] == '> aaaaa' } tmux.until { |lines| lines[-4] == ' b' } tmux.until { |lines| lines[-5] == ' ccc' } end def test_preview_correct_tab_width_after_ansi_reset_code writelines tempname, ["\x1b[31m+\x1b[m\t\x1b[32mgreen"] tmux.send_keys "#{FZF} --preview 'cat #{tempname}'", :Enter tmux.until { |lines| lines[1].include?('+ green') } end def test_phony tmux.send_keys %(seq 1000 | #{FZF} --query 333 --phony --preview 'echo {} {q}'), :Enter tmux.until { |lines| lines.match_count == 1000 } tmux.until { |lines| lines[1].include?('1 333') } tmux.send_keys 'foo' tmux.until { |lines| lines.match_count == 1000 } tmux.until { |lines| lines[1].include?('1 333foo') } end def test_reload tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq {q}),a:reload(seq 100),b:reload:seq 200' --header-lines 2 --multi 2), :Enter tmux.until { |lines| lines.match_count == 998 } tmux.send_keys 'a' tmux.until { |lines| lines.item_count == 98 && lines.match_count == 98 } tmux.send_keys 'b' tmux.until { |lines| lines.item_count == 198 && lines.match_count == 198 } tmux.send_keys :Tab tmux.until { |lines| lines[-2].include?('(1/2)') } tmux.send_keys '555' tmux.until { |lines| lines.item_count == 553 && lines.match_count == 1 } tmux.until { |lines| !lines[-2].include?('(1/2)') } end def test_reload_even_when_theres_no_match tmux.send_keys %(: | #{FZF} --bind 'space:reload(seq 10)'), :Enter tmux.until { |lines| lines.item_count.zero? } tmux.send_keys :Space tmux.until { |lines| lines.item_count == 10 } end def test_clear_list_when_header_lines_changed_due_to_reload tmux.send_keys %(seq 10 | #{FZF} --header 0 --header-lines 3 --bind 'space:reload(seq 1)'), :Enter tmux.until { |lines| lines.any? { |line| line.include?('9') } } tmux.send_keys :Space tmux.until { |lines| lines.none? { |line| line.include?('9') } } end def test_clear_query tmux.send_keys %(: | #{FZF} --query foo --bind space:clear-query), :Enter tmux.until { |lines| lines.item_count.zero? } tmux.until { |lines| lines.last.include?('> foo') } tmux.send_keys 'C-a', 'bar' tmux.until { |lines| lines.last.include?('> barfoo') } tmux.send_keys :Space tmux.until { |lines| lines.last == '>' } end def test_clear_selection tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter tmux.until { |lines| lines.match_count == 100 } tmux.send_keys :Tab tmux.until { |lines| lines[-2].include?('(1)') } tmux.send_keys 'foo' tmux.until { |lines| lines.match_count.zero? } tmux.until { |lines| lines[-2].include?('(1)') } tmux.send_keys :Space tmux.until { |lines| lines.match_count.zero? } tmux.until { |lines| !lines[-2].include?('(1)') } end end module TestShell def setup super end def teardown @tmux.kill end def set_var(name, val) tmux.prepare tmux.send_keys "export #{name}='#{val}'", :Enter tmux.prepare end def unset_var(name) tmux.prepare tmux.send_keys "unset #{name}", :Enter tmux.prepare end def test_ctrl_t set_var 'FZF_CTRL_T_COMMAND', 'seq 100' retries do tmux.prepare tmux.send_keys 'C-t' tmux.until { |lines| lines.item_count == 100 } end tmux.send_keys :Tab, :Tab, :Tab tmux.until { |lines| lines.any_include? ' (3)' } tmux.send_keys :Enter tmux.until { |lines| lines.any_include? '1 2 3' } tmux.send_keys 'C-c' end def test_ctrl_t_unicode writelines tempname, ['fzf-unicode 테스트1', 'fzf-unicode 테스트2'] set_var 'FZF_CTRL_T_COMMAND', "cat #{tempname}" retries do tmux.prepare tmux.send_keys 'echo ', 'C-t' tmux.until { |lines| lines.item_count == 2 } end tmux.send_keys 'fzf-unicode' tmux.until { |lines| lines.match_count == 2 } tmux.send_keys '1' tmux.until { |lines| lines.match_count == 1 } tmux.send_keys :Tab tmux.until { |lines| lines.select_count == 1 } tmux.send_keys :BSpace tmux.until { |lines| lines.match_count == 2 } tmux.send_keys '2' tmux.until { |lines| lines.match_count == 1 } tmux.send_keys :Tab tmux.until { |lines| lines.select_count == 2 } tmux.send_keys :Enter tmux.until { |lines| lines.any_include?(/echo.*fzf-unicode.*1.*fzf-unicode.*2/) } tmux.send_keys :Enter tmux.until { |lines| lines.any_include?(/^fzf-unicode.*1.*fzf-unicode.*2/) } end def test_alt_c lines = retries do tmux.prepare tmux.send_keys :Escape, :c tmux.until { |lines| lines.match_count.positive? } end expected = lines.reverse.select { |l| l.start_with? '>' }.first[2..-1] tmux.send_keys :Enter tmux.prepare tmux.send_keys :pwd, :Enter tmux.until { |lines| lines[-1].end_with?(expected) } end def test_alt_c_command set_var 'FZF_ALT_C_COMMAND', 'echo /tmp' tmux.prepare tmux.send_keys 'cd /', :Enter retries do tmux.prepare tmux.send_keys :Escape, :c tmux.until { |lines| lines.item_count == 1 } end tmux.send_keys :Enter tmux.prepare tmux.send_keys :pwd, :Enter tmux.until { |lines| lines[-1].end_with? '/tmp' } end def test_ctrl_r tmux.prepare tmux.send_keys 'echo 1st', :Enter; tmux.prepare tmux.send_keys 'echo 2nd', :Enter; tmux.prepare tmux.send_keys 'echo 3d', :Enter; tmux.prepare tmux.send_keys 'echo 3rd', :Enter; tmux.prepare tmux.send_keys 'echo 4th', :Enter retries do tmux.prepare tmux.send_keys 'C-r' tmux.until { |lines| lines.match_count.positive? } end tmux.send_keys 'C-r' tmux.send_keys '3d' tmux.until { |lines| lines[-3].end_with? 'echo 3rd' } tmux.send_keys :Enter tmux.until { |lines| lines[-1] == 'echo 3rd' } tmux.send_keys :Enter tmux.until { |lines| lines[-1] == '3rd' } end def retries(times = 3) (times - 1).times do begin return yield rescue RuntimeError end end yield end end module CompletionTest def test_file_completion FileUtils.mkdir_p '/tmp/fzf-test' FileUtils.mkdir_p '/tmp/fzf test' (1..100).each { |i| FileUtils.touch "/tmp/fzf-test/#{i}" } ['no~such~user', '/tmp/fzf test/foobar', '~/.fzf-home'].each do |f| FileUtils.touch File.expand_path(f) end tmux.prepare tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab tmux.until { |lines| lines.match_count.positive? } tmux.send_keys ' !d' tmux.until { |lines| lines.match_count == 2 } tmux.send_keys :Tab, :Tab tmux.until { |lines| lines.select_count == 2 } tmux.send_keys :Enter tmux.until(true) do |lines| lines[-1].include?('/tmp/fzf-test/10') && lines[-1].include?('/tmp/fzf-test/100') end # ~USERNAME** tmux.send_keys 'C-u' tmux.send_keys "cat ~#{ENV['USER']}**", :Tab tmux.until { |lines| lines.match_count.positive? } tmux.send_keys "'.fzf-home" tmux.until { |lines| lines.select { |l| l.include? '.fzf-home' }.count > 1 } tmux.send_keys :Enter tmux.until(true) do |lines| lines[-1].end_with?('.fzf-home') end # ~INVALID_USERNAME** tmux.send_keys 'C-u' tmux.send_keys 'cat ~such**', :Tab tmux.until(true) { |lines| lines.any_include? 'no~such~user' } tmux.send_keys :Enter tmux.until(true) { |lines| lines[-1].end_with?('no~such~user') } # /tmp/fzf\ test** tmux.send_keys 'C-u' tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab tmux.until { |lines| lines.match_count.positive? } tmux.send_keys 'foobar$' tmux.until { |lines| lines.match_count == 1 } tmux.send_keys :Enter tmux.until(true) { |lines| lines[-1].end_with?('/tmp/fzf\ test/foobar') } # Should include hidden files (1..100).each { |i| FileUtils.touch "/tmp/fzf-test/.hidden-#{i}" } tmux.send_keys 'C-u' tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab tmux.until(true) { |lines| lines.match_count == 100 && lines.any_include?('/tmp/fzf-test/.hidden-') } tmux.send_keys :Enter ensure ['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f| FileUtils.rm_rf File.expand_path(f) end end def test_file_completion_root tmux.send_keys 'ls /**', :Tab tmux.until { |lines| lines.match_count.positive? } tmux.send_keys :Enter end def test_dir_completion (1..100).each do |idx| FileUtils.mkdir_p "/tmp/fzf-test/d#{idx}" end FileUtils.touch '/tmp/fzf-test/d55/xxx' tmux.prepare tmux.send_keys 'cd /tmp/fzf-test/**', :Tab tmux.until { |lines| lines.match_count.positive? } tmux.send_keys :Tab, :Tab # Tab does not work here tmux.send_keys 55 tmux.until { |lines| lines.match_count == 1 } tmux.send_keys :Enter tmux.until(true) { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/' } tmux.send_keys :xx tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' } # Should not match regular files (bash-only) if self.class == TestBash tmux.send_keys :Tab tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' } end # Fail back to plusdirs tmux.send_keys :BSpace, :BSpace, :BSpace tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55' } tmux.send_keys :Tab tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/' } end def test_process_completion tmux.send_keys 'sleep 12345 &', :Enter lines = tmux.until { |lines| lines[-1].start_with? '[1]' } pid = lines[-1].split.last tmux.prepare tmux.send_keys 'C-L' tmux.send_keys 'kill ', :Tab tmux.until { |lines| lines.match_count.positive? } tmux.send_keys 'sleep12345' tmux.until { |lines| lines.any_include? 'sleep 12345' } tmux.send_keys :Enter tmux.until(true) { |lines| lines[-1].include? "kill #{pid}" } ensure if pid begin Process.kill 'KILL', pid.to_i rescue nil end end end def test_custom_completion tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter tmux.prepare tmux.send_keys 'ls /tmp/**', :Tab tmux.until { |lines| lines.match_count == 11 } tmux.send_keys :Tab, :Tab, :Tab tmux.until { |lines| lines.select_count == 3 } tmux.send_keys :Enter tmux.until(true) { |lines| lines[-1] == 'ls /tmp 1 2' } end def test_unset_completion tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter tmux.prepare # Using tmux tmux.send_keys 'unset FZFFOOBR**', :Tab tmux.until { |lines| lines.match_count == 1 } tmux.send_keys :Enter tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' } tmux.send_keys 'C-c' # FZF_TMUX=1 new_shell tmux.send_keys 'unset FZFFOOBR**', :Tab, pane: 0 tmux.until(false, 1) { |lines| lines.match_count == 1 } tmux.send_keys :Enter tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' } end def test_file_completion_unicode FileUtils.mkdir_p '/tmp/fzf-test' tmux.paste 'cd /tmp/fzf-test; echo -n test3 > "fzf-unicode 테스트1"; echo -n test4 > "fzf-unicode 테스트2"' tmux.prepare tmux.send_keys 'cat fzf-unicode**', :Tab tmux.until { |lines| lines.match_count == 2 } tmux.send_keys '1' tmux.until { |lines| lines.match_count == 1 } tmux.send_keys :Tab tmux.until { |lines| lines.select_count == 1 } tmux.send_keys :BSpace tmux.until { |lines| lines.match_count == 2 } tmux.send_keys '2' tmux.until { |lines| lines.select_count == 1 } tmux.send_keys :Tab tmux.until { |lines| lines.select_count == 2 } tmux.send_keys :Enter tmux.until(true) { |lines| lines.any_include? 'cat' } tmux.send_keys :Enter tmux.until { |lines| lines[-1].include? 'test3test4' } end end class TestBash < TestBase include TestShell include CompletionTest def new_shell tmux.prepare tmux.send_keys "FZF_TMUX=1 #{Shell.bash}", :Enter tmux.prepare end def setup super @tmux = Tmux.new :bash end def test_dynamic_completion_loader tmux.paste 'touch /tmp/foo; _fzf_completion_loader=1' tmux.paste '_completion_loader() { complete -o default fake; }' tmux.paste 'complete -F _fzf_path_completion -o default -o bashdefault fake' tmux.send_keys 'fake /tmp/foo**', :Tab tmux.until { |lines| lines.match_count.positive? } tmux.send_keys 'C-c' tmux.prepare tmux.send_keys 'fake /tmp/foo' tmux.send_keys :Tab , 'C-u' tmux.prepare tmux.send_keys 'fake /tmp/foo**', :Tab tmux.until { |lines| lines.match_count.positive? } end end class TestZsh < TestBase include TestShell include CompletionTest def new_shell tmux.send_keys "FZF_TMUX=1 #{Shell.zsh}", :Enter tmux.prepare end def setup super @tmux = Tmux.new :zsh end end class TestFish < TestBase include TestShell def new_shell tmux.send_keys 'env FZF_TMUX=1 fish', :Enter tmux.send_keys 'function fish_prompt; end; clear', :Enter tmux.until(&:empty?) end def set_var(name, val) tmux.prepare tmux.send_keys "set -g #{name} '#{val}'", :Enter tmux.prepare end def setup super @tmux = Tmux.new :fish end end fzf-0.20.0/uninstall000077500000000000000000000047421357617647500143510ustar00rootroot00000000000000#!/usr/bin/env bash xdg=0 prefix='~/.fzf' prefix_expand=~/.fzf fish_dir=${XDG_CONFIG_HOME:-$HOME/.config}/fish help() { cat << EOF usage: $0 [OPTIONS] --help Show this message --xdg Remove files generated under \$XDG_CONFIG_HOME/fzf EOF } for opt in "$@"; do case $opt in --help) help exit 0 ;; --xdg) xdg=1 prefix='"${XDG_CONFIG_HOME:-$HOME/.config}"/fzf/fzf' prefix_expand=${XDG_CONFIG_HOME:-$HOME/.config}/fzf/fzf ;; *) echo "unknown option: $opt" help exit 1 ;; esac done ask() { while true; do read -p "$1 ([y]/n) " -r REPLY=${REPLY:-"y"} if [[ $REPLY =~ ^[Yy]$ ]]; then return 0 elif [[ $REPLY =~ ^[Nn]$ ]]; then return 1 fi done } remove() { echo "Remove $1" rm -f "$1" } remove_line() { src=$(readlink "$1") if [ $? -eq 0 ]; then echo "Remove from $1 ($src):" else src=$1 echo "Remove from $1:" fi shift line_no=1 match=0 while [ -n "$1" ]; do line=$(sed -n "$line_no,\$p" "$src" | \grep -m1 -nF "$1") if [ $? -ne 0 ]; then shift line_no=1 continue fi line_no=$(( $(sed 's/:.*//' <<< "$line") + line_no - 1 )) content=$(sed 's/^[0-9]*://' <<< "$line") match=1 echo " - Line #$line_no: $content" [ "$content" = "$1" ] || ask " - Remove?" if [ $? -eq 0 ]; then awk -v n=$line_no 'NR == n {next} {print}' "$src" > "$src.bak" && mv "$src.bak" "$src" || break echo " - Removed" else echo " - Skipped" line_no=$(( line_no + 1 )) fi done [ $match -eq 0 ] && echo " - Nothing found" echo } for shell in bash zsh; do shell_config=${prefix_expand}.${shell} remove "${shell_config}" remove_line ~/.${shell}rc \ "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" \ "source ${prefix}.${shell}" done bind_file="${fish_dir}/functions/fish_user_key_bindings.fish" if [ -f "$bind_file" ]; then remove_line "$bind_file" "fzf_key_bindings" fi if [ -d "${fish_dir}/functions" ]; then remove "${fish_dir}/functions/fzf.fish" remove "${fish_dir}/functions/fzf_key_bindings.fish" if [ "$(ls -A "${fish_dir}/functions")" ]; then echo "Can't delete non-empty directory: \"${fish_dir}/functions\"" else rmdir "${fish_dir}/functions" fi fi config_dir=$(dirname "$prefix_expand") if [[ "$xdg" = 1 ]] && [[ "$config_dir" = */fzf ]] && [[ -d "$config_dir" ]]; then rmdir "$config_dir" fi