Compare commits

..

51 Commits

Author SHA1 Message Date
Junegunn Choi
a221c672fb 0.15.6 2016-11-09 01:45:27 +09:00
Junegunn Choi
f87d382ec8 Fix --color=bw on tcell build 2016-11-09 01:45:06 +09:00
Junegunn Choi
3dfc020fac Merge pull request #730 from laur89/master
Minor README markup fix
2016-11-09 00:06:42 +09:00
Laur Aliste
2d87896939 Minor README markup fix. 2016-11-08 15:41:46 +01:00
Junegunn Choi
2192d8d816 GOOS=windows make release 2016-11-08 03:32:41 +09:00
Junegunn Choi
d206949f62 Wait for additional keys after ESC for up to 100ms
Close #661
2016-11-08 03:07:26 +09:00
Junegunn Choi
4accc69022 Fix flaky test cases 2016-11-08 02:19:05 +09:00
Junegunn Choi
898d8d94c8 Fix issues in tcell renderer and Windows build
- Fix display of CJK wide characters
- Fix horizontal offset of header lines
- Add support for keys with ALT modifier, shift-tab, page-up and down
- Fix util.ExecCommand to properly parse command-line arguments
- Fix redraw on resize
- Implement Pause/Resume for execute action
- Remove runtime check of GOOS
- Change exit status to 2 when tcell failed to start
- TBD: Travis CI build for tcell renderer
    - Pending. tcell cannot reliably ingest keys from tmux send-keys
2016-11-08 02:06:34 +09:00
Michael Kelley
26895da969 Implement tcell-based renderer 2016-11-07 02:32:14 +09:00
Junegunn Choi
0c573b3dff Prepare for termbox/windows build
`TAGS=termbox make` (or `go build -tags termbox`)
2016-11-07 02:32:14 +09:00
Junegunn Choi
2cff00dce2 man fzf in README
Close #726
2016-11-01 00:39:02 +09:00
Junegunn Choi
06a6ad8bca Update ANSI processor to ignore ^N and ^O
This reverts commit 02c6ad0e59.
2016-10-30 12:29:29 +09:00
Junegunn Choi
02c6ad0e59 Strip ^N and ^O from preview output
https://github.com/junegunn/fzf/issues/391#issuecomment-257090266

e.g. fzf --preview 'printf "$(tput setaf 2)foo$(tput sgr0)bar\nbar\n"'
2016-10-30 11:43:06 +09:00
Junegunn Choi
9f321cbe13 Fix header lines being cleared on toggle-preview
Close #722
2016-10-28 03:13:50 +09:00
Junegunn Choi
9f30ca2923 0.15.5 2016-10-23 22:00:32 +09:00
Junegunn Choi
37f2d8f795 [vim] Respect g:fzf_colors
Close #711
2016-10-22 01:14:16 +09:00
Junegunn Choi
400e443a0a Make test cases less susceptible to timeout errors 2016-10-22 00:01:21 +09:00
Junegunn Choi
0a8d2996dc Set foreground color without affecting background
Close #712
2016-10-21 19:35:59 +09:00
Junegunn Choi
cfdb00b971 Allow other options to follow --color without spec 2016-10-21 19:20:16 +09:00
Junegunn Choi
9b9ad39143 [vim] Set g:loaded_fzf 2016-10-18 15:00:47 +09:00
Junegunn Choi
0541c0dbcf Use relative position instead of absolute distance for --tiebreak=end
Fix unintuitive result where `*fzf*/install` is ranked higher than
`fzf/src/fzf/*fzf*-linux_386` on --tiebreak=end.
2016-10-18 01:13:57 +09:00
Junegunn Choi
47b11cb8b4 Merge pull request #701 from nthapaliya/zsh_script_improvements
[zsh] GNU coreutils compatibility
2016-10-14 10:00:58 +09:00
Niraj Thapaliya
d3da310b92 Use command to ignore shell function 2016-10-13 09:53:24 -06:00
Niraj Thapaliya
93e0a6a9de Gnu [ evaluates both sides of a -o condition regardless
It doesn't short circuit like we expect, causing trouble when $dir is
empty

Use shell builtin instead
2016-10-13 09:52:49 -06:00
Junegunn Choi
ac549a853a [fzf-tmux] Fix bash condition
Fix #702
2016-10-13 10:42:26 +09:00
Junegunn Choi
053af9a1c8 [fzf-tmux/vim/nvim] Do not split small window
Close #699
2016-10-12 23:10:21 +09:00
Junegunn Choi
60112def02 Merge pull request #698 from Ambrevar/master
[fish] Yank commandline in fzf-history-widget
2016-10-12 01:54:51 +09:00
Pierre Neidhardt
2134c0c8a9 key-bindings.fish: Yank commandline in fzf-history-widget 2016-10-11 21:15:00 +05:30
Junegunn Choi
3222d62ddf 0.15.4 2016-10-04 02:17:36 +09:00
Junegunn Choi
aeb957a285 Use exact match by default for inverse search term
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.

| Token    | Match type                 | Description                       |
| -------- | -------------------------- | --------------------------------- |
| `sbtrkt` | fuzzy-match                | Items that match `sbtrkt`         |
| `^music` | prefix-exact-match         | Items that start with `music`     |
| `.mp3$`  | suffix-exact-match         | Items that end with `.mp3`        |
| `'wild`  | exact-match (quoted)       | Items that include `wild`         |
| `!fire`  | inverse-exact-match        | Items that do not include `fire`  |
| `!.mp3$` | inverse-suffix-exact-match | Items that do not end with `.mp3` |
2016-10-04 02:09:03 +09:00
Junegunn Choi
154cf22ffa Display scroll indicator in preview window 2016-10-04 01:40:45 +09:00
Junegunn Choi
51f532697e Adjust maximum scroll offset
It was possible that a few lines at the bottom may not be visible if
there are lines above that span multiple lines.
2016-10-04 01:39:48 +09:00
Junegunn Choi
01b88539ba [vim] Apply --multi and --prompt to :FZF command 2016-10-04 00:30:04 +09:00
Junegunn Choi
3066b206af Support field index expressions in preview and execute action
Also close #679. The placeholder for the current query is {q}.
2016-10-03 14:33:28 +09:00
Junegunn Choi
04492bab10 Use unicode.IsSpace to cover more whitespace characters 2016-09-29 22:40:22 +09:00
Junegunn Choi
8b0d0342d4 0.15.3 2016-09-29 03:05:20 +09:00
Junegunn Choi
957c12e7d7 Fix SEGV when trying to render preview but the window is closed
Close #677
2016-09-29 02:53:05 +09:00
Junegunn Choi
3b5ae0f8a2 Fix failing unit tests on ANSI attributes 2016-09-29 01:06:47 +09:00
Junegunn Choi
1fc5659842 Add support for more ANSI color attributes (#674)
Dim, underline, blink, reverse
2016-09-29 00:54:27 +09:00
Junegunn Choi
1acd2adce2 Update man page: missing actions 2016-09-26 15:33:46 +09:00
Junegunn Choi
1bc223d4b3 0.15.2 2016-09-25 22:20:43 +09:00
Junegunn Choi
bef405bfa5 Ignore VT100-related escape codes 2016-09-25 19:03:08 +09:00
Junegunn Choi
0612074abe Support high intensity colors
Close #671
2016-09-25 18:11:35 +09:00
Junegunn Choi
3bf51d8362 Merge pull request #670 from maverickwoo/fix-668
[bash-completion] Fix #668
2016-09-25 05:15:24 +09:00
Maverick Woo
2c8479a7c5 Fix #668
Handle uppercase letters in program names. This also deals with `-` and
`.`, both of which are quite common in program names, e.g., `xdg-open`
and `foo.sh`.
2016-09-24 15:39:13 -04:00
Junegunn Choi
8c8b5b313e Add preview-page-up and preview-page-down actions 2016-09-25 04:12:44 +09:00
Junegunn Choi
66d55fd893 Make preview windows scrollable
Close #669

You can use your mouse or binadble preview-up and preview-down actions
to scroll the content of the preview window.

    fzf --preview 'highlight -O ansi {}' --bind alt-j:preview-down,alt-k:preview-up
2016-09-25 02:02:00 +09:00
Junegunn Choi
7fa5e6c861 0.15.1 2016-09-21 01:28:24 +09:00
Junegunn Choi
00f96aae76 Avoid rendering delay when displaying extremely long lines
Related #666
2016-09-21 01:23:41 +09:00
Junegunn Choi
a749e6bd16 Fix temp directory in a test case 2016-09-21 01:15:35 +09:00
Junegunn Choi
791076d366 Fix panic when pattern occurs after 2^15-th column
Fix #666
2016-09-21 01:15:06 +09:00
42 changed files with 1970 additions and 831 deletions

View File

@@ -1,6 +1,10 @@
language: ruby
rvm:
- 2.2.0
matrix:
include:
- env: TAGS=
rvm: 2.2.0
# - env: TAGS=tcell
# rvm: 2.2.0
install:
- sudo apt-get update

View File

@@ -1,6 +1,55 @@
CHANGELOG
=========
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

View File

@@ -82,6 +82,15 @@ method used.
- brew: `brew update; brew reinstall fzf`
- vim-plug: `:PlugUpdate fzf`
### Windows
Pre-built binaries for Windows can be downloaded [here][bin]. However, other
components of the project may not work on Windows. You might want to consider
installing fzf on [Windows Subsystem for Linux][wsl] where everything runs
flawlessly.
[wsl]: https://blogs.msdn.microsoft.com/wsl/
Usage
-----
@@ -102,7 +111,7 @@ vim $(fzf)
#### Using the finder
- `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P)` to move cursor up and down
- `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
@@ -113,16 +122,16 @@ vim $(fzf)
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
!rmx`
!fire`
| Token | Match type | Description |
| -------- | -------------------- | -------------------------------- |
| `sbtrkt` | fuzzy-match | Items that match `sbtrkt` |
| `^music` | prefix-exact-match | Items that start with `music` |
| `.mp3$` | suffix-exact-match | Items that end with `.mp3` |
| `'wild` | exact-match (quoted) | Items that include `wild` |
| `!rmx` | inverse-fuzzy-match | Items that do not match `rmx` |
| `!'fire` | inverse-exact-match | Items that do not include `fire` |
| Token | Match type | Description |
| -------- | -------------------------- | --------------------------------- |
| `sbtrkt` | fuzzy-match | Items that match `sbtrkt` |
| `^music` | prefix-exact-match | Items that start with `music` |
| `.mp3$` | suffix-exact-match | Items that end with `.mp3` |
| `'wild` | exact-match (quoted) | Items that include `wild` |
| `!fire` | inverse-exact-match | Items that do not include `fire` |
| `!.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,
@@ -145,6 +154,10 @@ or `py`.
- Default options
- e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"`
#### Options
See the man page (`man fzf`) for the full list of options.
Examples
--------
@@ -305,7 +318,7 @@ If you have set up fzf for Vim, `:FZF` command will be added.
:FZF ~
" With options
:FZF --no-sort -m /tmp
:FZF --no-sort --reverse --inline-info /tmp
" Bang version starts in fullscreen instead of using tmux pane or Neovim split
:FZF!
@@ -319,7 +332,7 @@ Note that the environment variables `FZF_DEFAULT_COMMAND` and
`FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][fzf-config] for
customization.
[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim)
[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-Vim-plugin
#### `fzf#run`
@@ -347,7 +360,8 @@ page](https://github.com/junegunn/fzf/wiki/Examples-(vim)).
`fzf#wrap([name string,] [opts dict,] [fullscreen boolean])` is a helper
function that decorates the options dictionary so that it understands
`g:fzf_layout`, `g:fzf_action`, and `g:fzf_history_dir` like `:FZF`.
`g:fzf_layout`, `g:fzf_action`, `g:fzf_colors`, and `g:fzf_history_dir` like
`:FZF`.
```vim
command! -bang MyStuff

View File

@@ -17,6 +17,7 @@ swap=""
close=""
term=""
[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines)
[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols)
help() {
>&2 echo 'usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
@@ -83,7 +84,7 @@ while [[ $# -gt 0 ]]; do
else
if [[ -n "$swap" ]]; then
if [[ "$arg" =~ ^.l ]]; then
[[ -n "$COLUMNS" ]] && max=$COLUMNS || max=$(tput cols)
max=$columns
else
max=$lines
fi
@@ -108,7 +109,7 @@ while [[ $# -gt 0 ]]; do
[[ -n "$skip" ]] && args+=("$arg")
done
if [[ -z "$TMUX" ]] || [[ "$lines" -le 15 ]]; then
if [[ -z "$TMUX" || "$opt" =~ ^-h && "$columns" -le 40 || ! "$opt" =~ ^-h && "$lines" -le 15 ]]; then
"$fzf" "${args[@]}"
exit $?
fi

View File

@@ -2,8 +2,8 @@
set -u
[[ "$@" =~ --pre ]] && version=0.15.0 pre=1 ||
version=0.15.0 pre=0
[[ "$@" =~ --pre ]] && version=0.15.6 pre=1 ||
version=0.15.6 pre=0
auto_completion=
key_bindings=

View File

@@ -21,7 +21,7 @@ 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 "Sep 2016" "fzf 0.15.0" "fzf-tmux - open fzf in tmux split pane"
.TH fzf-tmux 1 "Nov 2016" "fzf 0.15.6" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
fzf-tmux - open fzf in tmux split pane

View File

@@ -21,7 +21,7 @@ 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 "Sep 2016" "fzf 0.15.0" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Nov 2016" "fzf 0.15.6" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -232,11 +232,17 @@ automatically truncated when the number of the lines exceeds the value.
.TP
.BI "--preview=" "COMMAND"
Execute the given command for the current line and display the result on the
preview window. \fB{}\fR is the placeholder for the quoted string of the
current line.
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). Also, \fB{q}\fR is replaced to the current
query string.
.RS
e.g. \fBfzf --preview="head -$LINES {}"\fR
\fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
Note that you can escape a placeholder pattern by prepending a backslash.
.RE
.TP
.BI "--preview-window=" "[POSITION][:SIZE[%]][:hidden]"
@@ -358,7 +364,7 @@ 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.
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
@@ -434,6 +440,10 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR
\fBpreview-down\fR
\fBpreview-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)
\fBselect-all\fR
@@ -456,9 +466,11 @@ binding \fBenter\fR key to \fBless\fR command like follows.
\fBfzf --bind "enter:execute(less {})"\fR
\fB{}\fR is the placeholder for the quoted string of the current line.
If the command contains parentheses, you can use any of the following
alternative notations to avoid parse errors.
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
@@ -477,7 +489,7 @@ alternative notations to avoid parse errors.
.RS
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.
comma-separated list of key-action pairs.
.RE
\fBexecute-multi(...)\fR is an alternative action that executes the command

View File

@@ -21,6 +21,11 @@
" 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:default_layout = { 'down': '~40%' }
let s:layout_keys = ['window', 'up', 'down', 'left', 'right']
let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf'
@@ -154,6 +159,22 @@ function! s:common_sink(action, lines) abort
endtry
endfunction
function! s:get_color(attr, ...)
for group in a:000
let code = synIDattr(synIDtrans(hlID(group)), a:attr, 'cterm')
if code =~ '^[0-9]\+$'
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) ? '' : ('--color='.colors)
endfunction
" [name string,] [opts dict,] [fullscreen boolean]
function! fzf#wrap(...)
let args = ['', {}, 0]
@@ -185,8 +206,10 @@ function! fzf#wrap(...)
endif
endif
" Colors: g:fzf_colors
let opts.options = s:defaults() .' '. get(opts, 'options', '')
" History: g:fzf_history_dir
let opts.options = get(opts, 'options', '')
if len(name) && len(get(g:, 'fzf_history_dir', ''))
let dir = expand(g:fzf_history_dir)
if !isdirectory(dir)
@@ -289,7 +312,8 @@ function! s:fzf_tmux(dict)
endfunction
function! s:splittable(dict)
return s:present(a:dict, 'up', 'down', 'left', 'right')
return s:present(a:dict, 'up', 'down') && &lines > 15 ||
\ s:present(a:dict, 'left', 'right') && &columns > 40
endfunction
function! s:pushd(dict)
@@ -405,24 +429,25 @@ function! s:split(dict)
\ 'right': ['vertical botright', 'vertical resize', &columns] }
let ppos = s:getpos()
try
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
if s:present(a:dict, 'window')
execute a:dict.window
else
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
@@ -558,11 +583,15 @@ let s:default_action = {
function! s:cmd(bang, ...) abort
let args = copy(a:000)
let opts = {}
let opts = { 'options': '--multi ' }
if len(args) && isdirectory(expand(args[-1]))
let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g')
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '/*$', '/', '')
let opts.options .= ' --prompt '.shellescape(opts.dir)
else
let opts.options .= ' --prompt '.shellescape(pathshorten(getcwd()).'/')
endif
call fzf#run(fzf#wrap('FZF', extend({'options': join(args)}, opts), a:bang))
let opts.options .= ' '.join(args)
call fzf#run(fzf#wrap('FZF', opts, a:bang))
endfunction
command! -nargs=* -complete=dir -bang FZF call s:cmd(<bang>0, <f-args>)

View File

@@ -32,7 +32,7 @@ fi
_fzf_orig_completion_filter() {
sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' |
awk -F= '{gsub(/[^a-z0-9_= ;]/, "_", $1); print $1"="$2}'
awk -F= '{gsub(/[^A-Za-z0-9_= ;]/, "_", $1); print $1"="$2}'
}
_fzf_opts_completion() {
@@ -117,7 +117,7 @@ _fzf_handle_dynamic_completion() {
__fzf_generic_path_completion() {
local cur base dir leftover matches trigger cmd fzf
[ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
cmd=$(echo "${COMP_WORDS[0]}" | sed 's/[^a-z0-9_=]/_/g')
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}"
@@ -162,7 +162,7 @@ _fzf_complete() {
type -t "$post" > /dev/null 2>&1 || post=cat
[ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
cmd=$(echo "${COMP_WORDS[0]}" | sed 's/[^a-z0-9_=]/_/g')
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]]; then
@@ -277,7 +277,7 @@ _fzf_defc() {
cmd="$1"
func="$2"
opts="$3"
orig_var="_fzf_orig_completion_$cmd"
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
orig="${!orig_var}"
if [ -n "$orig" ]; then
printf -v def "$orig" "$func"

View File

@@ -44,7 +44,7 @@ __fzf_generic_path_completion() {
setopt localoptions nonomatch
dir="$base"
while [ 1 ]; do
if [ -z "$dir" -o -d ${~dir} ]; then
if [[ -z "$dir" || -d ${~dir} ]]; then
leftover=${base/#"$dir"}
leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.'
@@ -111,7 +111,7 @@ _fzf_complete_telnet() {
_fzf_complete_ssh() {
_fzf_complete '+m' "$@" < <(
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \
command cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \
<(command grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | 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

View File

@@ -26,8 +26,8 @@ function fzf_key_bindings
end
function fzf-history-widget
history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS > $TMPDIR/fzf.result
and commandline (cat $TMPDIR/fzf.result)
history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q '(commandline)' > $TMPDIR/fzf.result
and commandline -- (cat $TMPDIR/fzf.result)
commandline -f repaint
rm -f $TMPDIR/fzf.result
end

View File

@@ -33,17 +33,24 @@ endif
all: fzf/$(BINARY)
ifeq ($(GOOS),windows)
release: fzf/$(BINARY32) fzf/$(BINARY64)
-cd fzf && cp $(BINARY32) $(RELEASE32).exe && zip $(RELEASE32).zip $(RELEASE32).exe
cd fzf && cp $(BINARY64) $(RELEASE64).exe && zip $(RELEASE64).zip $(RELEASE64).exe && \
rm -f $(RELEASE32).exe $(RELEASE64).exe
else
release: test fzf/$(BINARY32) fzf/$(BINARY64)
-cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32)
cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \
rm -f $(RELEASE32) $(RELEASE64)
endif
$(SRCDIR):
mkdir -p $(shell dirname $(SRCDIR))
ln -s $(ROOTDIR) $(SRCDIR)
deps: $(SRCDIR) $(SOURCES)
cd $(SRCDIR) && go get
cd $(SRCDIR) && go get -tags "$(TAGS)"
android-build: $(SRCDIR)
cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get
@@ -52,7 +59,7 @@ android-build: $(SRCDIR)
rm -f $(RELEASEARM7)
test: deps
SHELL=/bin/sh go test -v ./...
SHELL=/bin/sh GOOS=$(GOOS) go test -v -tags "$(TAGS)" ./...
install: $(BINDIR)/fzf

View File

@@ -83,9 +83,11 @@ Third-party libraries used
- [ncurses][ncurses]
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
- Licensed under [MIT](http://mattn.mit-license.org/2013)
- 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/2014)
- 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)
License
-------

View File

@@ -257,13 +257,14 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern []
}
// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
offset := 0
offset16 := 0
offset32 := 0
// Bonus point for each position
offset, B := alloc16(offset, slab, N, false)
offset16, B := alloc16(offset16, slab, N, false)
// The first occurrence of each character in the pattern
offset, F := alloc16(offset, slab, M, false)
offset32, F := alloc32(offset32, slab, M, false)
// Rune array
_, T := alloc32(0, slab, N, false)
offset32, T := alloc32(offset32, slab, N, false)
// Phase 1. Check if there's a match and calculate bonus for each point
pidx, lastIdx, prevClass := 0, 0, charNonWord
@@ -291,7 +292,7 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern []
if pidx < M {
if char == pattern[pidx] {
lastIdx = idx
F[pidx] = int16(idx)
F[pidx] = int32(idx)
pidx++
}
} else {
@@ -307,10 +308,10 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern []
// Phase 2. Fill in score matrix (H)
// Unlike the original algorithm, we do not allow omission.
width := lastIdx - int(F[0]) + 1
offset, H := alloc16(offset, slab, width*M, false)
offset16, H := alloc16(offset16, slab, width*M, false)
// Possible length of consecutive chunk at each position.
offset, C := alloc16(offset, slab, width*M, false)
offset16, C := alloc16(offset16, slab, width*M, false)
maxScore, maxScorePos := int16(0), 0
for i := 0; i < M; i++ {

View File

@@ -1,6 +1,7 @@
package algo
import (
"math"
"sort"
"strings"
"testing"
@@ -154,3 +155,12 @@ func TestEmptyPattern(t *testing.T) {
assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6, 0)
}
}
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)
}

View File

@@ -6,6 +6,8 @@ import (
"strconv"
"strings"
"unicode/utf8"
"github.com/junegunn/fzf/src/tui"
)
type ansiOffset struct {
@@ -14,26 +16,26 @@ type ansiOffset struct {
}
type ansiState struct {
fg int
bg int
bold bool
fg tui.Color
bg tui.Color
attr tui.Attr
}
func (s *ansiState) colored() bool {
return s.fg != -1 || s.bg != -1 || s.bold
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.bold == t.bold
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr
}
var ansiRegex *regexp.Regexp
func init() {
ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]")
ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*.|[\x0e\x0f]")
}
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
@@ -94,11 +96,11 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
// State
var state *ansiState
if prevState == nil {
state = &ansiState{-1, -1, false}
state = &ansiState{-1, -1, 0}
} else {
state = &ansiState{prevState.fg, prevState.bg, prevState.bold}
state = &ansiState{prevState.fg, prevState.bg, prevState.attr}
}
if ansiCode[len(ansiCode)-1] == 'K' {
if ansiCode[0] != '\x1b' || ansiCode[len(ansiCode)-1] != 'm' {
return state
}
@@ -108,7 +110,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
init := func() {
state.fg = -1
state.bg = -1
state.bold = false
state.attr = 0
state256 = 0
}
@@ -132,14 +134,26 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
case 49:
state.bg = -1
case 1:
state.bold = true
state.attr = tui.Bold
case 2:
state.attr = tui.Dim
case 4:
state.attr = tui.Underline
case 5:
state.attr = tui.Blink
case 7:
state.attr = tui.Reverse
case 0:
init()
default:
if num >= 30 && num <= 37 {
state.fg = num - 30
state.fg = tui.Color(num - 30)
} else if num >= 40 && num <= 47 {
state.bg = num - 40
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:
@@ -150,7 +164,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
state256 = 0
}
case 2:
*ptr = num
*ptr = tui.Color(num)
state256 = 0
}
}

View File

@@ -3,13 +3,19 @@ package fzf
import (
"fmt"
"testing"
"github.com/junegunn/fzf/src/tui"
)
func TestExtractColor(t *testing.T) {
assert := func(offset ansiOffset, b int32, e int32, fg int, bg int, bold bool) {
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.bold != bold {
t.Error(offset, b, e, fg, bg, bold)
offset.color.fg != fg || offset.color.bg != bg || offset.color.attr != attr {
t.Error(offset, b, e, fg, bg, attr)
}
}
@@ -121,7 +127,7 @@ func TestExtractColor(t *testing.T) {
if len(*offsets) != 1 {
t.Fail()
}
if state.fg != 2 || state.bg != -1 || !state.bold {
if state.fg != 2 || state.bg != -1 || state.attr == 0 {
t.Fail()
}
assert((*offsets)[0], 6, 11, 2, -1, true)
@@ -132,7 +138,7 @@ func TestExtractColor(t *testing.T) {
if len(*offsets) != 1 {
t.Fail()
}
if state.fg != 2 || state.bg != -1 || !state.bold {
if state.fg != 2 || state.bg != -1 || state.attr == 0 {
t.Fail()
}
assert((*offsets)[0], 0, 11, 2, -1, true)
@@ -143,7 +149,7 @@ func TestExtractColor(t *testing.T) {
if len(*offsets) != 2 {
t.Fail()
}
if state.fg != 200 || state.bg != 100 || state.bold {
if state.fg != 200 || state.bg != 100 || state.attr > 0 {
t.Fail()
}
assert((*offsets)[0], 0, 6, 2, -1, true)

View File

@@ -8,14 +8,13 @@ import (
const (
// Current version
version = "0.15.0"
version = "0.15.6"
// Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond
coordinatorDelayStep time.Duration = 10 * time.Millisecond
// Reader
defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
readerBufferSize = 64 * 1024
// Terminal

8
src/constants_unix.go Normal file
View File

@@ -0,0 +1,8 @@
// +build !windows
package fzf
const (
// Reader
defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
)

8
src/constants_windows.go Normal file
View File

@@ -0,0 +1,8 @@
// +build windows
package fzf
const (
// Reader
defaultCommand = `dir /s/b`
)

View File

@@ -1,7 +1,10 @@
package fzf
import (
"io/ioutil"
"os"
"os/user"
"runtime"
"testing"
)
@@ -10,23 +13,34 @@ func TestHistory(t *testing.T) {
// Invalid arguments
user, _ := user.Current()
paths := []string{"/etc", "/proc"}
if user.Name != "root" {
paths = append(paths, "/etc/sudoers")
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("/tmp/fzf-history", maxHistory)
h, _ := NewHistory(f.Name(), maxHistory)
for i := 0; i < maxHistory+10; i++ {
h.append("foobar")
}
}
{ // Read lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory)
h, _ := NewHistory(f.Name(), maxHistory)
if len(h.lines) != maxHistory+1 {
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
}
@@ -37,13 +51,13 @@ func TestHistory(t *testing.T) {
}
}
{ // Append lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory)
h, _ := NewHistory(f.Name(), maxHistory)
h.append("barfoo")
h.append("")
h.append("foobarbaz")
}
{ // Read lines again
h, _ := NewHistory("/tmp/fzf-history", maxHistory)
h, _ := NewHistory(f.Name(), maxHistory)
if len(h.lines) != maxHistory+1 {
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
}

View File

@@ -9,7 +9,7 @@ import (
"unicode/utf8"
"github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/go-shellwords"
)
@@ -142,7 +142,7 @@ type Options struct {
Multi bool
Ansi bool
Mouse bool
Theme *curses.ColorTheme
Theme *tui.ColorTheme
Black bool
Reverse bool
Cycle bool
@@ -187,7 +187,7 @@ func defaultOptions() *Options {
Multi: false,
Ansi: false,
Mouse: true,
Theme: curses.EmptyTheme(),
Theme: tui.EmptyTheme(),
Black: false,
Reverse: false,
Cycle: false,
@@ -246,7 +246,7 @@ func nextString(args []string, i *int, message string) string {
}
func optionalNextString(args []string, i *int) string {
if len(args) > *i+1 {
if len(args) > *i+1 && !strings.HasPrefix(args[*i+1], "-") {
*i++
return args[*i]
}
@@ -358,60 +358,60 @@ func parseKeyChords(str string, message string) map[int]string {
chord := 0
switch lkey {
case "up":
chord = curses.Up
chord = tui.Up
case "down":
chord = curses.Down
chord = tui.Down
case "left":
chord = curses.Left
chord = tui.Left
case "right":
chord = curses.Right
chord = tui.Right
case "enter", "return":
chord = curses.CtrlM
chord = tui.CtrlM
case "space":
chord = curses.AltZ + int(' ')
chord = tui.AltZ + int(' ')
case "bspace", "bs":
chord = curses.BSpace
chord = tui.BSpace
case "alt-enter", "alt-return":
chord = curses.AltEnter
chord = tui.AltEnter
case "alt-space":
chord = curses.AltSpace
chord = tui.AltSpace
case "alt-/":
chord = curses.AltSlash
chord = tui.AltSlash
case "alt-bs", "alt-bspace":
chord = curses.AltBS
chord = tui.AltBS
case "tab":
chord = curses.Tab
chord = tui.Tab
case "btab", "shift-tab":
chord = curses.BTab
chord = tui.BTab
case "esc":
chord = curses.ESC
chord = tui.ESC
case "del":
chord = curses.Del
chord = tui.Del
case "home":
chord = curses.Home
chord = tui.Home
case "end":
chord = curses.End
chord = tui.End
case "pgup", "page-up":
chord = curses.PgUp
chord = tui.PgUp
case "pgdn", "page-down":
chord = curses.PgDn
chord = tui.PgDn
case "shift-left":
chord = curses.SLeft
chord = tui.SLeft
case "shift-right":
chord = curses.SRight
chord = tui.SRight
case "double-click":
chord = curses.DoubleClick
chord = tui.DoubleClick
case "f10":
chord = curses.F10
chord = tui.F10
default:
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chord = curses.CtrlA + int(lkey[5]) - 'a'
chord = tui.CtrlA + int(lkey[5]) - 'a'
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
chord = curses.AltA + int(lkey[4]) - 'a'
chord = tui.AltA + int(lkey[4]) - 'a'
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' {
chord = curses.F1 + int(key[1]) - '1'
chord = tui.F1 + int(key[1]) - '1'
} else if utf8.RuneCountInString(key) == 1 {
chord = curses.AltZ + int([]rune(key)[0])
chord = tui.AltZ + int([]rune(key)[0])
} else {
errorExit("unsupported key: " + key)
}
@@ -458,7 +458,7 @@ func parseTiebreak(str string) []criterion {
return criteria
}
func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme {
if theme != nil {
dupe := *theme
return &dupe
@@ -466,16 +466,16 @@ func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
return nil
}
func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme {
func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
theme := dupeTheme(defaultTheme)
for _, str := range strings.Split(strings.ToLower(str), ",") {
switch str {
case "dark":
theme = dupeTheme(curses.Dark256)
theme = dupeTheme(tui.Dark256)
case "light":
theme = dupeTheme(curses.Light256)
theme = dupeTheme(tui.Light256)
case "16":
theme = dupeTheme(curses.Default16)
theme = dupeTheme(tui.Default16)
case "bw", "no":
theme = nil
default:
@@ -495,14 +495,12 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme
if err != nil || ansi32 < -1 || ansi32 > 255 {
fail()
}
ansi := int16(ansi32)
ansi := tui.Color(ansi32)
switch pair[0] {
case "fg":
theme.Fg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "bg":
theme.Bg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "fg+":
theme.Current = ansi
case "bg+":
@@ -574,9 +572,9 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
}
var key int
if len(pair[0]) == 1 && pair[0][0] == escapedColon {
key = ':' + curses.AltZ
key = ':' + tui.AltZ
} else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
key = ',' + curses.AltZ
key = ',' + tui.AltZ
} else {
keys := parseKeyChords(pair[0], "key name required")
key = firstKey(keys)
@@ -663,6 +661,14 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
keymap[key] = actTogglePreview
case "toggle-sort":
keymap[key] = actToggleSort
case "preview-up":
keymap[key] = actPreviewUp
case "preview-down":
keymap[key] = actPreviewDown
case "preview-page-up":
keymap[key] = actPreviewPageUp
case "preview-page-down":
keymap[key] = actPreviewPageDown
default:
if isExecuteAction(actLower) {
var offset int
@@ -862,7 +868,7 @@ func parseOptions(opts *Options, allArgs []string) {
case "--color":
spec := optionalNextString(allArgs, &i)
if len(spec) == 0 {
opts.Theme = curses.EmptyTheme()
opts.Theme = tui.EmptyTheme()
} else {
opts.Theme = parseTheme(opts.Theme, spec)
}
@@ -899,7 +905,7 @@ func parseOptions(opts *Options, allArgs []string) {
case "+c", "--no-color":
opts.Theme = nil
case "+2", "--no-256":
opts.Theme = curses.Default16
opts.Theme = tui.Default16
case "--black":
opts.Black = true
case "--no-black":
@@ -1065,11 +1071,11 @@ func parseOptions(opts *Options, allArgs []string) {
func postProcessOptions(opts *Options) {
// Default actions for CTRL-N / CTRL-P when --history is set
if opts.History != nil {
if _, prs := opts.Keymap[curses.CtrlP]; !prs {
opts.Keymap[curses.CtrlP] = actPreviousHistory
if _, prs := opts.Keymap[tui.CtrlP]; !prs {
opts.Keymap[tui.CtrlP] = actPreviousHistory
}
if _, prs := opts.Keymap[curses.CtrlN]; !prs {
opts.Keymap[curses.CtrlN] = actNextHistory
if _, prs := opts.Keymap[tui.CtrlN]; !prs {
opts.Keymap[tui.CtrlN] = actNextHistory
}
}

View File

@@ -2,9 +2,10 @@ package fzf
import (
"fmt"
"io/ioutil"
"testing"
"github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
)
@@ -133,48 +134,48 @@ func TestParseKeys(t *testing.T) {
if len(pairs) != 11 {
t.Error(11)
}
check(curses.CtrlZ, "ctrl-z")
check(curses.AltZ, "alt-z")
check(curses.F2, "f2")
check(curses.AltZ+'@', "@")
check(curses.AltA, "Alt-a")
check(curses.AltZ+'!', "!")
check(curses.CtrlA+'g'-'a', "ctrl-G")
check(curses.AltZ+'J', "J")
check(curses.AltZ+'g', "g")
check(curses.AltEnter, "ALT-enter")
check(curses.AltSpace, "alt-SPACE")
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.AltEnter, "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(curses.CtrlM, "Return")
check(curses.AltZ+' ', "space")
check(curses.Tab, "tab")
check(curses.BTab, "btab")
check(curses.ESC, "esc")
check(curses.Up, "up")
check(curses.Down, "down")
check(curses.Left, "left")
check(curses.Right, "right")
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(curses.Tab, "Ctrl-I")
check(curses.PgUp, "page-up")
check(curses.PgDn, "Page-Down")
check(curses.Home, "Home")
check(curses.End, "End")
check(curses.AltBS, "Alt-BSpace")
check(curses.SLeft, "shift-left")
check(curses.SRight, "shift-right")
check(curses.BTab, "shift-tab")
check(curses.CtrlM, "Enter")
check(curses.BSpace, "bspace")
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) {
@@ -191,36 +192,36 @@ func TestParseKeysWithComma(t *testing.T) {
pairs := parseKeyChords(",", "")
checkN(len(pairs), 1)
check(pairs, curses.AltZ+',', ",")
check(pairs, tui.AltZ+',', ",")
pairs = parseKeyChords(",,a,b", "")
checkN(len(pairs), 3)
check(pairs, curses.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b")
check(pairs, curses.AltZ+',', ",")
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, curses.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b")
check(pairs, curses.AltZ+',', ",")
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, curses.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b")
check(pairs, curses.AltZ+',', ",")
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, curses.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b")
check(pairs, curses.AltZ+'c', "c")
check(pairs, curses.AltZ+',', ",")
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, curses.AltZ+',', ",")
check(pairs, tui.AltZ+',', ",")
}
func TestBind(t *testing.T) {
@@ -236,41 +237,41 @@ func TestBind(t *testing.T) {
}
keymap := defaultKeymap()
execmap := make(map[int]string)
check(actBeginningOfLine, keymap[curses.CtrlA])
check(actBeginningOfLine, keymap[tui.CtrlA])
parseKeymap(keymap, execmap,
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+
"f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+
",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)")
check(actKillLine, keymap[curses.CtrlA])
check(actToggleSort, keymap[curses.CtrlB])
check(actPageUp, keymap[curses.AltZ+'c'])
check(actAbort, keymap[curses.AltZ+','])
check(actAccept, keymap[curses.AltZ+':'])
check(actPageDown, keymap[curses.AltZ])
check(actExecute, keymap[curses.F1])
check(actExecute, keymap[curses.F2])
check(actExecute, keymap[curses.F3])
check(actExecute, keymap[curses.F4])
checkString("ls {}", execmap[curses.F1])
checkString("echo {}, {}, {}", execmap[curses.F2])
checkString("echo '({})'", execmap[curses.F3])
checkString("less {}", execmap[curses.F4])
checkString("echo (,),[,],/,:,;,%,{}", execmap[curses.AltA])
checkString("echo (,),[,],/,:,@,%,{}", execmap[curses.AltB])
checkString("\nfoobar,Y:execute(baz)", execmap[curses.AltZ+'X'])
check(actKillLine, keymap[tui.CtrlA])
check(actToggleSort, keymap[tui.CtrlB])
check(actPageUp, keymap[tui.AltZ+'c'])
check(actAbort, keymap[tui.AltZ+','])
check(actAccept, keymap[tui.AltZ+':'])
check(actPageDown, keymap[tui.AltZ])
check(actExecute, keymap[tui.F1])
check(actExecute, keymap[tui.F2])
check(actExecute, keymap[tui.F3])
check(actExecute, keymap[tui.F4])
checkString("ls {}", execmap[tui.F1])
checkString("echo {}, {}, {}", execmap[tui.F2])
checkString("echo '({})'", execmap[tui.F3])
checkString("less {}", execmap[tui.F4])
checkString("echo (,),[,],/,:,;,%,{}", execmap[tui.AltA])
checkString("echo (,),[,],/,:,@,%,{}", execmap[tui.AltB])
checkString("\nfoobar,Y:execute(baz)", execmap[tui.AltZ+'X'])
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
checkString("foobar", execmap[curses.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])])
checkString("foobar", execmap[tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])])
}
parseKeymap(keymap, execmap, "f1:abort")
check(actAbort, keymap[curses.F1])
check(actAbort, keymap[tui.F1])
}
func TestColorSpec(t *testing.T) {
theme := curses.Dark256
theme := tui.Dark256
dark := parseTheme(theme, "dark")
if *dark != *theme {
t.Errorf("colors should be equivalent")
@@ -283,7 +284,7 @@ func TestColorSpec(t *testing.T) {
if *light == *theme {
t.Errorf("should not be equivalent")
}
if *light != *curses.Light256 {
if *light != *tui.Light256 {
t.Errorf("colors should be equivalent")
}
if light == theme {
@@ -294,29 +295,23 @@ func TestColorSpec(t *testing.T) {
if customized.Fg != 231 || customized.Bg != 232 {
t.Errorf("color not customized")
}
if *curses.Dark256 == *customized {
if *tui.Dark256 == *customized {
t.Errorf("colors should not be equivalent")
}
customized.Fg = curses.Dark256.Fg
customized.Bg = curses.Dark256.Bg
if *curses.Dark256 == *customized {
t.Errorf("colors should now 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 != curses.Dark256.Fg || customized.Bg == curses.Dark256.Bg {
if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
t.Errorf("color not customized")
}
if customized.UseDefault {
t.Errorf("not using default colors")
}
if !curses.Dark256.UseDefault {
t.Errorf("using default colors")
}
}
func TestParseNilTheme(t *testing.T) {
var theme *curses.ColorTheme
var theme *tui.ColorTheme
newTheme := parseTheme(theme, "prompt:12")
if newTheme != nil {
t.Errorf("color is disabled. keep it that way.")
@@ -336,21 +331,23 @@ func TestDefaultCtrlNP(t *testing.T) {
t.Error()
}
}
check([]string{}, curses.CtrlN, actDown)
check([]string{}, curses.CtrlP, actUp)
check([]string{}, tui.CtrlN, actDown)
check([]string{}, tui.CtrlP, actUp)
check([]string{"--bind=ctrl-n:accept"}, curses.CtrlN, actAccept)
check([]string{"--bind=ctrl-p:accept"}, curses.CtrlP, actAccept)
check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
hist := "--history=/tmp/foo"
check([]string{hist}, curses.CtrlN, actNextHistory)
check([]string{hist}, curses.CtrlP, actPreviousHistory)
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"}, curses.CtrlN, actAccept)
check([]string{hist, "--bind=ctrl-n:accept"}, curses.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"}, curses.CtrlN, actNextHistory)
check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlP, actAccept)
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 {

View File

@@ -163,12 +163,13 @@ func parseTerms(fuzzy bool, caseMode Case, str string) []termSet {
if strings.HasPrefix(text, "!") {
inv = true
typ = termExact
text = text[1:]
}
if strings.HasPrefix(text, "'") {
// Flip exactness
if fuzzy {
if fuzzy && !inv {
typ = termExact
text = text[1:]
} else {

View File

@@ -22,15 +22,15 @@ func TestParseTermsExtended(t *testing.T) {
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 != termFuzzy || !terms[4][0].inv ||
terms[5][0].typ != termExact || !terms[5][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 != termFuzzy || !terms[8][3].inv {
terms[8][3].typ != termExact || !terms[8][3].inv {
t.Errorf("%s", terms)
}
for idx, termSet := range terms[:8] {

View File

@@ -39,9 +39,15 @@ func (r *Reader) feed(src io.Reader) {
// 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 len(bytea) > 0 {
if err == nil {
bytea = bytea[:len(bytea)-1]
// 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) {
r.eventBox.Set(EvtReadNew, nil)

View File

@@ -3,8 +3,9 @@ package fzf
import (
"math"
"sort"
"unicode"
"github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
)
@@ -13,8 +14,8 @@ type Offset [2]int32
type colorOffset struct {
offset [2]int32
color int
bold bool
color tui.ColorPair
attr tui.Attr
index int32
}
@@ -56,21 +57,21 @@ func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result {
case byLength:
// If offsets is empty, trimLen will be 0, but we don't care
val = util.AsUint16(trimLen)
case byBegin:
case byBegin, byEnd:
if validOffsetFound {
whitePrefixLen := 0
for idx := 0; idx < numChars; idx++ {
r := item.text.Get(idx)
whitePrefixLen = idx
if idx == minBegin || r != ' ' && r != '\t' {
if idx == minBegin || !unicode.IsSpace(r) {
break
}
}
val = util.AsUint16(minBegin - whitePrefixLen)
}
case byEnd:
if validOffsetFound {
val = util.AsUint16(1 + numChars - maxEnd)
if criterion == byBegin {
val = util.AsUint16(minBegin - whitePrefixLen)
} else {
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/trimLen)
}
}
}
result.rank.points[idx] = val
@@ -91,14 +92,14 @@ func minRank() rank {
return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
}
func (result *Result) colorOffsets(matchOffsets []Offset, color int, bold bool, current bool) []colorOffset {
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, bold: bold})
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, attr: attr})
}
return offsets
}
@@ -142,29 +143,29 @@ func (result *Result) colorOffsets(matchOffsets []Offset, color int, bold bool,
if curr != 0 && idx > start {
if curr == -1 {
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, color: color, bold: bold})
offset: [2]int32{int32(start), int32(idx)}, color: color, attr: attr})
} else {
ansi := itemColors[curr-1]
fg := ansi.color.fg
if fg == -1 {
if current {
fg = curses.CurrentFG
fg = theme.Current
} else {
fg = curses.FG
fg = theme.Fg
}
}
bg := ansi.color.bg
if bg == -1 {
if current {
bg = curses.DarkBG
bg = theme.DarkBg
} else {
bg = curses.BG
bg = theme.Bg
}
}
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)},
color: curses.PairFor(fg, bg),
bold: ansi.color.bold || bold})
color: tui.PairFor(fg, bg),
attr: ansi.color.attr.Merge(attr)})
}
}
}

View File

@@ -1,3 +1,5 @@
// +build !tcell
package fzf
import (
@@ -5,7 +7,7 @@ import (
"sort"
"testing"
"github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
)
@@ -97,23 +99,27 @@ func TestColorOffset(t *testing.T) {
item := Result{
item: &Item{
colors: &[]ansiOffset{
ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}},
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, true}},
ansiOffset{[2]int32{30, 32}, ansiState{3, 7, false}},
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}}}
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}]
colors := item.colorOffsets(offsets, 99, false, true)
assert := func(idx int, b int32, e int32, c int, bold bool) {
colors := item.colorOffsets(offsets, tui.Dark256, 99, 0, 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.bold != bold {
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.attr != attr {
t.Error(o)
}
}
assert(0, 0, 5, curses.ColUser, false)
assert(0, 0, 5, tui.ColUser, false)
assert(1, 5, 15, 99, false)
assert(2, 15, 20, curses.ColUser, false)
assert(3, 22, 25, curses.ColUser+1, true)
assert(2, 15, 20, tui.ColUser, false)
assert(3, 22, 25, tui.ColUser+1, true)
assert(4, 25, 35, 99, false)
assert(5, 35, 40, curses.ColUser+2, true)
assert(5, 35, 40, tui.ColUser+2, true)
}

View File

@@ -7,12 +7,13 @@ import (
"os/signal"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"syscall"
"time"
C "github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
"github.com/junegunn/go-runewidth"
@@ -20,6 +21,12 @@ import (
// import "github.com/pkg/profile"
var placeholder *regexp.Regexp
func init() {
placeholder = regexp.MustCompile("\\\\?(?:{[0-9,-.]*}|{q})")
}
type jumpMode int
const (
@@ -28,6 +35,13 @@ const (
jumpAcceptEnabled
)
type previewer struct {
text string
lines int
offset int
enabled bool
}
// Terminal represents terminal input/output
type Terminal struct {
initDelay time.Duration
@@ -44,6 +58,7 @@ type Terminal struct {
multi bool
sort bool
toggleSort bool
delimiter Delimiter
expect map[int]string
keymap map[int]actionType
execmap map[int]string
@@ -55,9 +70,9 @@ type Terminal struct {
header0 []string
ansi bool
margin [4]sizeSpec
window *C.Window
bwindow *C.Window
pwindow *C.Window
window *tui.Window
bwindow *tui.Window
pwindow *tui.Window
count int
progress int
reading bool
@@ -68,8 +83,7 @@ type Terminal struct {
selected map[int32]selectedItem
reqBox *util.EventBox
preview previewOpts
previewing bool
previewTxt string
previewer previewer
previewBox *util.EventBox
eventBox *util.EventBox
mutex sync.Mutex
@@ -77,20 +91,16 @@ type Terminal struct {
suppress bool
startChan chan bool
slab *util.Slab
theme *tui.ColorTheme
}
type selectedItem struct {
at time.Time
text string
item *Item
}
type byTimeOrder []selectedItem
type previewRequest struct {
ok bool
str string
}
func (a byTimeOrder) Len() int {
return len(a)
}
@@ -119,6 +129,7 @@ const (
reqPrintQuery
reqPreviewEnqueue
reqPreviewDisplay
reqPreviewRefresh
reqQuit
)
@@ -165,6 +176,10 @@ const (
actPrintQuery
actToggleSort
actTogglePreview
actPreviewUp
actPreviewDown
actPreviewPageUp
actPreviewPageDown
actPreviousHistory
actNextHistory
actExecute
@@ -173,51 +188,51 @@ const (
func defaultKeymap() map[int]actionType {
keymap := make(map[int]actionType)
keymap[C.Invalid] = actInvalid
keymap[C.CtrlA] = actBeginningOfLine
keymap[C.CtrlB] = actBackwardChar
keymap[C.CtrlC] = actAbort
keymap[C.CtrlG] = actAbort
keymap[C.CtrlQ] = actAbort
keymap[C.ESC] = actAbort
keymap[C.CtrlD] = actDeleteCharEOF
keymap[C.CtrlE] = actEndOfLine
keymap[C.CtrlF] = actForwardChar
keymap[C.CtrlH] = actBackwardDeleteChar
keymap[C.BSpace] = actBackwardDeleteChar
keymap[C.Tab] = actToggleDown
keymap[C.BTab] = actToggleUp
keymap[C.CtrlJ] = actDown
keymap[C.CtrlK] = actUp
keymap[C.CtrlL] = actClearScreen
keymap[C.CtrlM] = actAccept
keymap[C.CtrlN] = actDown
keymap[C.CtrlP] = actUp
keymap[C.CtrlU] = actUnixLineDiscard
keymap[C.CtrlW] = actUnixWordRubout
keymap[C.CtrlY] = actYank
keymap[tui.Invalid] = actInvalid
keymap[tui.CtrlA] = actBeginningOfLine
keymap[tui.CtrlB] = actBackwardChar
keymap[tui.CtrlC] = actAbort
keymap[tui.CtrlG] = actAbort
keymap[tui.CtrlQ] = actAbort
keymap[tui.ESC] = actAbort
keymap[tui.CtrlD] = actDeleteCharEOF
keymap[tui.CtrlE] = actEndOfLine
keymap[tui.CtrlF] = actForwardChar
keymap[tui.CtrlH] = actBackwardDeleteChar
keymap[tui.BSpace] = actBackwardDeleteChar
keymap[tui.Tab] = actToggleDown
keymap[tui.BTab] = actToggleUp
keymap[tui.CtrlJ] = actDown
keymap[tui.CtrlK] = actUp
keymap[tui.CtrlL] = actClearScreen
keymap[tui.CtrlM] = actAccept
keymap[tui.CtrlN] = actDown
keymap[tui.CtrlP] = actUp
keymap[tui.CtrlU] = actUnixLineDiscard
keymap[tui.CtrlW] = actUnixWordRubout
keymap[tui.CtrlY] = actYank
keymap[C.AltB] = actBackwardWord
keymap[C.SLeft] = actBackwardWord
keymap[C.AltF] = actForwardWord
keymap[C.SRight] = actForwardWord
keymap[C.AltD] = actKillWord
keymap[C.AltBS] = actBackwardKillWord
keymap[tui.AltB] = actBackwardWord
keymap[tui.SLeft] = actBackwardWord
keymap[tui.AltF] = actForwardWord
keymap[tui.SRight] = actForwardWord
keymap[tui.AltD] = actKillWord
keymap[tui.AltBS] = actBackwardKillWord
keymap[C.Up] = actUp
keymap[C.Down] = actDown
keymap[C.Left] = actBackwardChar
keymap[C.Right] = actForwardChar
keymap[tui.Up] = actUp
keymap[tui.Down] = actDown
keymap[tui.Left] = actBackwardChar
keymap[tui.Right] = actForwardChar
keymap[C.Home] = actBeginningOfLine
keymap[C.End] = actEndOfLine
keymap[C.Del] = actDeleteChar
keymap[C.PgUp] = actPageUp
keymap[C.PgDn] = actPageDown
keymap[tui.Home] = actBeginningOfLine
keymap[tui.End] = actEndOfLine
keymap[tui.Del] = actDeleteChar
keymap[tui.PgUp] = actPageUp
keymap[tui.PgDn] = actPageDown
keymap[C.Rune] = actRune
keymap[C.Mouse] = actMouse
keymap[C.DoubleClick] = actAccept
keymap[tui.Rune] = actRune
keymap[tui.Mouse] = actMouse
keymap[tui.DoubleClick] = actAccept
return keymap
}
@@ -256,6 +271,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
multi: opts.Multi,
sort: opts.Sort > 0,
toggleSort: opts.ToggleSort,
delimiter: opts.Delimiter,
expect: opts.Expect,
keymap: opts.Keymap,
execmap: opts.Execmap,
@@ -275,16 +291,16 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(),
preview: opts.Preview,
previewing: previewBox != nil && !opts.Preview.hidden,
previewTxt: "",
previewer: previewer{"", 0, 0, previewBox != nil && !opts.Preview.hidden},
previewBox: previewBox,
eventBox: eventBox,
mutex: sync.Mutex{},
suppress: true,
slab: util.MakeSlab(slab16Size, slab32Size),
theme: opts.Theme,
startChan: make(chan bool, 1),
initFunc: func() {
C.Init(opts.Theme, opts.Black, opts.Mouse)
tui.Init(opts.Theme, opts.Black, opts.Mouse)
}}
}
@@ -363,7 +379,7 @@ func (t *Terminal) output() bool {
}
} else {
for _, sel := range t.sortSelected() {
t.printer(sel.text)
t.printer(sel.item.AsString(t.ansi))
}
}
return found
@@ -401,6 +417,8 @@ func displayWidth(runes []rune) int {
const (
minWidth = 16
minHeight = 4
maxDisplayWidthCalc = 1024
)
func calculateSize(base int, size sizeSpec, margin int, minSize int) int {
@@ -412,8 +430,8 @@ func calculateSize(base int, size sizeSpec, margin int, minSize int) int {
}
func (t *Terminal) resizeWindows() {
screenWidth := C.MaxX()
screenHeight := C.MaxY()
screenWidth := tui.MaxX()
screenHeight := tui.MaxY()
marginInt := [4]int{}
for idx, sizeSpec := range t.margin {
if sizeSpec.percent {
@@ -462,33 +480,33 @@ func (t *Terminal) resizeWindows() {
height := screenHeight - marginInt[0] - marginInt[2]
if t.isPreviewEnabled() {
createPreviewWindow := func(y int, x int, w int, h int) {
t.bwindow = C.NewWindow(y, x, w, h, true)
t.pwindow = C.NewWindow(y+1, x+2, w-4, h-2, false)
t.bwindow = tui.NewWindow(y, x, w, h, true)
t.pwindow = tui.NewWindow(y+1, x+2, w-4, h-2, false)
}
switch t.preview.position {
case posUp:
pheight := calculateSize(height, t.preview.size, minHeight, 3)
t.window = C.NewWindow(
t.window = tui.NewWindow(
marginInt[0]+pheight, marginInt[3], width, height-pheight, false)
createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
case posDown:
pheight := calculateSize(height, t.preview.size, minHeight, 3)
t.window = C.NewWindow(
t.window = tui.NewWindow(
marginInt[0], marginInt[3], width, height-pheight, false)
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
case posLeft:
pwidth := calculateSize(width, t.preview.size, minWidth, 5)
t.window = C.NewWindow(
t.window = tui.NewWindow(
marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false)
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
case posRight:
pwidth := calculateSize(width, t.preview.size, minWidth, 5)
t.window = C.NewWindow(
t.window = tui.NewWindow(
marginInt[0], marginInt[3], width-pwidth, height, false)
createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height)
}
} else {
t.window = C.NewWindow(
t.window = tui.NewWindow(
marginInt[0],
marginInt[3],
width,
@@ -514,24 +532,24 @@ func (t *Terminal) placeCursor() {
func (t *Terminal) printPrompt() {
t.move(0, 0, true)
t.window.CPrint(C.ColPrompt, true, t.prompt)
t.window.CPrint(C.ColNormal, true, string(t.input))
t.window.CPrint(tui.ColPrompt, tui.Bold, t.prompt)
t.window.CPrint(tui.ColNormal, tui.Bold, string(t.input))
}
func (t *Terminal) printInfo() {
if t.inlineInfo {
t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true)
if t.reading {
t.window.CPrint(C.ColSpinner, true, " < ")
t.window.CPrint(tui.ColSpinner, tui.Bold, " < ")
} else {
t.window.CPrint(C.ColPrompt, true, " < ")
t.window.CPrint(tui.ColPrompt, tui.Bold, " < ")
}
} else {
t.move(1, 0, true)
if t.reading {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
t.window.CPrint(C.ColSpinner, true, _spinner[idx])
t.window.CPrint(tui.ColSpinner, tui.Bold, _spinner[idx])
}
t.move(1, 2, false)
}
@@ -550,7 +568,7 @@ func (t *Terminal) printInfo() {
if t.progress > 0 && t.progress < 100 {
output += fmt.Sprintf(" (%d%%)", t.progress)
}
t.window.CPrint(C.ColInfo, false, output)
t.window.CPrint(tui.ColInfo, 0, output)
}
func (t *Terminal) printHeader() {
@@ -574,7 +592,8 @@ func (t *Terminal) printHeader() {
colors: colors}
t.move(line, 2, true)
t.printHighlighted(&Result{item: item}, false, C.ColHeader, 0, false)
t.printHighlighted(&Result{item: item},
tui.AttrRegular, tui.ColHeader, tui.ColDefault, false)
}
}
@@ -608,21 +627,21 @@ func (t *Terminal) printItem(result *Result, i int, current bool) {
} else if current {
label = ">"
}
t.window.CPrint(C.ColCursor, true, label)
t.window.CPrint(tui.ColCursor, tui.Bold, label)
if current {
if selected {
t.window.CPrint(C.ColSelected, true, ">")
t.window.CPrint(tui.ColSelected, tui.Bold, ">")
} else {
t.window.CPrint(C.ColCurrent, true, " ")
t.window.CPrint(tui.ColCurrent, tui.Bold, " ")
}
t.printHighlighted(result, true, C.ColCurrent, C.ColCurrentMatch, true)
t.printHighlighted(result, tui.Bold, tui.ColCurrent, tui.ColCurrentMatch, true)
} else {
if selected {
t.window.CPrint(C.ColSelected, true, ">")
t.window.CPrint(tui.ColSelected, tui.Bold, ">")
} else {
t.window.Print(" ")
}
t.printHighlighted(result, false, 0, C.ColMatch, false)
t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false)
}
}
@@ -651,6 +670,11 @@ func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
}
func trimLeft(runes []rune, width int) ([]rune, int32) {
if len(runes) > maxDisplayWidthCalc && len(runes) > width {
trimmed := len(runes) - width
return runes[trimmed:], int32(trimmed)
}
currentWidth := displayWidth(runes)
var trimmed int32
@@ -673,7 +697,7 @@ func overflow(runes []rune, max int) bool {
return false
}
func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 int, current bool) {
func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool) {
item := result.item
// Overflow
@@ -698,7 +722,7 @@ func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 in
maxe = util.Max(maxe, int(offset[1]))
}
offsets := result.colorOffsets(charOffsets, col2, bold, current)
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))
if overflow(text, maxWidth) {
@@ -747,11 +771,11 @@ func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 in
e := util.Constrain32(offset.offset[1], index, maxOffset)
substr, prefixWidth = processTabs(text[index:b], prefixWidth)
t.window.CPrint(col1, bold, substr)
t.window.CPrint(col1, attr, substr)
if b < e {
substr, prefixWidth = processTabs(text[b:e], prefixWidth)
t.window.CPrint(offset.color, offset.bold, substr)
t.window.CPrint(offset.color, offset.attr, substr)
}
index = e
@@ -761,18 +785,52 @@ func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 in
}
if index < maxOffset {
substr, _ = processTabs(text[index:], prefixWidth)
t.window.CPrint(col1, bold, substr)
t.window.CPrint(col1, attr, substr)
}
}
func numLinesMax(str string, max int) int {
lines := 0
for lines < max {
idx := strings.Index(str, "\n")
if idx < 0 {
break
}
str = str[idx+1:]
lines++
}
return lines
}
func (t *Terminal) printPreview() {
if !t.isPreviewEnabled() {
return
}
t.pwindow.Erase()
extractColor(t.previewTxt, nil, func(str string, ansi *ansiState) bool {
skip := t.previewer.offset
extractColor(t.previewer.text, nil, func(str string, ansi *ansiState) bool {
if skip > 0 {
newlines := numLinesMax(str, skip)
if skip <= newlines {
for i := 0; i < skip; i++ {
str = str[strings.Index(str, "\n")+1:]
}
skip = 0
} else {
skip -= newlines
return true
}
}
if ansi != nil && ansi.colored() {
return t.pwindow.CFill(str, ansi.fg, ansi.bg, ansi.bold)
return t.pwindow.CFill(str, ansi.fg, ansi.bg, ansi.attr)
}
return t.pwindow.Fill(str)
})
if t.previewer.offset > 0 {
offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines)
t.pwindow.Move(0, t.pwindow.Width-len(offset))
t.pwindow.CPrint(tui.ColInfo, tui.Reverse, offset)
}
}
func processTabs(runes []rune, prefixWidth int) (string, int) {
@@ -796,19 +854,16 @@ func (t *Terminal) printAll() {
t.printPrompt()
t.printInfo()
t.printHeader()
if t.isPreviewEnabled() {
t.printPreview()
}
t.printPreview()
}
func (t *Terminal) refresh() {
if !t.suppress {
if t.isPreviewEnabled() {
t.bwindow.Refresh()
t.pwindow.Refresh()
tui.RefreshWindows([]*tui.Window{t.bwindow, t.pwindow, t.window})
} else {
tui.RefreshWindows([]*tui.Window{t.window})
}
t.window.Refresh()
C.DoUpdate()
}
}
@@ -858,24 +913,82 @@ func (t *Terminal) rubout(pattern string) {
t.input = append(t.input[:t.cx], after...)
}
func keyMatch(key int, event C.Event) bool {
func keyMatch(key int, event tui.Event) bool {
return event.Type == key ||
event.Type == C.Rune && int(event.Char) == key-C.AltZ ||
event.Type == C.Mouse && key == C.DoubleClick && event.MouseEvent.Double
event.Type == tui.Rune && int(event.Char) == key-tui.AltZ ||
event.Type == tui.Mouse && key == tui.DoubleClick && event.MouseEvent.Double
}
func quoteEntry(entry string) string {
if util.IsWindows() {
return strconv.Quote(strings.Replace(entry, "\"", "\\\"", -1))
}
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
}
func (t *Terminal) executeCommand(template string, replacement string) {
command := strings.Replace(template, "{}", replacement, -1)
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, query string, items []*Item) string {
return placeholder.ReplaceAllStringFunc(template, func(match string) string {
// Escaped pattern
if match[0] == '\\' {
return match[1:]
}
// Current query
if match == "{q}" {
return quoteEntry(query)
}
replacements := make([]string, len(items))
if match == "{}" {
for idx, item := range items {
replacements[idx] = quoteEntry(item.AsString(stripAnsi))
}
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 {
chars := util.RunesToChars([]rune(item.AsString(stripAnsi)))
tokens := Tokenize(chars, delimiter)
trans := Transform(tokens, ranges)
str := string(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]]
}
}
str = strings.TrimSpace(str)
replacements[idx] = quoteEntry(str)
}
return strings.Join(replacements, " ")
})
}
func (t *Terminal) executeCommand(template string, items []*Item) {
command := replacePlaceholder(template, t.ansi, t.delimiter, string(t.input), items)
cmd := util.ExecCommand(command)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
C.Endwin()
tui.Pause()
cmd.Run()
if tui.Resume() {
t.printAll()
}
t.refresh()
}
@@ -884,11 +997,15 @@ func (t *Terminal) hasPreviewWindow() bool {
}
func (t *Terminal) isPreviewEnabled() bool {
return t.previewBox != nil && t.previewing
return t.previewBox != nil && t.previewer.enabled
}
func (t *Terminal) currentItem() *Item {
return t.merger.Get(t.cy).item
}
func (t *Terminal) current() string {
return t.merger.Get(t.cy).item.AsString(t.ansi)
return t.currentItem().AsString(t.ansi)
}
// Loop is called to start Terminal I/O
@@ -904,7 +1021,7 @@ func (t *Terminal) Loop() {
}()
resizeChan := make(chan os.Signal, 1)
signal.Notify(resizeChan, syscall.SIGWINCH)
notifyOnResize(resizeChan) // Non-portable
go func() {
for {
<-resizeChan
@@ -945,18 +1062,19 @@ func (t *Terminal) Loop() {
if t.hasPreviewWindow() {
go func() {
for {
request := previewRequest{false, ""}
var request *Item
t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events {
switch req {
case reqPreviewEnqueue:
request = value.(previewRequest)
request = value.(*Item)
}
}
events.Clear()
})
if request.ok {
command := strings.Replace(t.preview.command, "{}", quoteEntry(request.str), -1)
if request != nil {
command := replacePlaceholder(t.preview.command,
t.ansi, t.delimiter, string(t.input), []*Item{request})
cmd := util.ExecCommand(command)
out, _ := cmd.CombinedOutput()
t.reqBox.Set(reqPreviewDisplay, string(out))
@@ -976,7 +1094,7 @@ func (t *Terminal) Loop() {
}
go func() {
focused := previewRequest{false, ""}
var focused *Item
for {
t.reqBox.Wait(func(events *util.Events) {
defer events.Clear()
@@ -993,11 +1111,11 @@ func (t *Terminal) Loop() {
case reqList:
t.printList()
cnt := t.merger.Length()
var currentFocus previewRequest
var currentFocus *Item
if cnt > 0 && cnt > t.cy {
currentFocus = previewRequest{true, t.current()}
currentFocus = t.currentItem()
} else {
currentFocus = previewRequest{false, ""}
currentFocus = nil
}
if currentFocus != focused {
focused = currentFocus
@@ -1015,25 +1133,28 @@ func (t *Terminal) Loop() {
case reqRefresh:
t.suppress = false
case reqRedraw:
C.Clear()
C.Endwin()
C.Refresh()
tui.Clear()
tui.Refresh()
t.printAll()
case reqClose:
C.Close()
tui.Close()
if t.output() {
exit(exitOk)
}
exit(exitNoMatch)
case reqPreviewDisplay:
t.previewTxt = value.(string)
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:
C.Close()
tui.Close()
t.printer(string(t.input))
exit(exitOk)
case reqQuit:
C.Close()
tui.Close()
exit(exitInterrupt)
}
}
@@ -1046,7 +1167,7 @@ func (t *Terminal) Loop() {
looping := true
for looping {
event := C.GetChar()
event := tui.GetChar()
t.mutex.Lock()
previousInput := t.input
@@ -1061,7 +1182,7 @@ func (t *Terminal) Loop() {
}
selectItem := func(item *Item) bool {
if _, found := t.selected[item.Index()]; !found {
t.selected[item.Index()] = selectedItem{time.Now(), item.AsString(t.ansi)}
t.selected[item.Index()] = selectedItem{time.Now(), item}
return true
}
return false
@@ -1078,6 +1199,11 @@ func (t *Terminal) Loop() {
req(reqInfo)
}
}
scrollPreview := func(amount int) {
t.previewer.offset = util.Constrain(
t.previewer.offset+amount, 0, t.previewer.lines-1)
req(reqPreviewRefresh)
}
for key, ret := range t.expect {
if keyMatch(key, event) {
t.pressed = ret
@@ -1093,16 +1219,15 @@ func (t *Terminal) Loop() {
case actIgnore:
case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy).item
t.executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi)))
t.executeCommand(t.execmap[mapkey], []*Item{t.currentItem()})
}
case actExecuteMulti:
if len(t.selected) > 0 {
sels := make([]string, len(t.selected))
sels := make([]*Item, len(t.selected))
for i, sel := range t.sortSelected() {
sels[i] = quoteEntry(sel.text)
sels[i] = sel.item
}
t.executeCommand(t.execmap[mapkey], strings.Join(sels, " "))
t.executeCommand(t.execmap[mapkey], sels)
} else {
return doAction(actExecute, mapkey)
}
@@ -1111,19 +1236,35 @@ func (t *Terminal) Loop() {
return false
case actTogglePreview:
if t.hasPreviewWindow() {
t.previewing = !t.previewing
t.previewer.enabled = !t.previewer.enabled
t.resizeWindows()
cnt := t.merger.Length()
if t.previewing && cnt > 0 && cnt > t.cy {
t.previewBox.Set(reqPreviewEnqueue, previewRequest{true, t.current()})
if t.previewer.enabled && cnt > 0 && cnt > t.cy {
t.previewBox.Set(reqPreviewEnqueue, t.currentItem())
}
req(reqList, reqInfo)
req(reqList, reqInfo, reqHeader)
}
case actToggleSort:
t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort)
t.mutex.Unlock()
return false
case actPreviewUp:
if t.isPreviewEnabled() {
scrollPreview(-1)
}
case actPreviewDown:
if t.isPreviewEnabled() {
scrollPreview(1)
}
case actPreviewPageUp:
if t.isPreviewEnabled() {
scrollPreview(-t.pwindow.Height)
}
case actPreviewPageDown:
if t.isPreviewEnabled() {
scrollPreview(t.pwindow.Height)
}
case actBeginningOfLine:
t.cx = 0
case actBackwardChar:
@@ -1292,6 +1433,8 @@ func (t *Terminal) Loop() {
}
t.vmove(me.S)
req(reqList)
} else if t.isPreviewEnabled() && t.pwindow.Enclose(my, mx) {
scrollPreview(-me.S)
}
} else if t.window.Enclose(my, mx) {
mx -= t.window.Left
@@ -1308,7 +1451,7 @@ func (t *Terminal) Loop() {
// Double-click
if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
return doAction(t.keymap[C.DoubleClick], C.DoubleClick)
return doAction(t.keymap[tui.DoubleClick], tui.DoubleClick)
}
}
} else if me.Down {
@@ -1331,8 +1474,8 @@ func (t *Terminal) Loop() {
mapkey := event.Type
if t.jumping == jumpDisabled {
action := t.keymap[mapkey]
if mapkey == C.Rune {
mapkey = int(event.Char) + int(C.AltZ)
if mapkey == tui.Rune {
mapkey = int(event.Char) + int(tui.AltZ)
if act, prs := t.keymap[mapkey]; prs {
action = act
}
@@ -1347,7 +1490,7 @@ func (t *Terminal) Loop() {
}
changed = string(previousInput) != string(t.input)
} else {
if mapkey == C.Rune {
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 {

73
src/terminal_test.go Normal file
View File

@@ -0,0 +1,73 @@
package 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.RunesToChars([]rune(trimmed))}
}
func TestReplacePlaceholder(t *testing.T) {
items1 := []*Item{newItem(" foo'bar \x1b[31mbaz\x1b[m")}
items2 := []*Item{
newItem("foo'bar \x1b[31mbaz\x1b[m"),
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
var result string
check := func(expected string) {
if result != expected {
t.Errorf("expected: %s, actual: %s", expected, result)
}
}
// {}, preserve ansi
result = replacePlaceholder("echo {}", false, Delimiter{}, "query", items1)
check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'")
// {}, strip ansi
result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items1)
check("echo ' foo'\\''bar baz'")
// {}, with multiple items
result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items2)
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ'")
// {..}, strip leading whitespaces, preserve ansi
result = replacePlaceholder("echo {..}", false, Delimiter{}, "query", items1)
check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'")
// {..}, strip leading whitespaces, strip ansi
result = replacePlaceholder("echo {..}", true, Delimiter{}, "query", items1)
check("echo 'foo'\\''bar baz'")
// {q}
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, "query", items1)
check("echo ' foo'\\''bar baz' 'query'")
// {q}, multiple items
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, "query 'string'", items2)
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'")
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, "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{}, "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}/'' ''")
// String delimiter
delim := "'"
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, "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}, "query", items1)
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
}

13
src/terminal_unix.go Normal file
View File

@@ -0,0 +1,13 @@
// +build !windows
package fzf
import (
"os"
"os/signal"
"syscall"
)
func notifyOnResize(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGWINCH)
}

11
src/terminal_windows.go Normal file
View File

@@ -0,0 +1,11 @@
// +build windows
package fzf
import (
"os"
)
func notifyOnResize(resizeChan chan<- os.Signal) {
// TODO
}

View File

@@ -1,4 +1,7 @@
package curses
// +build !windows
// +build !tcell
package tui
/*
#include <ncurses.h>
@@ -10,7 +13,6 @@ package curses
SCREEN *c_newterm () {
return newterm(NULL, stderr, stdin);
}
*/
import "C"
@@ -23,87 +25,26 @@ import (
"unicode/utf8"
)
// Types of user action
type ColorPair int16
type Attr C.int
type WindowImpl C.WINDOW
const (
Rune = iota
Bold = C.A_BOLD
Dim = C.A_DIM
Blink = C.A_BLINK
Reverse = C.A_REVERSE
Underline = C.A_UNDERLINE
)
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
Invalid
Mouse
DoubleClick
BTab
BSpace
Del
PgUp
PgDn
Up
Down
Left
Right
Home
End
SLeft
SRight
F1
F2
F3
F4
F5
F6
F7
F8
F9
F10
AltEnter
AltSpace
AltSlash
AltBS
AltA
AltB
AltC
AltD
AltE
AltF
AltZ = AltA + 'z' - 'a'
const (
AttrRegular Attr = 0
)
// Pallete
const (
ColNormal = iota
ColDefault ColorPair = iota
ColNormal
ColPrompt
ColMatch
ColCurrent
@@ -117,200 +58,26 @@ const (
ColUser // Should be the last entry
)
const (
doubleClickDuration = 500 * time.Millisecond
colDefault = -1
colUndefined = -2
)
type ColorTheme struct {
UseDefault bool
Fg int16
Bg int16
DarkBg int16
Prompt int16
Match int16
Current int16
CurrentMatch int16
Spinner int16
Info int16
Cursor int16
Selected int16
Header int16
Border int16
}
type Event struct {
Type int
Char rune
MouseEvent *MouseEvent
}
type MouseEvent struct {
Y int
X int
S int
Down bool
Double bool
Mod bool
}
var (
_buf []byte
_in *os.File
_color func(int, bool) C.int
_colorMap map[int]int
_prevDownTime time.Time
_clickY []int
_screen *C.SCREEN
Default16 *ColorTheme
Dark256 *ColorTheme
Light256 *ColorTheme
FG int
CurrentFG int
BG int
DarkBG int
_in *os.File
_screen *C.SCREEN
_colorMap map[int]ColorPair
_colorFn func(ColorPair, Attr) C.int
)
type Window struct {
win *C.WINDOW
Top int
Left int
Width int
Height int
}
func NewWindow(top int, left int, width int, height int, border bool) *Window {
win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left))
if border {
attr := _color(ColBorder, false)
C.wattron(win, attr)
C.box(win, 0, 0)
C.wattroff(win, attr)
}
return &Window{
win: win,
Top: top,
Left: left,
Width: width,
Height: height,
}
}
func EmptyTheme() *ColorTheme {
return &ColorTheme{
UseDefault: true,
Fg: colUndefined,
Bg: colUndefined,
DarkBg: colUndefined,
Prompt: colUndefined,
Match: colUndefined,
Current: colUndefined,
CurrentMatch: colUndefined,
Spinner: colUndefined,
Info: colUndefined,
Cursor: colUndefined,
Selected: colUndefined,
Header: colUndefined,
Border: colUndefined}
}
func init() {
_prevDownTime = time.Unix(0, 0)
_clickY = []int{}
_colorMap = make(map[int]int)
Default16 = &ColorTheme{
UseDefault: true,
Fg: 15,
Bg: 0,
DarkBg: C.COLOR_BLACK,
Prompt: C.COLOR_BLUE,
Match: C.COLOR_GREEN,
Current: C.COLOR_YELLOW,
CurrentMatch: C.COLOR_GREEN,
Spinner: C.COLOR_GREEN,
Info: C.COLOR_WHITE,
Cursor: C.COLOR_RED,
Selected: C.COLOR_MAGENTA,
Header: C.COLOR_CYAN,
Border: C.COLOR_BLACK}
Dark256 = &ColorTheme{
UseDefault: true,
Fg: 15,
Bg: 0,
DarkBg: 236,
Prompt: 110,
Match: 108,
Current: 254,
CurrentMatch: 151,
Spinner: 148,
Info: 144,
Cursor: 161,
Selected: 168,
Header: 109,
Border: 59}
Light256 = &ColorTheme{
UseDefault: true,
Fg: 15,
Bg: 0,
DarkBg: 251,
Prompt: 25,
Match: 66,
Current: 237,
CurrentMatch: 23,
Spinner: 65,
Info: 101,
Cursor: 161,
Selected: 168,
Header: 31,
Border: 145}
_colorMap = make(map[int]ColorPair)
}
func attrColored(pair int, bold bool) C.int {
var attr C.int
if pair > ColNormal {
attr = C.COLOR_PAIR(C.int(pair))
}
if bold {
attr = attr | C.A_BOLD
}
return attr
func (a Attr) Merge(b Attr) Attr {
return a | b
}
func attrMono(pair int, bold bool) C.int {
var attr C.int
switch pair {
case ColCurrent:
if bold {
attr = C.A_REVERSE
}
case ColMatch:
attr = C.A_UNDERLINE
case ColCurrentMatch:
attr = C.A_UNDERLINE | C.A_REVERSE
func DefaultTheme() *ColorTheme {
if C.tigetnum(C.CString("colors")) >= 256 {
return Dark256
}
if bold {
attr = attr | C.A_BOLD
}
return attr
}
func MaxX() int {
return int(C.COLS)
}
func MaxY() int {
return int(C.LINES)
}
func getch(nonblock bool) int {
b := make([]byte, 1)
syscall.SetNonblock(int(_in.Fd()), nonblock)
_, err := _in.Read(b)
if err != nil {
return -1
}
return int(b[0])
return Default16
}
func Init(theme *ColorTheme, black bool, mouse bool) {
@@ -337,61 +104,42 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
C.noecho()
C.raw() // stty dsusp undef
if theme != nil {
_color = theme != nil
if _color {
C.start_color()
var baseTheme *ColorTheme
if C.tigetnum(C.CString("colors")) >= 256 {
baseTheme = Dark256
} else {
baseTheme = Default16
}
initPairs(baseTheme, theme, black)
_color = attrColored
InitTheme(theme, black)
initPairs(theme)
C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal))))
_colorFn = attrColored
} else {
_color = attrMono
_colorFn = attrMono
}
}
func override(a int16, b int16) C.short {
if b == colUndefined {
return C.short(a)
func initPairs(theme *ColorTheme) {
C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg))
initPair := func(group ColorPair, fg Color, bg Color) {
C.init_pair(C.short(group), C.short(fg), C.short(bg))
}
return C.short(b)
initPair(ColNormal, theme.Fg, theme.Bg)
initPair(ColPrompt, theme.Prompt, theme.Bg)
initPair(ColMatch, theme.Match, theme.Bg)
initPair(ColCurrent, theme.Current, theme.DarkBg)
initPair(ColCurrentMatch, theme.CurrentMatch, theme.DarkBg)
initPair(ColSpinner, theme.Spinner, theme.Bg)
initPair(ColInfo, theme.Info, theme.Bg)
initPair(ColCursor, theme.Cursor, theme.DarkBg)
initPair(ColSelected, theme.Selected, theme.DarkBg)
initPair(ColHeader, theme.Header, theme.Bg)
initPair(ColBorder, theme.Border, theme.Bg)
}
func initPairs(baseTheme *ColorTheme, theme *ColorTheme, black bool) {
fg := override(baseTheme.Fg, theme.Fg)
bg := override(baseTheme.Bg, theme.Bg)
if black {
bg = C.COLOR_BLACK
} else if theme.UseDefault {
fg = colDefault
bg = colDefault
C.use_default_colors()
}
if theme.UseDefault {
FG = colDefault
BG = colDefault
} else {
FG = int(fg)
BG = int(bg)
C.assume_default_colors(C.int(override(baseTheme.Fg, theme.Fg)), C.int(bg))
}
func Pause() {
C.endwin()
}
currentFG := override(baseTheme.Current, theme.Current)
darkBG := override(baseTheme.DarkBg, theme.DarkBg)
CurrentFG = int(currentFG)
DarkBG = int(darkBG)
C.init_pair(ColPrompt, override(baseTheme.Prompt, theme.Prompt), bg)
C.init_pair(ColMatch, override(baseTheme.Match, theme.Match), bg)
C.init_pair(ColCurrent, currentFG, darkBG)
C.init_pair(ColCurrentMatch, override(baseTheme.CurrentMatch, theme.CurrentMatch), darkBG)
C.init_pair(ColSpinner, override(baseTheme.Spinner, theme.Spinner), bg)
C.init_pair(ColInfo, override(baseTheme.Info, theme.Info), bg)
C.init_pair(ColCursor, override(baseTheme.Cursor, theme.Cursor), darkBG)
C.init_pair(ColSelected, override(baseTheme.Selected, theme.Selected), darkBG)
C.init_pair(ColHeader, override(baseTheme.Header, theme.Header), bg)
C.init_pair(ColBorder, override(baseTheme.Border, theme.Border), bg)
func Resume() bool {
return false
}
func Close() {
@@ -399,15 +147,172 @@ func Close() {
C.delscreen(_screen)
}
func NewWindow(top int, left int, width int, height int, border bool) *Window {
win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left))
if _color {
C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal))))
}
if border {
attr := _colorFn(ColBorder, 0)
C.wattron(win, attr)
C.box(win, 0, 0)
C.wattroff(win, attr)
}
return &Window{
impl: (*WindowImpl)(win),
Top: top,
Left: left,
Width: width,
Height: height,
}
}
func attrColored(pair ColorPair, a Attr) C.int {
var attr C.int
if pair > 0 {
attr = C.COLOR_PAIR(C.int(pair))
}
return attr | C.int(a)
}
func attrMono(pair ColorPair, a Attr) C.int {
var attr C.int
switch pair {
case ColCurrent:
if C.int(a)&C.A_BOLD == C.A_BOLD {
attr = C.A_REVERSE
}
case ColMatch:
attr = C.A_UNDERLINE
case ColCurrentMatch:
attr = C.A_UNDERLINE | C.A_REVERSE
}
if C.int(a)&C.A_BOLD == C.A_BOLD {
attr = attr | C.A_BOLD
}
return attr
}
func MaxX() int {
return int(C.COLS)
}
func MaxY() int {
return int(C.LINES)
}
func (w *Window) win() *C.WINDOW {
return (*C.WINDOW)(w.impl)
}
func (w *Window) Close() {
C.delwin(w.win())
}
func (w *Window) Enclose(y int, x int) bool {
return bool(C.wenclose(w.win(), C.int(y), C.int(x)))
}
func (w *Window) Move(y int, x int) {
C.wmove(w.win(), C.int(y), C.int(x))
}
func (w *Window) MoveAndClear(y int, x int) {
w.Move(y, x)
C.wclrtoeol(w.win())
}
func (w *Window) Print(text string) {
C.waddstr(w.win(), C.CString(strings.Map(func(r rune) rune {
if r < 32 {
return -1
}
return r
}, text)))
}
func (w *Window) CPrint(pair ColorPair, a Attr, text string) {
attr := _colorFn(pair, a)
C.wattron(w.win(), attr)
w.Print(text)
C.wattroff(w.win(), attr)
}
func Clear() {
C.clear()
C.endwin()
}
func Refresh() {
C.refresh()
}
func (w *Window) Erase() {
C.werase(w.win())
}
func (w *Window) Fill(str string) bool {
return C.waddstr(w.win(), C.CString(str)) == C.OK
}
func (w *Window) CFill(str string, fg Color, bg Color, a Attr) bool {
attr := _colorFn(PairFor(fg, bg), a)
C.wattron(w.win(), attr)
ret := w.Fill(str)
C.wattroff(w.win(), attr)
return ret
}
func RefreshWindows(windows []*Window) {
for _, w := range windows {
C.wnoutrefresh(w.win())
}
C.doupdate()
}
func PairFor(fg Color, bg Color) ColorPair {
key := (int(fg) << 8) + int(bg)
if found, prs := _colorMap[key]; prs {
return found
}
id := ColorPair(len(_colorMap) + int(ColUser))
C.init_pair(C.short(id), C.short(fg), C.short(bg))
_colorMap[key] = id
return id
}
func getch(nonblock bool) int {
b := make([]byte, 1)
syscall.SetNonblock(int(_in.Fd()), nonblock)
_, err := _in.Read(b)
if err != nil {
return -1
}
return int(b[0])
}
func GetBytes() []byte {
c := getch(false)
retries := 0
if c == 27 {
// Wait for additional keys after ESC for 100ms (10 * 10ms)
retries = 10
}
_buf = append(_buf, byte(c))
for {
c = getch(true)
if c == -1 {
if retries > 0 {
retries--
time.Sleep(10 * time.Millisecond)
continue
}
break
}
retries = 0
_buf = append(_buf, byte(c))
}
@@ -621,84 +526,3 @@ func GetChar() Event {
sz = rsz
return Event{Rune, r, nil}
}
func (w *Window) Close() {
C.delwin(w.win)
}
func (w *Window) Enclose(y int, x int) bool {
return bool(C.wenclose(w.win, C.int(y), C.int(x)))
}
func (w *Window) Move(y int, x int) {
C.wmove(w.win, C.int(y), C.int(x))
}
func (w *Window) MoveAndClear(y int, x int) {
w.Move(y, x)
C.wclrtoeol(w.win)
}
func (w *Window) Print(text string) {
C.waddstr(w.win, C.CString(strings.Map(func(r rune) rune {
if r < 32 {
return -1
}
return r
}, text)))
}
func (w *Window) CPrint(pair int, bold bool, text string) {
attr := _color(pair, bold)
C.wattron(w.win, attr)
w.Print(text)
C.wattroff(w.win, attr)
}
func Clear() {
C.clear()
}
func Endwin() {
C.endwin()
}
func Refresh() {
C.refresh()
}
func (w *Window) Erase() {
C.werase(w.win)
}
func (w *Window) Fill(str string) bool {
return C.waddstr(w.win, C.CString(str)) == C.OK
}
func (w *Window) CFill(str string, fg int, bg int, bold bool) bool {
attr := _color(PairFor(fg, bg), bold)
C.wattron(w.win, attr)
ret := w.Fill(str)
C.wattroff(w.win, attr)
return ret
}
func (w *Window) Refresh() {
C.wnoutrefresh(w.win)
}
func DoUpdate() {
C.doupdate()
}
func PairFor(fg int, bg int) int {
key := (fg << 8) + bg
if found, prs := _colorMap[key]; prs {
return found
}
id := len(_colorMap) + ColUser
C.init_pair(C.short(id), C.short(fg), C.short(bg))
_colorMap[key] = id
return id
}

575
src/tui/tcell.go Normal file
View File

@@ -0,0 +1,575 @@
// +build tcell windows
package tui
import (
"time"
"unicode/utf8"
"fmt"
"os"
"runtime"
"github.com/gdamore/tcell"
"github.com/gdamore/tcell/encoding"
"github.com/junegunn/go-runewidth"
)
type ColorPair [2]Color
func (p ColorPair) fg() Color {
return p[0]
}
func (p ColorPair) bg() Color {
return p[1]
}
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 WindowTcell struct {
LastX int
LastY int
MoveCursor bool
Border bool
}
type WindowImpl WindowTcell
const (
Bold = Attr(tcell.AttrBold)
Dim = Attr(tcell.AttrDim)
Blink = Attr(tcell.AttrBlink)
Reverse = Attr(tcell.AttrReverse)
Underline = Attr(tcell.AttrUnderline)
)
const (
AttrRegular Attr = 0
)
var (
ColDefault = ColorPair{colDefault, colDefault}
ColNormal ColorPair
ColPrompt ColorPair
ColMatch ColorPair
ColCurrent ColorPair
ColCurrentMatch ColorPair
ColSpinner ColorPair
ColInfo ColorPair
ColCursor ColorPair
ColSelected ColorPair
ColHeader ColorPair
ColBorder ColorPair
ColUser ColorPair
)
func DefaultTheme() *ColorTheme {
if _screen.Colors() >= 256 {
return Dark256
}
return Default16
}
func PairFor(fg Color, bg Color) ColorPair {
return [2]Color{fg, bg}
}
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
_mouse bool
)
func initScreen() {
s, e := tcell.NewScreen()
if e != nil {
fmt.Fprintf(os.Stderr, "%v\n", e)
os.Exit(2)
}
if e = s.Init(); e != nil {
fmt.Fprintf(os.Stderr, "%v\n", e)
os.Exit(2)
}
if _mouse {
s.EnableMouse()
} else {
s.DisableMouse()
}
_screen = s
}
func Init(theme *ColorTheme, black bool, mouse bool) {
encoding.Register()
_mouse = mouse
initScreen()
_color = theme != nil
if _color {
InitTheme(theme, black)
} else {
theme = DefaultTheme()
}
ColNormal = ColorPair{theme.Fg, theme.Bg}
ColPrompt = ColorPair{theme.Prompt, theme.Bg}
ColMatch = ColorPair{theme.Match, theme.Bg}
ColCurrent = ColorPair{theme.Current, theme.DarkBg}
ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg}
ColSpinner = ColorPair{theme.Spinner, theme.Bg}
ColInfo = ColorPair{theme.Info, theme.Bg}
ColCursor = ColorPair{theme.Cursor, theme.DarkBg}
ColSelected = ColorPair{theme.Selected, theme.DarkBg}
ColHeader = ColorPair{theme.Header, theme.Bg}
ColBorder = ColorPair{theme.Border, theme.Bg}
}
func MaxX() int {
ncols, _ := _screen.Size()
return int(ncols)
}
func MaxY() int {
_, nlines := _screen.Size()
return int(nlines)
}
func (w *Window) win() *WindowTcell {
return (*WindowTcell)(w.impl)
}
func Clear() {
_screen.Sync()
_screen.Clear()
}
func Refresh() {
// noop
}
func GetChar() Event {
ev := _screen.PollEvent()
switch ev := ev.(type) {
case *tcell.EventResize:
return Event{Invalid, 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, mod}}
} else if button&tcell.WheelUp != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, +1, 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.
down := button&tcell.Button1 != 0 // left
double := false
if down {
now := time.Now()
if now.Sub(_prevDownTime) < doubleClickDuration {
_clickY = append(_clickY, x)
} else {
_clickY = []int{x}
}
_prevDownTime = now
} else {
if len(_clickY) > 1 && _clickY[0] == _clickY[1] &&
time.Now().Sub(_prevDownTime) < doubleClickDuration {
double = true
}
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}}
}
// process keyboard:
case *tcell.EventKey:
alt := (ev.Modifiers() & tcell.ModAlt) > 0
switch ev.Key() {
case tcell.KeyCtrlA:
return Event{CtrlA, 0, nil}
case tcell.KeyCtrlB:
return Event{CtrlB, 0, nil}
case tcell.KeyCtrlC:
return Event{CtrlC, 0, nil}
case tcell.KeyCtrlD:
return Event{CtrlD, 0, nil}
case tcell.KeyCtrlE:
return Event{CtrlE, 0, nil}
case tcell.KeyCtrlF:
return Event{CtrlF, 0, nil}
case tcell.KeyCtrlG:
return Event{CtrlG, 0, nil}
case tcell.KeyCtrlJ:
return Event{CtrlJ, 0, nil}
case tcell.KeyCtrlK:
return Event{CtrlK, 0, nil}
case tcell.KeyCtrlL:
return Event{CtrlL, 0, nil}
case tcell.KeyCtrlM:
if alt {
return Event{AltEnter, 0, nil}
}
return Event{CtrlM, 0, nil}
case tcell.KeyCtrlN:
return Event{CtrlN, 0, nil}
case tcell.KeyCtrlO:
return Event{CtrlO, 0, nil}
case tcell.KeyCtrlP:
return Event{CtrlP, 0, nil}
case tcell.KeyCtrlQ:
return Event{CtrlQ, 0, nil}
case tcell.KeyCtrlR:
return Event{CtrlR, 0, nil}
case tcell.KeyCtrlS:
return Event{CtrlS, 0, nil}
case tcell.KeyCtrlT:
return Event{CtrlT, 0, nil}
case tcell.KeyCtrlU:
return Event{CtrlU, 0, nil}
case tcell.KeyCtrlV:
return Event{CtrlV, 0, nil}
case tcell.KeyCtrlW:
return Event{CtrlW, 0, nil}
case tcell.KeyCtrlX:
return Event{CtrlX, 0, nil}
case tcell.KeyCtrlY:
return Event{CtrlY, 0, nil}
case tcell.KeyCtrlZ:
return Event{CtrlZ, 0, nil}
case tcell.KeyBackspace, tcell.KeyBackspace2:
if alt {
return Event{AltBS, 0, nil}
}
return Event{BSpace, 0, nil}
case tcell.KeyUp:
return Event{Up, 0, nil}
case tcell.KeyDown:
return Event{Down, 0, nil}
case tcell.KeyLeft:
return Event{Left, 0, nil}
case tcell.KeyRight:
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.KeyTab:
return Event{Tab, 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{Invalid, 0, nil}
case tcell.KeyF12:
return Event{Invalid, 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}
}
}
return Event{Rune, r, nil}
case tcell.KeyEsc:
return Event{ESC, 0, nil}
}
}
return Event{Invalid, 0, nil}
}
func Pause() {
_screen.Fini()
}
func Resume() bool {
initScreen()
return true
}
func Close() {
_screen.Fini()
}
func RefreshWindows(windows []*Window) {
// TODO
for _, w := range windows {
if w.win().MoveCursor {
_screen.ShowCursor(w.Left+w.win().LastX, w.Top+w.win().LastY)
w.win().MoveCursor = false
}
w.win().LastX = 0
w.win().LastY = 0
if w.win().Border {
w.DrawBorder()
}
}
_screen.Show()
}
func NewWindow(top int, left int, width int, height int, border bool) *Window {
// TODO
win := new(WindowTcell)
win.Border = border
return &Window{
impl: (*WindowImpl)(win),
Top: top,
Left: left,
Width: width,
Height: height,
}
}
func (w *Window) Close() {
// TODO
}
func fill(x, y, w, h int, r rune) {
for ly := 0; ly <= h; ly++ {
for lx := 0; lx <= w; lx++ {
_screen.SetContent(x+lx, y+ly, r, nil, ColDefault.style())
}
}
}
func (w *Window) Erase() {
// TODO
fill(w.Left, w.Top, w.Width, w.Height, ' ')
}
func (w *Window) 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 *Window) Move(y int, x int) {
w.win().LastX = x
w.win().LastY = y
w.win().MoveCursor = true
}
func (w *Window) MoveAndClear(y int, x int) {
w.Move(y, x)
for i := w.win().LastX; i < w.Width; i++ {
_screen.SetContent(i+w.Left, w.win().LastY+w.Top, rune(' '), nil, ColDefault.style())
}
w.win().LastX = x
}
func (w *Window) Print(text string) {
w.PrintString(text, ColDefault, 0)
}
func (w *Window) PrintString(text string, pair ColorPair, a Attr) {
t := text
lx := 0
var style tcell.Style
if _color {
style = pair.style().
Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0)
} else {
style = ColDefault.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.win().LastY++
lx = 0
} else {
if r == '\u000D' { // skip carriage return
continue
}
var xPos = w.Left + w.win().LastX + lx
var yPos = w.Top + w.win().LastY
if xPos < (w.Left+w.Width) && yPos < (w.Top+w.Height) {
_screen.SetContent(xPos, yPos, r, nil, style)
}
lx += runewidth.RuneWidth(r)
}
}
w.win().LastX += lx
}
func (w *Window) CPrint(pair ColorPair, a Attr, text string) {
w.PrintString(text, pair, a)
}
func (w *Window) FillString(text string, pair ColorPair, a Attr) bool {
lx := 0
var style tcell.Style
if _color {
style = pair.style()
} else {
style = ColDefault.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.win().LastY++
w.win().LastX = 0
lx = 0
} else {
var xPos = w.Left + w.win().LastX + lx
// word wrap:
if xPos > (w.Left + w.Width) {
w.win().LastY++
w.win().LastX = 0
lx = 0
xPos = w.Left
}
var yPos = w.Top + w.win().LastY
if yPos >= (w.Top + w.Height) {
return false
}
_screen.SetContent(xPos, yPos, r, nil, style)
lx += runewidth.RuneWidth(r)
}
}
w.win().LastX += lx
return true
}
func (w *Window) Fill(str string) bool {
return w.FillString(str, ColDefault, 0)
}
func (w *Window) CFill(str string, fg Color, bg Color, a Attr) bool {
return w.FillString(str, ColorPair{fg, bg}, a)
}
func (w *Window) DrawBorder() {
left := w.Left
right := left + w.Width
top := w.Top
bot := top + w.Height
var style tcell.Style
if _color {
style = ColBorder.style()
} else {
style = ColDefault.style()
}
for x := left; x < right; x++ {
_screen.SetContent(x, top, tcell.RuneHLine, nil, style)
_screen.SetContent(x, bot-1, tcell.RuneHLine, nil, style)
}
for y := top; y < bot; y++ {
_screen.SetContent(left, y, tcell.RuneVLine, nil, style)
_screen.SetContent(right-1, y, tcell.RuneVLine, nil, style)
}
_screen.SetContent(left, top, tcell.RuneULCorner, nil, style)
_screen.SetContent(right-1, top, tcell.RuneURCorner, nil, style)
_screen.SetContent(left, bot-1, tcell.RuneLLCorner, nil, style)
_screen.SetContent(right-1, bot-1, tcell.RuneLRCorner, nil, style)
}

250
src/tui/tui.go Normal file
View File

@@ -0,0 +1,250 @@
package tui
import (
"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
Invalid
Mouse
DoubleClick
BTab
BSpace
Del
PgUp
PgDn
Up
Down
Left
Right
Home
End
SLeft
SRight
F1
F2
F3
F4
F5
F6
F7
F8
F9
F10
AltEnter
AltSpace
AltSlash
AltBS
AltA
AltB
AltC
AltD
AltE
AltF
AltZ = AltA + 'z' - 'a'
)
const (
doubleClickDuration = 500 * time.Millisecond
)
type Color int16
const (
colUndefined Color = -2
colDefault = -1
)
const (
colBlack Color = iota
colRed
colGreen
colYellow
colBlue
colMagenta
colCyan
colWhite
)
type ColorTheme struct {
Fg Color
Bg Color
DarkBg 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
Down bool
Double bool
Mod bool
}
var (
_buf []byte
_color bool
_prevDownTime time.Time
_clickY []int
Default16 *ColorTheme
Dark256 *ColorTheme
Light256 *ColorTheme
)
type Window struct {
impl *WindowImpl
Top int
Left int
Width int
Height int
}
func EmptyTheme() *ColorTheme {
return &ColorTheme{
Fg: colUndefined,
Bg: colUndefined,
DarkBg: colUndefined,
Prompt: colUndefined,
Match: colUndefined,
Current: colUndefined,
CurrentMatch: colUndefined,
Spinner: colUndefined,
Info: colUndefined,
Cursor: colUndefined,
Selected: colUndefined,
Header: colUndefined,
Border: colUndefined}
}
func init() {
_prevDownTime = time.Unix(0, 0)
_clickY = []int{}
Default16 = &ColorTheme{
Fg: colDefault,
Bg: colDefault,
DarkBg: colBlack,
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,
DarkBg: 236,
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,
DarkBg: 251,
Prompt: 25,
Match: 66,
Current: 237,
CurrentMatch: 23,
Spinner: 65,
Info: 101,
Cursor: 161,
Selected: 168,
Header: 31,
Border: 145}
}
func InitTheme(theme *ColorTheme, black bool) {
_color = theme != nil
if !_color {
return
}
baseTheme := DefaultTheme()
if black {
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.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
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)
}

View File

@@ -1,4 +1,4 @@
package curses
package tui
import (
"testing"

View File

@@ -1,6 +1,7 @@
package util
import (
"unicode"
"unicode/utf8"
)
@@ -63,7 +64,7 @@ func (chars *Chars) TrimLength() int {
len := chars.Length()
for i = len - 1; i >= 0; i-- {
char := chars.Get(i)
if char != ' ' && char != '\t' {
if !unicode.IsSpace(char) {
break
}
}
@@ -75,7 +76,7 @@ func (chars *Chars) TrimLength() int {
var j int
for j = 0; j < len; j++ {
char := chars.Get(j)
if char != ' ' && char != '\t' {
if !unicode.IsSpace(char) {
break
}
}
@@ -86,7 +87,7 @@ func (chars *Chars) TrailingWhitespaces() int {
whitespaces := 0
for i := chars.Length() - 1; i >= 0; i-- {
char := chars.Get(i)
if char != ' ' && char != '\t' {
if !unicode.IsSpace(char) {
break
}
whitespaces++

View File

@@ -1,13 +1,11 @@
package util
// #include <unistd.h>
import "C"
import (
"math"
"os"
"os/exec"
"time"
"github.com/junegunn/go-isatty"
)
// Max returns the largest integer
@@ -95,14 +93,5 @@ func DurWithin(
// IsTty returns true is stdin is a terminal
func IsTty() bool {
return int(C.isatty(C.int(os.Stdin.Fd()))) != 0
}
// ExecCommand executes the given command with $SHELL
func ExecCommand(command string) *exec.Cmd {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "sh"
}
return exec.Command(shell, "-c", command)
return isatty.IsTerminal(os.Stdin.Fd())
}

22
src/util/util_unix.go Normal file
View File

@@ -0,0 +1,22 @@
// +build !windows
package util
import (
"os"
"os/exec"
)
// ExecCommand executes the given command with $SHELL
func ExecCommand(command string) *exec.Cmd {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "sh"
}
return exec.Command(shell, "-c", command)
}
// IsWindows returns true on Windows
func IsWindows() bool {
return false
}

28
src/util/util_windows.go Normal file
View File

@@ -0,0 +1,28 @@
// +build windows
package util
import (
"os"
"os/exec"
"github.com/junegunn/go-shellwords"
)
// ExecCommand executes the given command with $SHELL
func ExecCommand(command string) *exec.Cmd {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "cmd"
}
args, _ := shellwords.Parse(command)
allArgs := make([]string, len(args)+1)
allArgs[0] = "/c"
copy(allArgs[1:], args)
return exec.Command(shell, allArgs...)
}
// IsWindows returns true on Windows
func IsWindows() bool {
return true
}

View File

@@ -143,6 +143,10 @@ Execute (fzf#wrap):
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 (Cleanup):
unlet g:dir
Restore

View File

@@ -136,8 +136,10 @@ class Tmux
def prepare
tries = 0
begin
self.send_keys 'C-u', 'hello', 'Right'
self.until { |lines| lines[-1].end_with?('hello') }
self.until do |lines|
self.send_keys 'C-u', 'hello'
lines[-1].end_with?('hello')
end
rescue Exception
(tries += 1) < 5 ? retry : raise
end
@@ -604,8 +606,8 @@ class TestGoFZF < TestBase
], `#{FZF} -fo --tiebreak=end < #{tempname}`.split($/)
assert_equal [
' xxxxoxxx',
'xxxxxoxxx',
' xxxxoxxx',
'xxxxoxxxx',
'xxxoxxxxxx',
'xxoxxxxxxx',
@@ -1056,7 +1058,7 @@ class TestGoFZF < TestBase
def test_invalid_term
lines = `TERM=xxx #{FZF}`
assert_equal 2, $?.exitstatus
assert lines.include?('Invalid $TERM: xxx')
assert lines.include?('Invalid $TERM: xxx') || lines.include?('terminal entry not found')
end
def test_invalid_option
@@ -1251,24 +1253,29 @@ module TestShell
tmux.send_keys 'cat ', 'C-t', pane: 0
tmux.until(1) { |lines| lines.item_count >= 1 }
tmux.send_keys 'fzf-unicode', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' }
redraw = ->() { tmux.send_keys 'C-l', pane: 1 }
tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) }
tmux.send_keys '1', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) }
tmux.send_keys :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(1)' }
tmux.until(1) { |lines| redraw.(); lines[-2].include? '(1)' }
tmux.send_keys :BSpace, pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' }
tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) }
tmux.send_keys '2', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) }
tmux.send_keys :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(2)' }
tmux.until(1) { |lines| redraw.(); lines[-2].include? '(2)' }
tmux.send_keys :Enter, pane: 1
tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') }
tmux.until { |lines| lines[-1].include?('fzf-unicode') || lines[-2].include?('fzf-unicode') }
tmux.until do |lines|
tmux.send_keys 'C-l'
[-1, -2].map { |offset| lines[offset] }.any? do |line|
line.start_with?('cat') && line.include?('fzf-unicode')
end
end
tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'test1test2' }
end
@@ -1479,23 +1486,27 @@ module CompletionTest
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, pane: 0
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' }
redraw = ->() { tmux.send_keys 'C-l', pane: 1 }
tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) }
tmux.send_keys '1', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) }
tmux.send_keys :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(1)' }
tmux.until(1) { |lines| redraw.(); lines[-2].include? '(1)' }
tmux.send_keys :BSpace, pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' }
tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) }
tmux.send_keys '2', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) }
tmux.send_keys :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(2)' }
tmux.until(1) { |lines| redraw.(); lines[-2].include? '(2)' }
tmux.send_keys :Enter, pane: 1
tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') }
tmux.until do |lines|
tmux.send_keys 'C-l'
lines[-1].include?('cat') || lines[-2].include?('cat')
end
tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'test3test4' }
end