mirror of
https://github.com/junegunn/fzf.git
synced 2025-07-28 10:42:00 -07:00
Compare commits
136 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ee5aeb80a4 | ||
|
02ceae15a2 | ||
|
e514739280 | ||
|
72265298f9 | ||
|
4b700192c1 | ||
|
fe83589ade | ||
|
fcf63c74f1 | ||
|
c95bb109c8 | ||
|
bd9c46ee34 | ||
|
736aeaa1d3 | ||
|
dd1f26522c | ||
|
712b7b2188 | ||
|
5b749e2d5c | ||
|
d85a69a709 | ||
|
a425e96fb2 | ||
|
7763fdf6ba | ||
|
dd156b59fc | ||
|
36dceecd58 | ||
|
421b9b271a | ||
|
ed57dcb924 | ||
|
95c77bfb98 | ||
|
2e3e721344 | ||
|
da2c28d5c2 | ||
|
dbddee9de9 | ||
|
8731d75607 | ||
|
f2ce233a6d | ||
|
6a75e30941 | ||
|
a3244c4892 | ||
|
a5ad8fd3bd | ||
|
deccdb1ec5 | ||
|
12a43b5e62 | ||
|
e1291aa6d2 | ||
|
bb26f32ac7 | ||
|
4d928001b8 | ||
|
c4baa6a10c | ||
|
71dec3dc5e | ||
|
e5017c0431 | ||
|
cbb5134874 | ||
|
ff248d566d | ||
|
6ccc12c332 | ||
|
2a669e9a17 | ||
|
5130abe76f | ||
|
fa7c8977a8 | ||
|
24fa183297 | ||
|
131aa5dd15 | ||
|
a06ccc928f | ||
|
d09ad13208 | ||
|
8ac37d5927 | ||
|
7ef0e50507 | ||
|
62ab8ece5e | ||
|
8e2e63f9b9 | ||
|
f96173cbe4 | ||
|
11015df52f | ||
|
05ed57a9f0 | ||
|
4bece04207 | ||
|
ede7bfb901 | ||
|
44d3faa048 | ||
|
e0036b5ad2 | ||
|
208d4f2173 | ||
|
dc3957ce79 | ||
|
4ecb7f3a16 | ||
|
03f5ef08c8 | ||
|
2720816266 | ||
|
1896aa1748 | ||
|
5b68027bee | ||
|
48863ac55c | ||
|
d64828ce6d | ||
|
2aa739be81 | ||
|
9977a3e9fc | ||
|
f8082bc53a | ||
|
996dcb14a3 | ||
|
0c127cfdc1 | ||
|
ae274158de | ||
|
340af463cd | ||
|
78a3f81972 | ||
|
d18b8e0d2c | ||
|
6c6c0a4778 | ||
|
a16d8f66a9 | ||
|
45793d75c2 | ||
|
9d545f9578 | ||
|
a30999a785 | ||
|
1a50f1eca1 | ||
|
1448d631a7 | ||
|
fd137a9e87 | ||
|
3670273719 | ||
|
6c0fd7f9ca | ||
|
42a2371d26 | ||
|
45faad7e04 | ||
|
73eacf1137 | ||
|
7b0d9e1e07 | ||
|
c7b0764002 | ||
|
847c512539 | ||
|
97330ee8fc | ||
|
0508e70f9b | ||
|
8a502af4c1 | ||
|
c60bfb2b0f | ||
|
16b5902aa2 | ||
|
a442fe0fd0 | ||
|
ab9ae4f643 | ||
|
d9a51030ea | ||
|
67026718c1 | ||
|
a71c471405 | ||
|
3858086047 | ||
|
dffef3d9f3 | ||
|
de1c6b8727 | ||
|
6f17f412ba | ||
|
746961bf43 | ||
|
182a6d99fd | ||
|
af31088481 | ||
|
43425158f4 | ||
|
8524ea7441 | ||
|
6a65006f55 | ||
|
d75ed841a9 | ||
|
3cd2547e91 | ||
|
8c661d4e8c | ||
|
4b332d831e | ||
|
22487810ba | ||
|
c49e65d926 | ||
|
2e8814bb57 | ||
|
dc557c0d4c | ||
|
a2beb159f1 | ||
|
7ce427ff47 | ||
|
a221c672fb | ||
|
f87d382ec8 | ||
|
3dfc020fac | ||
|
2d87896939 | ||
|
2192d8d816 | ||
|
d206949f62 | ||
|
4accc69022 | ||
|
898d8d94c8 | ||
|
26895da969 | ||
|
0c573b3dff | ||
|
2cff00dce2 | ||
|
06a6ad8bca | ||
|
02c6ad0e59 | ||
|
9f321cbe13 |
1
.github/ISSUE_TEMPLATE.md
vendored
1
.github/ISSUE_TEMPLATE.md
vendored
@@ -11,6 +11,7 @@
|
||||
- [ ] Linux
|
||||
- [ ] Mac OS X
|
||||
- [ ] Windows
|
||||
- [ ] Windows Subsystem for Linux
|
||||
- [ ] Etc.
|
||||
- Shell
|
||||
- [ ] bash
|
||||
|
@@ -1,6 +1,10 @@
|
||||
language: ruby
|
||||
rvm:
|
||||
- 2.2.0
|
||||
matrix:
|
||||
include:
|
||||
- env: TAGS=
|
||||
rvm: 2.3.3
|
||||
# - env: TAGS=tcell
|
||||
# rvm: 2.2.0
|
||||
|
||||
install:
|
||||
- sudo apt-get update
|
||||
|
56
BUILD.md
Normal file
56
BUILD.md
Normal file
@@ -0,0 +1,56 @@
|
||||
Building fzf
|
||||
============
|
||||
|
||||
Build instructions
|
||||
------------------
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- `go` executable in $PATH
|
||||
|
||||
### Using Makefile
|
||||
|
||||
Makefile will set up and use its own `$GOPATH` under the project root.
|
||||
|
||||
```sh
|
||||
# Source files are located in src directory
|
||||
cd src
|
||||
|
||||
# Build fzf binary for your platform in src/fzf
|
||||
make
|
||||
|
||||
# Build fzf binary and copy it to bin directory
|
||||
make install
|
||||
|
||||
# Build 32-bit and 64-bit executables and tarballs
|
||||
make release
|
||||
|
||||
# Make release archives for all supported platforms
|
||||
make release-all
|
||||
```
|
||||
|
||||
### Using `go get`
|
||||
|
||||
Alternatively, you can build fzf directly with `go get` command without
|
||||
manually cloning the repository.
|
||||
|
||||
```sh
|
||||
go get -u github.com/junegunn/fzf/src/fzf
|
||||
```
|
||||
|
||||
Third-party libraries used
|
||||
--------------------------
|
||||
|
||||
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
|
||||
- Licensed under [MIT](http://mattn.mit-license.org)
|
||||
- [mattn/go-shellwords](https://github.com/mattn/go-shellwords)
|
||||
- Licensed under [MIT](http://mattn.mit-license.org)
|
||||
- [mattn/go-isatty](https://github.com/mattn/go-isatty)
|
||||
- Licensed under [MIT](http://mattn.mit-license.org)
|
||||
- [tcell](https://github.com/gdamore/tcell)
|
||||
- Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE)
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
[MIT](LICENSE)
|
83
CHANGELOG.md
83
CHANGELOG.md
@@ -1,6 +1,89 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.16.4
|
||||
------
|
||||
- Added `--border` option to draw border above and below the finder
|
||||
- Bug fixes and improvements
|
||||
|
||||
0.16.3
|
||||
------
|
||||
- Fixed a bug where fzf incorrectly display the lines when straddling tab
|
||||
characters are trimmed
|
||||
- Placeholder expression used in `--preview` and `execute` action can
|
||||
optionally take `+` flag to be used with multiple selections
|
||||
- e.g. `git log --oneline | fzf --multi --preview 'git show {+1}'`
|
||||
- Added `execute-silent` action for executing a command silently without
|
||||
switching to the alternate screen. This is useful when the process is
|
||||
short-lived and you're not interested in its output.
|
||||
- e.g. `fzf --bind 'ctrl-y:execute!(echo -n {} | pbcopy)'`
|
||||
- `ctrl-space` is allowed in `--bind`
|
||||
|
||||
0.16.2
|
||||
------
|
||||
- Dropped ncurses dependency
|
||||
- Binaries for freebsd, openbsd, arm5, arm6, arm7, and arm8
|
||||
- Official 24-bit color support
|
||||
- Added support for composite actions in `--bind`. Multiple actions can be
|
||||
chained using `+` separator.
|
||||
- e.g. `fzf --bind 'ctrl-y:execute(echo -n {} | pbcopy)+abort'`
|
||||
- `--preview-window` with size 0 is allowed. This is used to make fzf execute
|
||||
preview command in the background without displaying the result.
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
0.16.1
|
||||
------
|
||||
- Fixed `--height` option to properly fill the window with the background
|
||||
color
|
||||
- Added `half-page-up` and `half-page-down` actions
|
||||
- Added `-L` flag to the default find command
|
||||
|
||||
0.16.0
|
||||
------
|
||||
- *Added `--height HEIGHT[%]` option*
|
||||
- fzf can now display finder without occupying the full screen
|
||||
- Preview window will truncate long lines by default. Line wrap can be enabled
|
||||
by `:wrap` flag in `--preview-window`.
|
||||
- Latin script letters will be normalized before matching so that it's easier
|
||||
to match against accented letters. e.g. `sodanco` can match `Só Danço Samba`.
|
||||
- Normalization can be disabled via `--literal`
|
||||
- Added `--filepath-word` to make word-wise movements/actions (`alt-b`,
|
||||
`alt-f`, `alt-bs`, `alt-d`) respect path separators
|
||||
|
||||
0.15.9
|
||||
------
|
||||
- Fixed rendering glitches introduced in 0.15.8
|
||||
- The default escape delay is reduced to 50ms and is configurable via
|
||||
`$ESCDELAY`
|
||||
- Scroll indicator at the top-right corner of the preview window is always
|
||||
displayed when there's overflow
|
||||
- Can now be built with ncurses 6 or tcell to support extra features
|
||||
- *ncurses 6*
|
||||
- Supports more than 256 color pairs
|
||||
- Supports italics
|
||||
- *tcell*
|
||||
- 24-bit color support
|
||||
- See https://github.com/junegunn/fzf/blob/master/BUILD.md
|
||||
|
||||
0.15.8
|
||||
------
|
||||
- Updated ANSI processor to handle more VT-100 escape sequences
|
||||
- Added `--no-bold` (and `--bold`) option
|
||||
- Improved escape sequence processing for WSL
|
||||
- Added support for `alt-[0-9]`, `f11`, and `f12` for `--bind` and `--expect`
|
||||
|
||||
0.15.7
|
||||
------
|
||||
- Fixed panic when color is disabled and header lines contain ANSI colors
|
||||
|
||||
0.15.6
|
||||
------
|
||||
- Windows binaries! (@kelleyma49)
|
||||
- Fixed the bug where header lines are cleared when preview window is toggled
|
||||
- Fixed not to display ^N and ^O on screen
|
||||
- Fixed cursor keys (or any key sequence that starts with ESC) on WSL by
|
||||
making fzf wait for additional keystrokes after ESC for up to 100ms
|
||||
|
||||
0.15.5
|
||||
------
|
||||
- Setting foreground color will no longer set background color to black
|
||||
|
117
README.md
117
README.md
@@ -72,7 +72,17 @@ But it's recommended that you use a plugin manager like
|
||||
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' }
|
||||
```
|
||||
|
||||
### Upgrading 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/
|
||||
|
||||
Upgrading fzf
|
||||
-------------
|
||||
|
||||
fzf is being actively developed and you might want to upgrade it once in a
|
||||
while. Please follow the instruction below depending on the installation
|
||||
@@ -82,6 +92,11 @@ method used.
|
||||
- brew: `brew update; brew reinstall fzf`
|
||||
- vim-plug: `:PlugUpdate fzf`
|
||||
|
||||
Building fzf
|
||||
------------
|
||||
|
||||
See [BUILD.md](BUILD.md).
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
@@ -102,13 +117,36 @@ 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
|
||||
- Mouse: scroll, click, double-click; shift-click and shift-scroll on
|
||||
multi-select mode
|
||||
|
||||
#### Layout
|
||||
|
||||
fzf by default starts in fullscreen mode, but you can make it start below the
|
||||
cursor with `--height` option.
|
||||
|
||||
```sh
|
||||
vim $(fzf --height 40%)
|
||||
```
|
||||
|
||||
Also check out `--reverse` option if you prefer "top-down" layout instead of
|
||||
the default "bottom-up" layout.
|
||||
|
||||
```sh
|
||||
vim $(fzf --height 40% --reverse)
|
||||
```
|
||||
|
||||
You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by
|
||||
default. For example,
|
||||
|
||||
```sh
|
||||
export FZF_DEFAULT_OPTS='--height 40% --reverse --border'
|
||||
```
|
||||
|
||||
#### Search syntax
|
||||
|
||||
Unless otherwise specified, fzf starts in "extended-search mode" where you can
|
||||
@@ -145,6 +183,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
|
||||
--------
|
||||
|
||||
@@ -171,6 +213,13 @@ cat /usr/share/dict/words | fzf-tmux -l 20% --multi --reverse
|
||||
It will still work even when you're not on tmux, silently ignoring `-[udlr]`
|
||||
options, so you can invariably use `fzf-tmux` in your scripts.
|
||||
|
||||
Alternatively, you can use `--height HEIGHT[%]` option not to start fzf in
|
||||
fullscreen mode.
|
||||
|
||||
```sh
|
||||
fzf --height 40%
|
||||
```
|
||||
|
||||
Key bindings for command line
|
||||
-----------------------------
|
||||
|
||||
@@ -188,14 +237,16 @@ fish.
|
||||
- Set `FZF_ALT_C_COMMAND` to override the default command
|
||||
- Set `FZF_ALT_C_OPTS` to pass additional options
|
||||
|
||||
If you're on a tmux session, fzf will start in a split pane. You may disable
|
||||
this tmux integration by setting `FZF_TMUX` to 0, or change the height of the
|
||||
pane with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`).
|
||||
If you're on a tmux session, you can start fzf in a split pane by setting
|
||||
`FZF_TMUX` to 1, and change the height of the pane with `FZF_TMUX_HEIGHT`
|
||||
(e.g. `20`, `50%`).
|
||||
|
||||
If you use vi mode on bash, you need to add `set -o vi` *before* `source
|
||||
~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi
|
||||
mode.
|
||||
|
||||
More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Configuring-shell-key-bindings).
|
||||
|
||||
Fuzzy completion for bash and zsh
|
||||
---------------------------------
|
||||
|
||||
@@ -307,7 +358,7 @@ If you have set up fzf for Vim, `:FZF` command will be added.
|
||||
" With options
|
||||
:FZF --no-sort --reverse --inline-info /tmp
|
||||
|
||||
" Bang version starts in fullscreen instead of using tmux pane or Neovim split
|
||||
" Bang version starts fzf in fullscreen mode
|
||||
:FZF!
|
||||
```
|
||||
|
||||
@@ -358,20 +409,6 @@ command! -bang MyStuff
|
||||
Tips
|
||||
----
|
||||
|
||||
#### Rendering issues
|
||||
|
||||
If you have any rendering issues, check the following:
|
||||
|
||||
1. Make sure `$TERM` is correctly set. fzf will use 256-color only if it
|
||||
contains `256` (e.g. `xterm-256color`)
|
||||
2. If you're on screen or tmux, `$TERM` should be either `screen` or
|
||||
`screen-256color`
|
||||
3. Some terminal emulators (e.g. mintty) have problem displaying default
|
||||
background color and make some text unable to read. In that case, try
|
||||
`--black` option. And if it solves your problem, I recommend including it
|
||||
in `FZF_DEFAULT_OPTS` for further convenience.
|
||||
4. If you still have problem, try `--no-256` option or even `--no-color`.
|
||||
|
||||
#### Respecting `.gitignore`, `.hgignore`, and `svn:ignore`
|
||||
|
||||
[ag](https://github.com/ggreer/the_silver_searcher) or
|
||||
@@ -414,19 +451,41 @@ export FZF_DEFAULT_COMMAND='
|
||||
|
||||
It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362)
|
||||
that it doesn't allow reading from STDIN in command substitution, which means
|
||||
simple `vim (fzf)` won't work as expected. The workaround is to store the result
|
||||
of fzf to a temporary file.
|
||||
simple `vim (fzf)` won't work as expected. The workaround is to use the `read`
|
||||
fish command:
|
||||
|
||||
```sh
|
||||
fzf > $TMPDIR/fzf.result; and vim (cat $TMPDIR/fzf.result)
|
||||
fzf | read -l result; and vim $result
|
||||
```
|
||||
|
||||
License
|
||||
-------
|
||||
or, for multiple results:
|
||||
|
||||
[MIT](LICENSE)
|
||||
```sh
|
||||
fzf -m | while read -l r; set result $result $r; end; and vim $result
|
||||
```
|
||||
|
||||
Author
|
||||
------
|
||||
The globbing system is different in fish and thus `**` completion will not work.
|
||||
However, the `CTRL-T` command will use the last token on the commandline as the
|
||||
root folder for the recursive search. For instance, hitting `CTRL-T` at the end
|
||||
of the following commandline
|
||||
|
||||
Junegunn Choi
|
||||
```sh
|
||||
ls /var/
|
||||
```
|
||||
|
||||
will list all files and folders under `/var/`.
|
||||
|
||||
When using a custom `FZF_CTRL_T_COMMAND`, use the unexpanded `$dir` variable to
|
||||
make use of this feature. `$dir` defaults to `.` when the last token is not a
|
||||
valid directory. Example:
|
||||
|
||||
```sh
|
||||
set -l FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'"
|
||||
```
|
||||
|
||||
[License](LICENSE)
|
||||
------------------
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Junegunn Choi
|
||||
|
@@ -114,6 +114,9 @@ if [[ -z "$TMUX" || "$opt" =~ ^-h && "$columns" -le 40 || ! "$opt" =~ ^-h && "$l
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# --height option is not allowed
|
||||
args+=("--no-height")
|
||||
|
||||
# Handle zoomed tmux pane by moving it to a temp window
|
||||
if tmux list-panes -F '#F' | grep -q Z; then
|
||||
zoomed=1
|
||||
|
53
install
53
install
@@ -2,9 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
[[ "$@" =~ --pre ]] && version=0.15.5 pre=1 ||
|
||||
version=0.15.5 pre=0
|
||||
|
||||
version=0.16.4
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
@@ -48,7 +46,7 @@ for opt in "$@"; do
|
||||
--no-update-rc) update_config=0 ;;
|
||||
--32) binary_arch=386 ;;
|
||||
--64) binary_arch=amd64 ;;
|
||||
--bin|--pre) ;;
|
||||
--bin) ;;
|
||||
*)
|
||||
echo "unknown option: $opt"
|
||||
help
|
||||
@@ -90,17 +88,6 @@ check_binary() {
|
||||
return 1
|
||||
}
|
||||
|
||||
symlink() {
|
||||
echo " - Creating symlink: bin/$1 -> bin/fzf"
|
||||
(cd "$fzf_base"/bin &&
|
||||
rm -f fzf &&
|
||||
ln -sf $1 fzf)
|
||||
if [ $? -ne 0 ]; then
|
||||
binary_error="Failed to create symlink"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
link_fzf_in_path() {
|
||||
if which_fzf="$(command -v fzf)"; then
|
||||
echo " - Found in \$PATH"
|
||||
@@ -121,14 +108,11 @@ try_wget() {
|
||||
|
||||
download() {
|
||||
echo "Downloading bin/fzf ..."
|
||||
if [ $pre = 0 ]; then
|
||||
if [[ ! "$version" =~ alpha ]]; then
|
||||
if [ -x "$fzf_base"/bin/fzf ]; then
|
||||
echo " - Already exists"
|
||||
check_binary && return
|
||||
fi
|
||||
if [ -x "$fzf_base"/bin/$1 ]; then
|
||||
symlink $1 && check_binary && return
|
||||
fi
|
||||
link_fzf_in_path && return
|
||||
fi
|
||||
mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin
|
||||
@@ -137,7 +121,10 @@ download() {
|
||||
return
|
||||
fi
|
||||
|
||||
local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz
|
||||
local url
|
||||
[[ "$version" =~ alpha ]] &&
|
||||
url=https://github.com/junegunn/fzf-bin/releases/download/alpha/${1}.tgz ||
|
||||
url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz
|
||||
set -o pipefail
|
||||
if ! (try_curl $url || try_wget $url); then
|
||||
set +o pipefail
|
||||
@@ -146,12 +133,12 @@ download() {
|
||||
fi
|
||||
set +o pipefail
|
||||
|
||||
if [ ! -f $1 ]; then
|
||||
if [ ! -f fzf ]; then
|
||||
binary_error="Failed to download ${1}"
|
||||
return
|
||||
fi
|
||||
|
||||
chmod +x $1 && symlink $1 && check_binary
|
||||
chmod +x fzf && check_binary
|
||||
}
|
||||
|
||||
# Try to download binary executable
|
||||
@@ -159,10 +146,18 @@ archi=$(uname -sm)
|
||||
binary_available=1
|
||||
binary_error=""
|
||||
case "$archi" in
|
||||
Darwin\ x86_64) download fzf-$version-darwin_${binary_arch:-amd64} ;;
|
||||
Darwin\ i*86) download fzf-$version-darwin_${binary_arch:-386} ;;
|
||||
Linux\ x86_64) download fzf-$version-linux_${binary_arch:-amd64} ;;
|
||||
Linux\ i*86) download fzf-$version-linux_${binary_arch:-386} ;;
|
||||
Darwin\ *64) download fzf-$version-darwin_${binary_arch:-amd64} ;;
|
||||
Darwin\ *86) download fzf-$version-darwin_${binary_arch:-386} ;;
|
||||
Linux\ *64) download fzf-$version-linux_${binary_arch:-amd64} ;;
|
||||
Linux\ *86) download fzf-$version-linux_${binary_arch:-386} ;;
|
||||
Linux\ armv5*) download fzf-$version-linux_${binary_arch:-arm5} ;;
|
||||
Linux\ armv6*) download fzf-$version-linux_${binary_arch:-arm6} ;;
|
||||
Linux\ armv7*) download fzf-$version-linux_${binary_arch:-arm7} ;;
|
||||
Linux\ armv8*) download fzf-$version-linux_${binary_arch:-arm8} ;;
|
||||
FreeBSD\ *64) download fzf-$version-freebsd_${binary_arch:-amd64} ;;
|
||||
FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386} ;;
|
||||
OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64} ;;
|
||||
OpenBSD\ *86) download fzf-$version-openbsd_${binary_arch:-386} ;;
|
||||
*) binary_available=0 binary_error=1 ;;
|
||||
esac
|
||||
|
||||
@@ -301,12 +296,6 @@ if [[ ! "\$PATH" == *$fzf_base/bin* ]]; then
|
||||
export PATH="\$PATH:$fzf_base/bin"
|
||||
fi
|
||||
|
||||
# Man path
|
||||
# --------
|
||||
if [[ ! "\$MANPATH" == *$fzf_base/man* && -d "$fzf_base/man" ]]; then
|
||||
export MANPATH="\$MANPATH:$fzf_base/man"
|
||||
fi
|
||||
|
||||
# Auto-completion
|
||||
# ---------------
|
||||
$fzf_completion
|
||||
|
@@ -1,7 +1,7 @@
|
||||
.ig
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Junegunn Choi
|
||||
Copyright (c) 2017 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -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 "Oct 2016" "fzf 0.15.5" "fzf-tmux - open fzf in tmux split pane"
|
||||
.TH fzf-tmux 1 "Feb 2017" "fzf 0.16.4" "fzf-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf-tmux - open fzf in tmux split pane
|
||||
|
@@ -1,7 +1,7 @@
|
||||
.ig
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Junegunn Choi
|
||||
Copyright (c) 2017 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -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 "Oct 2016" "fzf 0.15.5" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "Feb 2017" "fzf 0.16.4" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -48,6 +48,9 @@ Case-insensitive match (default: smart-case match)
|
||||
.B "+i"
|
||||
Case-sensitive match
|
||||
.TP
|
||||
.B "--literal"
|
||||
Do not normalize latin script letters for matching.
|
||||
.TP
|
||||
.BI "--algo=" TYPE
|
||||
Fuzzy matching algorithm (default: v2)
|
||||
|
||||
@@ -126,13 +129,36 @@ Number of screen columns to keep to the right of the highlighted substring
|
||||
(default: 10). Setting it to a large value will cause the text to be positioned
|
||||
on the center of the screen.
|
||||
.TP
|
||||
.B "--filepath-word"
|
||||
Make word-wise movements and actions respect path separators. The following
|
||||
actions are affected:
|
||||
|
||||
\fBbackward-kill-word\fR
|
||||
.br
|
||||
\fBbackward-word\fR
|
||||
.br
|
||||
\fBforward-word\fR
|
||||
.br
|
||||
\fBkill-word\fR
|
||||
.TP
|
||||
.BI "--jump-labels=" "CHARS"
|
||||
Label characters for \fBjump\fR and \fBjump-accept\fR
|
||||
.SS Layout
|
||||
.TP
|
||||
.BI "--height=" "HEIGHT[%]"
|
||||
Display fzf window below the cursor with the given height instead of using
|
||||
the full screen.
|
||||
.TP
|
||||
.BI "--min-height=" "HEIGHT"
|
||||
Minimum height when \fB--height\fR is given in percent (default: 10).
|
||||
Ignored when \fB--height\fR is not specified.
|
||||
.TP
|
||||
.B "--reverse"
|
||||
Reverse orientation
|
||||
.TP
|
||||
.B "--border"
|
||||
Draw border above and below the finder
|
||||
.TP
|
||||
.BI "--margin=" MARGIN
|
||||
Comma-separated expression for margins around the finder.
|
||||
.br
|
||||
@@ -185,7 +211,8 @@ Number of spaces for a tab character (default: 8)
|
||||
.BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]"
|
||||
Color configuration. The name of the base color scheme is followed by custom
|
||||
color mappings. Ansi color code of -1 denotes terminal default
|
||||
foreground/background color.
|
||||
foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR
|
||||
format.
|
||||
|
||||
.RS
|
||||
e.g. \fBfzf --color=bg+:24\fR
|
||||
@@ -216,6 +243,9 @@ e.g. \fBfzf --color=bg+:24\fR
|
||||
\fBheader \fRHeader
|
||||
.RE
|
||||
.TP
|
||||
.B "--no-bold"
|
||||
Do not use bold text
|
||||
.TP
|
||||
.B "--black"
|
||||
Use black background
|
||||
.SS History
|
||||
@@ -235,20 +265,32 @@ Execute the given command for the current line and display the result on the
|
||||
preview window. \fB{}\fR in the command is the placeholder that is replaced to
|
||||
the single-quoted string of the current line. To transform the replacement
|
||||
string, specify field index expressions between the braces (See \fBFIELD INDEX
|
||||
EXPRESSION\fR for the details). Also, \fB{q}\fR is replaced to the current
|
||||
query string.
|
||||
EXPRESSION\fR for the details).
|
||||
|
||||
.RS
|
||||
e.g. \fBfzf --preview="head -$LINES {}"\fR
|
||||
\fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
|
||||
|
||||
A placeholder expression starting with \fB+\fR flag will be replaced to the
|
||||
space-separated list of the selected lines (or the current line if no selection
|
||||
was made) individually quoted.
|
||||
|
||||
e.g. \fBfzf --multi --preview="head -10 {+}"\fR
|
||||
\fBgit log --oneline | fzf --multi --preview 'git show {+1}'\fR
|
||||
|
||||
Also, \fB{q}\fR is replaced to the current query string.
|
||||
|
||||
Note that you can escape a placeholder pattern by prepending a backslash.
|
||||
.RE
|
||||
.TP
|
||||
.BI "--preview-window=" "[POSITION][:SIZE[%]][:hidden]"
|
||||
.BI "--preview-window=" "[POSITION][:SIZE[%]][:wrap][:hidden]"
|
||||
Determine the layout of the preview window. If the argument ends with
|
||||
\fB:hidden\fR, the preview window will be hidden by default until
|
||||
\fBtoggle-preview\fR action is triggered.
|
||||
\fBtoggle-preview\fR action is triggered. Long lines are truncated by default.
|
||||
Line wrap can be enabled with \fB:wrap\fR flag.
|
||||
|
||||
If size is given as 0, preview window will not be visible, but fzf will still
|
||||
execute the command in the background.
|
||||
|
||||
.RS
|
||||
.B POSITION: (default: right)
|
||||
@@ -292,10 +334,10 @@ e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "--read0"
|
||||
Read input delimited by ASCII NUL character instead of newline character
|
||||
Read input delimited by ASCII NUL characters instead of newline characters
|
||||
.TP
|
||||
.B "--print0"
|
||||
Print output delimited by ASCII NUL character instead of newline character
|
||||
Print output delimited by ASCII NUL characters instead of newline characters
|
||||
.TP
|
||||
.B "--sync"
|
||||
Synchronous search for multi-staged filtering. If specified, fzf will launch
|
||||
@@ -387,8 +429,10 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
|
||||
|
||||
.B AVAILABLE KEYS: (SYNONYMS)
|
||||
\fIctrl-[a-z]\fR
|
||||
\fIctrl-space\fR
|
||||
\fIalt-[a-z]\fR
|
||||
\fIf[1-10]\fR
|
||||
\fIalt-[0-9]\fR
|
||||
\fIf[1-12]\fR
|
||||
\fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
|
||||
\fIspace\fR
|
||||
\fIbspace\fR (\fIbs\fR)
|
||||
@@ -429,7 +473,8 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
|
||||
\fBdown\fR \fIctrl-j ctrl-n down\fR
|
||||
\fBend-of-line\fR \fIctrl-e end\fR
|
||||
\fBexecute(...)\fR (see below for the details)
|
||||
\fBexecute-multi(...)\fR (see below for the details)
|
||||
\fBexecute-silent(...)\fR (see below for the details)
|
||||
\fRexecute-multi(...)\fR (deprecated in favor of \fB{+}\fR expression)
|
||||
\fBforward-char\fR \fIctrl-f right\fR
|
||||
\fBforward-word\fR \fIalt-f shift-right\fR
|
||||
\fBignore\fR
|
||||
@@ -440,6 +485,8 @@ 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
|
||||
\fBhalf-page-down\fR
|
||||
\fBhalf-page-up\fR
|
||||
\fBpreview-down\fR
|
||||
\fBpreview-up\fR
|
||||
\fBpreview-page-down\fR
|
||||
@@ -449,17 +496,21 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
|
||||
\fBselect-all\fR
|
||||
\fBtoggle\fR
|
||||
\fBtoggle-all\fR
|
||||
\fBtoggle-down\fR \fIctrl-i (tab)\fR
|
||||
\fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle-up\fR : \fBtoggle-down\fR)
|
||||
\fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle-down\fR : \fBtoggle-up\fR)
|
||||
\fBtoggle+down\fR \fIctrl-i (tab)\fR
|
||||
\fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
|
||||
\fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
|
||||
\fBtoggle-preview\fR
|
||||
\fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR)
|
||||
\fBtoggle-up\fR \fIbtab (shift-tab)\fR
|
||||
\fBtoggle-sort\fR
|
||||
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
|
||||
\fBunix-line-discard\fR \fIctrl-u\fR
|
||||
\fBunix-word-rubout\fR \fIctrl-w\fR
|
||||
\fBup\fR \fIctrl-k ctrl-p up\fR
|
||||
\fByank\fR \fIctrl-y\fR
|
||||
|
||||
Multiple actions can be chained using \fB+\fR separator.
|
||||
|
||||
\fBfzf --bind 'ctrl-a:select-all+accept'\fR
|
||||
|
||||
With \fBexecute(...)\fR action, you can execute arbitrary commands without
|
||||
leaving fzf. For example, you can turn fzf into a simple file browser by
|
||||
binding \fBenter\fR key to \fBless\fR command like follows.
|
||||
@@ -492,10 +543,12 @@ the closing character. The catch is that it should be the last one in the
|
||||
comma-separated list of key-action pairs.
|
||||
.RE
|
||||
|
||||
\fBexecute-multi(...)\fR is an alternative action that executes the command
|
||||
with the selected entries when multi-select is enabled (\fB--multi\fR). With
|
||||
this action, \fB{}\fR is replaced with the quoted strings of the selected
|
||||
entries separated by spaces.
|
||||
fzf switches to the alternate screen when executing a command. However, if the
|
||||
command is expected to complete quickly, and you are not interested in its
|
||||
output, you might want to use \fBexecute-silent\fR instead, which silently
|
||||
executes the command without the switching. Note that fzf will not be
|
||||
responsible until the command is complete. For asynchronous execution, start
|
||||
your command as a background process (i.e. appending \fB&\fR).
|
||||
|
||||
.SH AUTHOR
|
||||
Junegunn Choi (\fIjunegunn.c@gmail.com\fR)
|
||||
|
136
plugin/fzf.vim
136
plugin/fzf.vim
@@ -1,4 +1,4 @@
|
||||
" Copyright (c) 2016 Junegunn Choi
|
||||
" Copyright (c) 2017 Junegunn Choi
|
||||
"
|
||||
" MIT License
|
||||
"
|
||||
@@ -28,10 +28,12 @@ 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'
|
||||
let s:install = expand('<sfile>:h:h').'/install'
|
||||
let s:is_win = has('win32') || has('win64')
|
||||
let s:base_dir = expand('<sfile>:h:h')
|
||||
let s:fzf_go = s:base_dir.'/bin/fzf'
|
||||
let s:fzf_tmux = s:base_dir.'/bin/fzf-tmux'
|
||||
let s:install = s:base_dir.'/install'
|
||||
let s:installed = 0
|
||||
let s:fzf_tmux = expand('<sfile>:h:h').'/bin/fzf-tmux'
|
||||
|
||||
let s:cpo_save = &cpo
|
||||
set cpo&vim
|
||||
@@ -42,6 +44,11 @@ function! s:fzf_exec()
|
||||
let s:exec = s:fzf_go
|
||||
elseif executable('fzf')
|
||||
let s:exec = 'fzf'
|
||||
elseif s:is_win
|
||||
call s:warn('fzf executable not found.')
|
||||
call s:warn('Download fzf binary for Windows from https://github.com/junegunn/fzf-bin/releases/')
|
||||
call s:warn('and place it as '.s:base_dir.'\bin\fzf.exe')
|
||||
throw 'fzf executable not found'
|
||||
elseif !s:installed && executable(s:install) &&
|
||||
\ input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
|
||||
redraw
|
||||
@@ -80,7 +87,13 @@ function! s:shellesc(arg)
|
||||
endfunction
|
||||
|
||||
function! s:escape(path)
|
||||
return escape(a:path, ' $%#''"\')
|
||||
let escaped_chars = '$%#''"'
|
||||
|
||||
if has('unix')
|
||||
let escaped_chars .= ' \'
|
||||
endif
|
||||
|
||||
return escape(a:path, escaped_chars)
|
||||
endfunction
|
||||
|
||||
" Upgrade legacy options
|
||||
@@ -149,7 +162,8 @@ function! s:common_sink(action, lines) abort
|
||||
else
|
||||
call s:open(cmd, item)
|
||||
endif
|
||||
if exists('#BufEnter') && isdirectory(item)
|
||||
if !has('patch-8.0.0177') && !has('nvim-0.2') && exists('#BufEnter')
|
||||
\ && isdirectory(item)
|
||||
doautocmd BufEnter
|
||||
endif
|
||||
endfor
|
||||
@@ -160,9 +174,12 @@ function! s:common_sink(action, lines) abort
|
||||
endfunction
|
||||
|
||||
function! s:get_color(attr, ...)
|
||||
let gui = has('termguicolors') && &termguicolors
|
||||
let fam = gui ? 'gui' : 'cterm'
|
||||
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
|
||||
for group in a:000
|
||||
let code = synIDattr(synIDtrans(hlID(group)), a:attr, 'cterm')
|
||||
if code =~ '^[0-9]\+$'
|
||||
let code = synIDattr(synIDtrans(hlID(group)), a:attr, fam)
|
||||
if code =~? pat
|
||||
return code
|
||||
endif
|
||||
endfor
|
||||
@@ -191,6 +208,10 @@ function! fzf#wrap(...)
|
||||
endfor
|
||||
let [name, opts, bang] = args
|
||||
|
||||
if len(name)
|
||||
let opts.name = name
|
||||
end
|
||||
|
||||
" Layout: g:fzf_layout (and deprecated g:fzf_height)
|
||||
if bang
|
||||
for key in s:layout_keys
|
||||
@@ -231,10 +252,31 @@ function! fzf#wrap(...)
|
||||
return opts
|
||||
endfunction
|
||||
|
||||
function! fzf#shellescape(path)
|
||||
if s:is_win
|
||||
let shellslash = &shellslash
|
||||
try
|
||||
set noshellslash
|
||||
return shellescape(a:path)
|
||||
finally
|
||||
let &shellslash = shellslash
|
||||
endtry
|
||||
endif
|
||||
return shellescape(a:path)
|
||||
endfunction
|
||||
|
||||
function! fzf#run(...) abort
|
||||
try
|
||||
let oshell = &shell
|
||||
set shell=sh
|
||||
let useshellslash = &shellslash
|
||||
|
||||
if s:is_win
|
||||
set shell=cmd.exe
|
||||
set noshellslash
|
||||
else
|
||||
set shell=sh
|
||||
endif
|
||||
|
||||
if has('nvim') && len(filter(range(1, bufnr('$')), 'bufname(v:val) =~# ";#FZF"'))
|
||||
call s:warn('FZF is already running!')
|
||||
return []
|
||||
@@ -251,7 +293,7 @@ try
|
||||
if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND)
|
||||
let temps.source = tempname()
|
||||
call writefile(split($FZF_DEFAULT_COMMAND, "\n"), temps.source)
|
||||
let dict.source = (empty($SHELL) ? 'sh' : $SHELL) . ' ' . s:shellesc(temps.source)
|
||||
let dict.source = (empty($SHELL) ? &shell : $SHELL) . ' ' . s:shellesc(temps.source)
|
||||
endif
|
||||
|
||||
if has_key(dict, 'source')
|
||||
@@ -269,18 +311,35 @@ try
|
||||
else
|
||||
let prefix = ''
|
||||
endif
|
||||
let tmux = (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict)
|
||||
let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
|
||||
|
||||
if has('nvim') && !tmux
|
||||
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0)
|
||||
let use_height = has_key(dict, 'down') &&
|
||||
\ !(has('nvim') || s:is_win || s:present(dict, 'up', 'left', 'right')) &&
|
||||
\ executable('tput') && filereadable('/dev/tty')
|
||||
let use_term = has('nvim')
|
||||
let use_tmux = (!use_height && !use_term || prefer_tmux) && s:tmux_enabled() && s:splittable(dict)
|
||||
if prefer_tmux && use_tmux
|
||||
let use_height = 0
|
||||
let use_term = 0
|
||||
endif
|
||||
if use_height
|
||||
let optstr .= ' --height='.s:calc_size(&lines, dict.down, dict)
|
||||
elseif use_term
|
||||
let optstr .= ' --no-height'
|
||||
endif
|
||||
let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
|
||||
|
||||
if use_term
|
||||
return s:execute_term(dict, command, temps)
|
||||
endif
|
||||
|
||||
let lines = tmux ? s:execute_tmux(dict, command, temps) : s:execute(dict, command, temps)
|
||||
let lines = use_tmux ? s:execute_tmux(dict, command, temps)
|
||||
\ : s:execute(dict, command, use_height, temps)
|
||||
call s:callback(dict, lines)
|
||||
return lines
|
||||
finally
|
||||
let &shell = oshell
|
||||
let &shellslash = useshellslash
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
@@ -353,7 +412,11 @@ function! s:xterm_launcher()
|
||||
\ &columns, &lines/2, getwinposx(), getwinposy())
|
||||
endfunction
|
||||
unlet! s:launcher
|
||||
let s:launcher = function('s:xterm_launcher')
|
||||
if s:is_win
|
||||
let s:launcher = '%s'
|
||||
else
|
||||
let s:launcher = function('s:xterm_launcher')
|
||||
endif
|
||||
|
||||
function! s:exit_handler(code, command, ...)
|
||||
if a:code == 130
|
||||
@@ -368,18 +431,28 @@ function! s:exit_handler(code, command, ...)
|
||||
return 1
|
||||
endfunction
|
||||
|
||||
function! s:execute(dict, command, temps) abort
|
||||
function! s:execute(dict, command, use_height, temps) abort
|
||||
call s:pushd(a:dict)
|
||||
silent! !clear 2> /dev/null
|
||||
if has('unix') && !a:use_height
|
||||
silent! !clear 2> /dev/null
|
||||
endif
|
||||
let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#')
|
||||
if has('gui_running')
|
||||
let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher)))
|
||||
let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher
|
||||
let command = printf(fmt, "'".substitute(escaped, "'", "'\"'\"'", 'g')."'")
|
||||
if has('unix')
|
||||
let escaped = "'".substitute(escaped, "'", "'\"'\"'", 'g')."'"
|
||||
endif
|
||||
let command = printf(fmt, escaped)
|
||||
else
|
||||
let command = escaped
|
||||
endif
|
||||
execute 'silent !'.command
|
||||
if a:use_height
|
||||
let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty'
|
||||
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin))
|
||||
else
|
||||
execute 'silent !'.command
|
||||
endif
|
||||
let exit_status = v:shell_error
|
||||
redraw!
|
||||
return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
|
||||
@@ -430,7 +503,7 @@ function! s:split(dict)
|
||||
let ppos = s:getpos()
|
||||
try
|
||||
if s:present(a:dict, 'window')
|
||||
execute a:dict.window
|
||||
execute 'keepalt' a:dict.window
|
||||
elseif !s:splittable(a:dict)
|
||||
execute (tabpagenr()-1).'tabnew'
|
||||
else
|
||||
@@ -457,22 +530,24 @@ endfunction
|
||||
|
||||
function! s:execute_term(dict, command, temps) abort
|
||||
let winrest = winrestcmd()
|
||||
let pbuf = bufnr('')
|
||||
let [ppos, winopts] = s:split(a:dict)
|
||||
let fzf = { 'buf': bufnr('%'), 'ppos': ppos, 'dict': a:dict, 'temps': a:temps,
|
||||
let b:fzf = a:dict
|
||||
let fzf = { 'buf': bufnr(''), 'pbuf': pbuf, 'ppos': ppos, 'dict': a:dict, 'temps': a:temps,
|
||||
\ 'winopts': winopts, 'winrest': winrest, 'lines': &lines,
|
||||
\ 'columns': &columns, 'command': a:command }
|
||||
function! fzf.switch_back(inplace)
|
||||
if a:inplace && bufnr('') == self.buf
|
||||
" FIXME: Can't re-enter normal mode from terminal mode
|
||||
" execute "normal! \<c-^>"
|
||||
b #
|
||||
if bufexists(self.pbuf)
|
||||
execute 'keepalt b' self.pbuf
|
||||
endif
|
||||
" No other listed buffer
|
||||
if bufnr('') == self.buf
|
||||
enew
|
||||
endif
|
||||
endif
|
||||
endfunction
|
||||
function! fzf.on_exit(id, code)
|
||||
function! fzf.on_exit(id, code, _event)
|
||||
if s:getpos() == self.ppos " {'window': 'enew'}
|
||||
for [opt, val] in items(self.winopts)
|
||||
execute 'let' opt '=' val
|
||||
@@ -581,14 +656,19 @@ let s:default_action = {
|
||||
\ 'ctrl-x': 'split',
|
||||
\ 'ctrl-v': 'vsplit' }
|
||||
|
||||
function! s:shortpath()
|
||||
let short = pathshorten(fnamemodify(getcwd(), ':~:.'))
|
||||
return empty(short) ? '~/' : short . (short =~ '/$' ? '' : '/')
|
||||
endfunction
|
||||
|
||||
function! s:cmd(bang, ...) abort
|
||||
let args = copy(a:000)
|
||||
let opts = { 'options': '--multi ' }
|
||||
if len(args) && isdirectory(expand(args[-1]))
|
||||
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '/*$', '/', '')
|
||||
let opts.options .= ' --prompt '.shellescape(opts.dir)
|
||||
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
|
||||
let opts.options .= ' --prompt '.fzf#shellescape(opts.dir)
|
||||
else
|
||||
let opts.options .= ' --prompt '.shellescape(pathshorten(getcwd()).'/')
|
||||
let opts.options .= ' --prompt '.fzf#shellescape(s:shortpath())
|
||||
endif
|
||||
let opts.options .= ' '.join(args)
|
||||
call fzf#run(fzf#wrap('FZF', opts, a:bang))
|
||||
|
@@ -5,7 +5,7 @@
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/-completion.bash
|
||||
#
|
||||
# - $FZF_TMUX (default: 1)
|
||||
# - $FZF_TMUX (default: 0)
|
||||
# - $FZF_TMUX_HEIGHT (default: '40%')
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
@@ -30,6 +30,14 @@ fi
|
||||
|
||||
###########################################################
|
||||
|
||||
# To redraw line after fzf closes (printf '\e[5n')
|
||||
bind '"\e[0n": redraw-current-line'
|
||||
|
||||
__fzfcmd_complete() {
|
||||
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] &&
|
||||
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
|
||||
}
|
||||
|
||||
_fzf_orig_completion_filter() {
|
||||
sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' |
|
||||
awk -F= '{gsub(/[^A-Za-z0-9_= ;]/, "_", $1); print $1"="$2}'
|
||||
@@ -43,35 +51,43 @@ _fzf_opts_completion() {
|
||||
opts="
|
||||
-x --extended
|
||||
-e --exact
|
||||
--algo
|
||||
-i +i
|
||||
-n --nth
|
||||
--with-nth
|
||||
-d --delimiter
|
||||
+s --no-sort
|
||||
--tac
|
||||
--tiebreak
|
||||
--bind
|
||||
-m --multi
|
||||
--no-mouse
|
||||
--color
|
||||
--black
|
||||
--reverse
|
||||
--bind
|
||||
--cycle
|
||||
--no-hscroll
|
||||
--jump-labels
|
||||
--height
|
||||
--literal
|
||||
--reverse
|
||||
--margin
|
||||
--inline-info
|
||||
--prompt
|
||||
--header
|
||||
--header-lines
|
||||
--ansi
|
||||
--tabstop
|
||||
--color
|
||||
--no-bold
|
||||
--history
|
||||
--history-size
|
||||
--preview
|
||||
--preview-window
|
||||
-q --query
|
||||
-1 --select-1
|
||||
-0 --exit-0
|
||||
-f --filter
|
||||
--print-query
|
||||
--expect
|
||||
--toggle-sort
|
||||
--sync
|
||||
--cycle
|
||||
--history
|
||||
--history-size
|
||||
--header
|
||||
--header-lines
|
||||
--margin"
|
||||
--sync"
|
||||
|
||||
case "${prev}" in
|
||||
--tiebreak)
|
||||
@@ -116,7 +132,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"
|
||||
fzf="$(__fzfcmd_complete)"
|
||||
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
||||
COMPREPLY=()
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
@@ -132,8 +148,7 @@ __fzf_generic_path_completion() {
|
||||
leftover=${leftover/#\/}
|
||||
[ -z "$dir" ] && dir='.'
|
||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
||||
tput sc
|
||||
matches=$(eval "$1 $(printf %q "$dir")" | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read -r item; do
|
||||
matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $2 -q "$leftover" | while read -r item; do
|
||||
printf "%q$3 " "$item"
|
||||
done)
|
||||
matches=${matches% }
|
||||
@@ -142,7 +157,7 @@ __fzf_generic_path_completion() {
|
||||
else
|
||||
COMPREPLY=( "$cur" )
|
||||
fi
|
||||
tput rc
|
||||
printf '\e[5n'
|
||||
return 0
|
||||
fi
|
||||
dir=$(dirname "$dir")
|
||||
@@ -160,7 +175,7 @@ _fzf_complete() {
|
||||
local cur selected trigger cmd fzf post
|
||||
post="$(caller 0 | awk '{print $2}')_post"
|
||||
type -t "$post" > /dev/null 2>&1 || post=cat
|
||||
[ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
|
||||
fzf="$(__fzfcmd_complete)"
|
||||
|
||||
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
@@ -168,10 +183,9 @@ _fzf_complete() {
|
||||
if [[ "$cur" == *"$trigger" ]]; then
|
||||
cur=${cur:0:${#cur}-${#trigger}}
|
||||
|
||||
tput sc
|
||||
selected=$(cat | $fzf $FZF_COMPLETION_OPTS $1 -q "$cur" | $post | tr '\n' ' ')
|
||||
selected=$(cat | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $1 -q "$cur" | $post | tr '\n' ' ')
|
||||
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
||||
tput rc
|
||||
printf '\e[5n'
|
||||
|
||||
if [ -n "$selected" ]; then
|
||||
COMPREPLY=("$selected")
|
||||
@@ -200,10 +214,9 @@ _fzf_complete_kill() {
|
||||
[ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1
|
||||
|
||||
local selected fzf
|
||||
[ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
|
||||
tput sc
|
||||
selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ')
|
||||
tput rc
|
||||
fzf="$(__fzfcmd_complete)"
|
||||
selected=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ')
|
||||
printf '\e[5n'
|
||||
|
||||
if [ -n "$selected" ]; then
|
||||
COMPREPLY=( "$selected" )
|
||||
@@ -221,7 +234,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 grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -oE '^[a-z0-9.,-]+' ~/.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
|
||||
)
|
||||
@@ -261,12 +274,9 @@ a_cmds="
|
||||
x_cmds="kill ssh telnet unset unalias export"
|
||||
|
||||
# Preserve existing completion
|
||||
if [ "$_fzf_completion_loaded" != '0.11.3' ]; then
|
||||
# Really wish I could use associative array but OSX comes with bash 3.2 :(
|
||||
eval $(complete | command grep '\-F' | command grep -v _fzf_ |
|
||||
command grep -E " ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter)
|
||||
export _fzf_completion_loaded=0.11.3
|
||||
fi
|
||||
eval $(complete |
|
||||
sed -E '/-F/!d; / _fzf/d; '"/ ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g; s/+/\\+/g'))$/"'!d' |
|
||||
_fzf_orig_completion_filter)
|
||||
|
||||
if type _completion_loader > /dev/null 2>&1; then
|
||||
_fzf_completion_loader=1
|
||||
|
@@ -5,7 +5,7 @@
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/-completion.zsh
|
||||
#
|
||||
# - $FZF_TMUX (default: 1)
|
||||
# - $FZF_TMUX (default: 0)
|
||||
# - $FZF_TMUX_HEIGHT (default: '40%')
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
@@ -30,6 +30,11 @@ fi
|
||||
|
||||
###########################################################
|
||||
|
||||
__fzfcmd_complete() {
|
||||
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] &&
|
||||
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
|
||||
}
|
||||
|
||||
__fzf_generic_path_completion() {
|
||||
local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches
|
||||
# (Q) flag removes a quoting level: "foo\ bar" => "foo bar"
|
||||
@@ -39,7 +44,7 @@ __fzf_generic_path_completion() {
|
||||
fzf_opts=$4
|
||||
suffix=$5
|
||||
tail=$6
|
||||
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
|
||||
fzf="$(__fzfcmd_complete)"
|
||||
|
||||
setopt localoptions nonomatch
|
||||
dir="$base"
|
||||
@@ -50,7 +55,7 @@ __fzf_generic_path_completion() {
|
||||
[ -z "$dir" ] && dir='.'
|
||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
||||
dir=${~dir}
|
||||
matches=$(eval "$compgen $(printf %q "$dir")" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do
|
||||
matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "$leftover" | while read item; do
|
||||
echo -n "${(q)item}$suffix "
|
||||
done)
|
||||
matches=${matches% }
|
||||
@@ -90,10 +95,10 @@ _fzf_complete() {
|
||||
post="${funcstack[2]}_post"
|
||||
type $post > /dev/null 2>&1 || post=cat
|
||||
|
||||
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
|
||||
fzf="$(__fzfcmd_complete)"
|
||||
|
||||
_fzf_feed_fifo "$fifo"
|
||||
matches=$(cat "$fifo" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ')
|
||||
matches=$(cat "$fifo" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ')
|
||||
if [ -n "$matches" ]; then
|
||||
LBUFFER="$lbuf$matches"
|
||||
fi
|
||||
@@ -112,7 +117,7 @@ _fzf_complete_telnet() {
|
||||
_fzf_complete_ssh() {
|
||||
_fzf_complete '+m' "$@" < <(
|
||||
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 -oE '^[a-z0-9.,-]+' ~/.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
|
||||
)
|
||||
@@ -157,8 +162,8 @@ fzf-completion() {
|
||||
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
|
||||
# Kill completion (do not require trigger sequence)
|
||||
if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then
|
||||
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
|
||||
matches=$(ps -ef | sed 1d | ${=fzf} ${=FZF_COMPLETION_OPTS} -m | awk '{print $2}' | tr '\n' ' ')
|
||||
fzf="$(__fzfcmd_complete)"
|
||||
matches=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ')
|
||||
if [ -n "$matches" ]; then
|
||||
LBUFFER="$LBUFFER$matches"
|
||||
fi
|
||||
|
@@ -1,11 +1,11 @@
|
||||
# Key bindings
|
||||
# ------------
|
||||
__fzf_select__() {
|
||||
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
|
||||
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | sed 1d | cut -b3-"}"
|
||||
eval "$cmd | fzf -m $FZF_CTRL_T_OPTS" | while read -r item; do
|
||||
-o -type l -print 2> /dev/null | cut -b3-"}"
|
||||
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" fzf -m "$@" | while read -r item; do
|
||||
printf '%q ' "$item"
|
||||
done
|
||||
echo
|
||||
@@ -13,8 +13,13 @@ __fzf_select__() {
|
||||
|
||||
if [[ $- =~ i ]]; then
|
||||
|
||||
__fzf_use_tmux__() {
|
||||
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]
|
||||
}
|
||||
|
||||
__fzfcmd() {
|
||||
[ "${FZF_TMUX:-1}" != 0 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
|
||||
__fzf_use_tmux__ &&
|
||||
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
|
||||
}
|
||||
|
||||
__fzf_select_tmux__() {
|
||||
@@ -26,7 +31,7 @@ __fzf_select_tmux__() {
|
||||
height="-l $height"
|
||||
fi
|
||||
|
||||
tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; RESULT=\"\$(__fzf_select__)\"; tmux setb -b fzf \"\$RESULT\" \\; pasteb -b fzf -t $TMUX_PANE \\; deleteb -b fzf || tmux send-keys -t $TMUX_PANE \"\$RESULT\"'"
|
||||
tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; RESULT=\"\$(__fzf_select__ --no-height)\"; tmux setb -b fzf \"\$RESULT\" \\; pasteb -b fzf -t $TMUX_PANE \\; deleteb -b fzf || tmux send-keys -t $TMUX_PANE \"\$RESULT\"'"
|
||||
}
|
||||
|
||||
fzf-file-widget() {
|
||||
@@ -41,9 +46,9 @@ fzf-file-widget() {
|
||||
|
||||
__fzf_cd__() {
|
||||
local cmd dir
|
||||
cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
|
||||
cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"}"
|
||||
dir=$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS") && printf 'cd %q' "$dir"
|
||||
dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir"
|
||||
}
|
||||
|
||||
__fzf_history__() (
|
||||
@@ -51,7 +56,7 @@ __fzf_history__() (
|
||||
shopt -u nocaseglob nocasematch
|
||||
line=$(
|
||||
HISTTIMEFORMAT= history |
|
||||
eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" |
|
||||
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" $(__fzfcmd) |
|
||||
command grep '^ *[0-9]') &&
|
||||
if [[ $- =~ H ]]; then
|
||||
sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"
|
||||
@@ -60,25 +65,18 @@ __fzf_history__() (
|
||||
fi
|
||||
)
|
||||
|
||||
__fzf_use_tmux__() {
|
||||
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-1}" != 0 ] && [ ${LINES:-40} -gt 15 ]
|
||||
}
|
||||
|
||||
[ $BASH_VERSINFO -gt 3 ] && __use_bind_x=1 || __use_bind_x=0
|
||||
__fzf_use_tmux__ && __use_tmux=1 || __use_tmux=0
|
||||
|
||||
if [[ ! -o vi ]]; then
|
||||
# Required to refresh the prompt after fzf
|
||||
bind '"\er": redraw-current-line'
|
||||
bind '"\e^": history-expand-line'
|
||||
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
if [ $__use_bind_x -eq 1 ]; then
|
||||
if [ $BASH_VERSINFO -gt 3 ]; then
|
||||
bind -x '"\C-t": "fzf-file-widget"'
|
||||
elif [ $__use_tmux -eq 1 ]; then
|
||||
bind '"\C-t": " \C-u \C-a\C-k$(__fzf_select_tmux__)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"'
|
||||
elif __fzf_use_tmux__; then
|
||||
bind '"\C-t": " \C-u \C-a\C-k`__fzf_select_tmux__`\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"'
|
||||
else
|
||||
bind '"\C-t": " \C-u \C-a\C-k$(__fzf_select__)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"'
|
||||
bind '"\C-t": " \C-u \C-a\C-k`__fzf_select__`\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"'
|
||||
fi
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
@@ -102,24 +100,22 @@ else
|
||||
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
# - FIXME: Selected items are attached to the end regardless of cursor position
|
||||
if [ $__use_bind_x -eq 1 ]; then
|
||||
if [ $BASH_VERSINFO -gt 3 ]; then
|
||||
bind -x '"\C-t": "fzf-file-widget"'
|
||||
elif [ $__use_tmux -eq 1 ]; then
|
||||
bind '"\C-t": "\C-x\C-a$a \C-x\C-addi$(__fzf_select_tmux__)\C-x\C-e\C-x\C-a0P$xa"'
|
||||
elif __fzf_use_tmux__; then
|
||||
bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select_tmux__`\C-x\C-e\C-x\C-a0P$xa"'
|
||||
else
|
||||
bind '"\C-t": "\C-x\C-a$a \C-x\C-addi$(__fzf_select__)\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "'
|
||||
bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select__`\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "'
|
||||
fi
|
||||
bind -m vi-command '"\C-t": "i\C-t"'
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
bind '"\C-r": "\C-x\C-addi$(__fzf_history__)\C-x\C-e\C-x^\C-x\C-a$a\C-x\C-r"'
|
||||
bind '"\C-r": "\C-x\C-addi`__fzf_history__`\C-x\C-e\C-x^\C-x\C-a$a\C-x\C-r"'
|
||||
bind -m vi-command '"\C-r": "i\C-r"'
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
bind '"\ec": "\C-x\C-addi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"'
|
||||
bind -m vi-command '"\ec": "ddi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"'
|
||||
bind '"\ec": "\C-x\C-addi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"'
|
||||
bind -m vi-command '"\ec": "ddi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"'
|
||||
fi
|
||||
|
||||
unset -v __use_tmux __use_bind_x
|
||||
|
||||
fi
|
||||
|
@@ -1,58 +1,75 @@
|
||||
# Key bindings
|
||||
# ------------
|
||||
function fzf_key_bindings
|
||||
# Due to a bug of fish, we cannot use command substitution,
|
||||
# so we use temporary file instead
|
||||
if [ -z "$TMPDIR" ]
|
||||
set -g TMPDIR /tmp
|
||||
end
|
||||
|
||||
function __fzf_escape
|
||||
while read item
|
||||
echo -n (echo -n "$item" | sed -E 's/([ "$~'\''([{<>})])/\\\\\\1/g')' '
|
||||
# Store last token in $dir as root for the 'find' command
|
||||
function fzf-file-widget -d "List files and folders"
|
||||
set -l dir (commandline -t)
|
||||
# The commandline token might be escaped, we need to unescape it.
|
||||
set dir (eval "printf '%s' $dir")
|
||||
if [ ! -d "$dir" ]
|
||||
set dir .
|
||||
end
|
||||
end
|
||||
# Some 'find' versions print undesired duplicated slashes if the path ends with slashes.
|
||||
set dir (string replace --regex '(.)/+$' '$1' "$dir")
|
||||
|
||||
function fzf-file-widget
|
||||
# "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
|
||||
# $dir itself, even if hidden.
|
||||
set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND "
|
||||
command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | sed 1d | cut -b3-"
|
||||
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m $FZF_CTRL_T_OPTS > $TMPDIR/fzf.result"
|
||||
and for i in (seq 20); commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) 2> /dev/null; and break; sleep 0.1; end
|
||||
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | sed 's#^\./##'"
|
||||
|
||||
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
|
||||
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m" | while read -l r; set result $result $r; end
|
||||
end
|
||||
if [ -z "$result" ]
|
||||
commandline -f repaint
|
||||
return
|
||||
end
|
||||
|
||||
if [ "$dir" != . ]
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
end
|
||||
for i in $result
|
||||
commandline -it -- (string escape $i)
|
||||
commandline -it -- ' '
|
||||
end
|
||||
commandline -f repaint
|
||||
rm -f $TMPDIR/fzf.result
|
||||
end
|
||||
|
||||
function fzf-history-widget
|
||||
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)
|
||||
function fzf-history-widget -d "Show command history"
|
||||
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS +s --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m"
|
||||
history | eval (__fzfcmd) -q '(commandline)' | read -l result
|
||||
and commandline -- $result
|
||||
end
|
||||
commandline -f repaint
|
||||
rm -f $TMPDIR/fzf.result
|
||||
end
|
||||
|
||||
function fzf-cd-widget
|
||||
function fzf-cd-widget -d "Change directory"
|
||||
set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND "
|
||||
command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"
|
||||
# Fish hangs if the command before pipe redirects (2> /dev/null)
|
||||
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m $FZF_ALT_C_OPTS > $TMPDIR/fzf.result"
|
||||
[ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ]
|
||||
and cd (cat $TMPDIR/fzf.result)
|
||||
command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"
|
||||
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
|
||||
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m" | read -l result
|
||||
[ "$result" ]; and cd $result
|
||||
end
|
||||
commandline -f repaint
|
||||
rm -f $TMPDIR/fzf.result
|
||||
end
|
||||
|
||||
function __fzfcmd
|
||||
set -q FZF_TMUX; or set FZF_TMUX 1
|
||||
|
||||
set -q FZF_TMUX; or set FZF_TMUX 0
|
||||
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
|
||||
if [ $FZF_TMUX -eq 1 ]
|
||||
if set -q FZF_TMUX_HEIGHT
|
||||
echo "fzf-tmux -d$FZF_TMUX_HEIGHT"
|
||||
else
|
||||
echo "fzf-tmux -d40%"
|
||||
end
|
||||
echo "fzf-tmux -d$FZF_TMUX_HEIGHT"
|
||||
else
|
||||
echo "fzf"
|
||||
end
|
||||
@@ -68,4 +85,3 @@ function fzf_key_bindings
|
||||
bind -M insert \ec fzf-cd-widget
|
||||
end
|
||||
end
|
||||
|
||||
|
@@ -4,12 +4,12 @@ if [[ $- == *i* ]]; then
|
||||
|
||||
# CTRL-T - Paste the selected file path(s) into the command line
|
||||
__fsel() {
|
||||
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
|
||||
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | sed 1d | cut -b3-"}"
|
||||
-o -type l -print 2> /dev/null | cut -b3-"}"
|
||||
setopt localoptions pipefail 2> /dev/null
|
||||
eval "$cmd | $(__fzfcmd) -m $FZF_CTRL_T_OPTS" | while read item; do
|
||||
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do
|
||||
echo -n "${(q)item} "
|
||||
done
|
||||
local ret=$?
|
||||
@@ -17,8 +17,13 @@ __fsel() {
|
||||
return $ret
|
||||
}
|
||||
|
||||
__fzf_use_tmux__() {
|
||||
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]
|
||||
}
|
||||
|
||||
__fzfcmd() {
|
||||
[ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
|
||||
__fzf_use_tmux__ &&
|
||||
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
|
||||
}
|
||||
|
||||
fzf-file-widget() {
|
||||
@@ -33,10 +38,10 @@ bindkey '^T' fzf-file-widget
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
fzf-cd-widget() {
|
||||
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
|
||||
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"}"
|
||||
setopt localoptions pipefail 2> /dev/null
|
||||
cd "${$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS"):-.}"
|
||||
cd "${$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m):-.}"
|
||||
local ret=$?
|
||||
zle reset-prompt
|
||||
typeset -f zle-line-init >/dev/null && zle zle-line-init
|
||||
@@ -49,7 +54,8 @@ bindkey '\ec' fzf-cd-widget
|
||||
fzf-history-widget() {
|
||||
local selected num
|
||||
setopt localoptions noglobsubst pipefail 2> /dev/null
|
||||
selected=( $(fc -l 1 | eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") )
|
||||
selected=( $(fc -l 1 |
|
||||
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(q)LBUFFER} +m" $(__fzfcmd)) )
|
||||
local ret=$?
|
||||
if [ -n "$selected" ]; then
|
||||
num=$selected[1]
|
||||
|
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Junegunn Choi
|
||||
Copyright (c) 2017 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
70
src/Makefile
70
src/Makefile
@@ -15,35 +15,76 @@ SRCDIR := $(GOPATH)/src/github.com/junegunn/fzf/src
|
||||
DOCKEROPTS := -i -t -v $(ROOTDIR):/fzf/src
|
||||
BINARY32 := fzf-$(GOOS)_386
|
||||
BINARY64 := fzf-$(GOOS)_amd64
|
||||
BINARYARM5 := fzf-$(GOOS)_arm5
|
||||
BINARYARM6 := fzf-$(GOOS)_arm6
|
||||
BINARYARM7 := fzf-$(GOOS)_arm7
|
||||
BINARYARM8 := fzf-$(GOOS)_arm8
|
||||
VERSION := $(shell awk -F= '/version =/ {print $$2}' constants.go | tr -d "\" ")
|
||||
RELEASE32 := fzf-$(VERSION)-$(GOOS)_386
|
||||
RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64
|
||||
RELEASEARM5 := fzf-$(VERSION)-$(GOOS)_arm5
|
||||
RELEASEARM6 := fzf-$(VERSION)-$(GOOS)_arm6
|
||||
RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7
|
||||
RELEASEARM8 := fzf-$(VERSION)-$(GOOS)_arm8
|
||||
export GOPATH
|
||||
|
||||
# https://en.wikipedia.org/wiki/Uname
|
||||
UNAME_M := $(shell uname -m)
|
||||
ifeq ($(UNAME_M),x86_64)
|
||||
BINARY := $(BINARY64)
|
||||
else ifeq ($(UNAME_M),amd64)
|
||||
BINARY := $(BINARY64)
|
||||
else ifeq ($(UNAME_M),i686)
|
||||
BINARY := $(BINARY32)
|
||||
else ifeq ($(UNAME_M),i386)
|
||||
BINARY := $(BINARY32)
|
||||
else ifeq ($(UNAME_M),armv5l)
|
||||
BINARY := $(BINARYARM5)
|
||||
else ifeq ($(UNAME_M),armv6l)
|
||||
BINARY := $(BINARYARM6)
|
||||
else ifeq ($(UNAME_M),armv7l)
|
||||
BINARY := $(BINARYARM7)
|
||||
else
|
||||
$(error "Build on $(UNAME_M) is not supported, yet.")
|
||||
endif
|
||||
|
||||
all: fzf/$(BINARY)
|
||||
|
||||
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)
|
||||
ifeq ($(GOOS),windows)
|
||||
release: fzf/$(BINARY32) fzf/$(BINARY64)
|
||||
cd fzf && cp -f $(BINARY32) fzf.exe && zip $(RELEASE32).zip fzf.exe
|
||||
cd fzf && cp -f $(BINARY64) fzf.exe && zip $(RELEASE64).zip fzf.exe
|
||||
cd fzf && rm -f fzf.exe
|
||||
else ifeq ($(GOOS),linux)
|
||||
release: fzf/$(BINARY32) fzf/$(BINARY64) fzf/$(BINARYARM5) fzf/$(BINARYARM6) fzf/$(BINARYARM7) fzf/$(BINARYARM8)
|
||||
cd fzf && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf
|
||||
cd fzf && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf
|
||||
cd fzf && cp -f $(BINARYARM5) fzf && tar -czf $(RELEASEARM5).tgz fzf
|
||||
cd fzf && cp -f $(BINARYARM6) fzf && tar -czf $(RELEASEARM6).tgz fzf
|
||||
cd fzf && cp -f $(BINARYARM7) fzf && tar -czf $(RELEASEARM7).tgz fzf
|
||||
cd fzf && cp -f $(BINARYARM8) fzf && tar -czf $(RELEASEARM8).tgz fzf
|
||||
cd fzf && rm -f fzf
|
||||
else
|
||||
release: fzf/$(BINARY32) fzf/$(BINARY64)
|
||||
cd fzf && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf
|
||||
cd fzf && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf
|
||||
cd fzf && rm -f fzf
|
||||
endif
|
||||
|
||||
release-all: clean test
|
||||
GOOS=darwin make release
|
||||
GOOS=linux make release
|
||||
GOOS=freebsd make release
|
||||
GOOS=openbsd make release
|
||||
GOOS=windows make release
|
||||
|
||||
$(SRCDIR):
|
||||
mkdir -p $(shell dirname $(SRCDIR))
|
||||
ln -s $(ROOTDIR) $(SRCDIR)
|
||||
|
||||
deps: $(SRCDIR) $(SOURCES)
|
||||
cd $(SRCDIR) && go get
|
||||
cd $(SRCDIR) && go get -tags "$(TAGS)"
|
||||
./deps
|
||||
|
||||
android-build: $(SRCDIR)
|
||||
cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get
|
||||
@@ -52,7 +93,7 @@ android-build: $(SRCDIR)
|
||||
rm -f $(RELEASEARM7)
|
||||
|
||||
test: deps
|
||||
SHELL=/bin/sh go test -v ./...
|
||||
SHELL=/bin/sh GOOS= go test -v -tags "$(TAGS)" ./...
|
||||
|
||||
install: $(BINDIR)/fzf
|
||||
|
||||
@@ -63,10 +104,23 @@ clean:
|
||||
cd fzf && rm -f fzf-*
|
||||
|
||||
fzf/$(BINARY32): deps
|
||||
cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -ldflags -w -tags "$(TAGS)" -o $(BINARY32)
|
||||
cd fzf && GOARCH=386 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY32)
|
||||
|
||||
fzf/$(BINARY64): deps
|
||||
cd fzf && go build -a -ldflags -w -tags "$(TAGS)" -o $(BINARY64)
|
||||
cd fzf && GOARCH=amd64 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY64)
|
||||
|
||||
# https://github.com/golang/go/wiki/GoArm
|
||||
fzf/$(BINARYARM5): deps
|
||||
cd fzf && GOARCH=arm GOARM=5 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM5)
|
||||
|
||||
fzf/$(BINARYARM6): deps
|
||||
cd fzf && GOARCH=arm GOARM=6 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM6)
|
||||
|
||||
fzf/$(BINARYARM7): deps
|
||||
cd fzf && GOARCH=arm GOARM=7 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM7)
|
||||
|
||||
fzf/$(BINARYARM8): deps
|
||||
cd fzf && GOARCH=arm64 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM8)
|
||||
|
||||
$(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR)
|
||||
cp -f fzf/$(BINARY) $(BINDIR)
|
||||
|
@@ -50,16 +50,7 @@ starts much faster though the difference may not be noticeable.
|
||||
Build
|
||||
-----
|
||||
|
||||
```sh
|
||||
# Build fzf executables and tarballs
|
||||
make release
|
||||
|
||||
# Install the executable to ../bin directory
|
||||
make install
|
||||
|
||||
# Build executables and tarballs for Linux using Docker
|
||||
make linux
|
||||
```
|
||||
See [BUILD.md](../BUILD.md)
|
||||
|
||||
Test
|
||||
----
|
||||
@@ -68,24 +59,39 @@ Unit tests can be run with `make test`. Integration tests are written in Ruby
|
||||
script that should be run on tmux.
|
||||
|
||||
```sh
|
||||
cd src
|
||||
|
||||
# Unit tests
|
||||
make test
|
||||
|
||||
# Integration tests
|
||||
ruby ../test/test_go.rb
|
||||
|
||||
# Build binary for the platform
|
||||
make
|
||||
|
||||
# Install the executable to ../bin directory
|
||||
make install
|
||||
|
||||
# Integration tests
|
||||
ruby ../test/test_go.rb
|
||||
# Make release archives
|
||||
make release
|
||||
|
||||
# Make release archives for all supported platforms
|
||||
make release-all
|
||||
```
|
||||
|
||||
Third-party libraries used
|
||||
--------------------------
|
||||
|
||||
- [ncurses][ncurses]
|
||||
- ~[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)
|
||||
- [tcell](https://github.com/gdamore/tcell)
|
||||
- Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE)
|
||||
|
||||
License
|
||||
-------
|
||||
@@ -97,4 +103,4 @@ License
|
||||
[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock
|
||||
[ncurses]: https://www.gnu.org/software/ncurses/
|
||||
[req]: http://golang.org/doc/install
|
||||
[termbox]: https://github.com/nsf/termbox-go
|
||||
[tcell]: https://github.com/gdamore/tcell
|
||||
|
@@ -234,9 +234,24 @@ func bonusAt(input util.Chars, idx int) int16 {
|
||||
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx)))
|
||||
}
|
||||
|
||||
type Algo func(caseSensitive bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int)
|
||||
func normalizeRune(r rune) rune {
|
||||
if r < 0x00C0 || r > 0x2184 {
|
||||
return r
|
||||
}
|
||||
|
||||
func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
n := normalized[r]
|
||||
if n > 0 {
|
||||
return n
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Algo functions make two assumptions
|
||||
// 1. "pattern" is given in lowercase if "caseSensitive" is false
|
||||
// 2. "pattern" is already normalized if "normalize" is true
|
||||
type Algo func(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int)
|
||||
|
||||
func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
// Assume that pattern is given in lowercase if case-insensitive.
|
||||
// First check if there's a match and calculate bonus for each position.
|
||||
// If the input string is too long, consider finding the matching chars in
|
||||
@@ -247,13 +262,13 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern []
|
||||
case 0:
|
||||
return Result{0, 0, 0}, posArray(withPos, M)
|
||||
case 1:
|
||||
return ExactMatchNaive(caseSensitive, forward, input, pattern[0:1], withPos, slab)
|
||||
return ExactMatchNaive(caseSensitive, normalize, forward, input, pattern[0:1], withPos, slab)
|
||||
}
|
||||
|
||||
// Since O(nm) algorithm can be prohibitively expensive for large input,
|
||||
// we fall back to the greedy algorithm.
|
||||
if slab != nil && N*M > cap(slab.I16) {
|
||||
return FuzzyMatchV1(caseSensitive, forward, input, pattern, withPos, slab)
|
||||
return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab)
|
||||
}
|
||||
|
||||
// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
|
||||
@@ -285,6 +300,10 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern []
|
||||
}
|
||||
}
|
||||
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
|
||||
T[idx] = char
|
||||
B[idx] = bonusFor(prevClass, class)
|
||||
prevClass = class
|
||||
@@ -432,7 +451,7 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern []
|
||||
}
|
||||
|
||||
// Implement the same sorting criteria as V2
|
||||
func calculateScore(caseSensitive bool, text util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) {
|
||||
func calculateScore(caseSensitive bool, normalize bool, text util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) {
|
||||
pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0)
|
||||
pos := posArray(withPos, len(pattern))
|
||||
prevClass := charNonWord
|
||||
@@ -449,6 +468,10 @@ func calculateScore(caseSensitive bool, text util.Chars, pattern []rune, sidx in
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
}
|
||||
// pattern is already normalized
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
if char == pattern[pidx] {
|
||||
if withPos {
|
||||
*pos = append(*pos, idx)
|
||||
@@ -488,7 +511,7 @@ func calculateScore(caseSensitive bool, text util.Chars, pattern []rune, sidx in
|
||||
}
|
||||
|
||||
// FuzzyMatchV1 performs fuzzy-match
|
||||
func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
if len(pattern) == 0 {
|
||||
return Result{0, 0, 0}, nil
|
||||
}
|
||||
@@ -514,6 +537,9 @@ func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []r
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
}
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
pchar := pattern[indexAt(pidx, lenPattern, forward)]
|
||||
if char == pchar {
|
||||
if sidx < 0 {
|
||||
@@ -553,7 +579,7 @@ func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []r
|
||||
sidx, eidx = lenRunes-eidx, lenRunes-sidx
|
||||
}
|
||||
|
||||
score, pos := calculateScore(caseSensitive, text, pattern, sidx, eidx, withPos)
|
||||
score, pos := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, withPos)
|
||||
return Result{sidx, eidx, score}, pos
|
||||
}
|
||||
return Result{-1, -1, 0}, nil
|
||||
@@ -568,7 +594,7 @@ func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []r
|
||||
// bonus point, instead of stopping immediately after finding the first match.
|
||||
// The solution is much cheaper since there is only one possible alignment of
|
||||
// the pattern.
|
||||
func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
if len(pattern) == 0 {
|
||||
return Result{0, 0, 0}, nil
|
||||
}
|
||||
@@ -593,6 +619,9 @@ func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
}
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
pidx_ := indexAt(pidx, lenPattern, forward)
|
||||
pchar := pattern[pidx_]
|
||||
if pchar == char {
|
||||
@@ -624,14 +653,14 @@ func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern
|
||||
sidx = lenRunes - (bestPos + 1)
|
||||
eidx = lenRunes - (bestPos - lenPattern + 1)
|
||||
}
|
||||
score, _ := calculateScore(caseSensitive, text, pattern, sidx, eidx, false)
|
||||
score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
|
||||
return Result{sidx, eidx, score}, nil
|
||||
}
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
// PrefixMatch performs prefix-match
|
||||
func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
if len(pattern) == 0 {
|
||||
return Result{0, 0, 0}, nil
|
||||
}
|
||||
@@ -645,17 +674,20 @@ func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru
|
||||
if !caseSensitive {
|
||||
char = unicode.ToLower(char)
|
||||
}
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
if char != r {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
}
|
||||
lenPattern := len(pattern)
|
||||
score, _ := calculateScore(caseSensitive, text, pattern, 0, lenPattern, false)
|
||||
score, _ := calculateScore(caseSensitive, normalize, text, pattern, 0, lenPattern, false)
|
||||
return Result{0, lenPattern, score}, nil
|
||||
}
|
||||
|
||||
// SuffixMatch performs suffix-match
|
||||
func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
lenRunes := text.Length()
|
||||
trimmedLen := lenRunes - text.TrailingWhitespaces()
|
||||
if len(pattern) == 0 {
|
||||
@@ -671,6 +703,9 @@ func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru
|
||||
if !caseSensitive {
|
||||
char = unicode.ToLower(char)
|
||||
}
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
if char != r {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
@@ -678,21 +713,37 @@ func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru
|
||||
lenPattern := len(pattern)
|
||||
sidx := trimmedLen - lenPattern
|
||||
eidx := trimmedLen
|
||||
score, _ := calculateScore(caseSensitive, text, pattern, sidx, eidx, false)
|
||||
score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
|
||||
return Result{sidx, eidx, score}, nil
|
||||
}
|
||||
|
||||
// EqualMatch performs equal-match
|
||||
func EqualMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
func EqualMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
lenPattern := len(pattern)
|
||||
if text.Length() != lenPattern {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
runesStr := text.ToString()
|
||||
if !caseSensitive {
|
||||
runesStr = strings.ToLower(runesStr)
|
||||
match := true
|
||||
if normalize {
|
||||
runes := text.ToRunes()
|
||||
for idx, pchar := range pattern {
|
||||
char := runes[idx]
|
||||
if !caseSensitive {
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
if normalizeRune(pchar) != normalizeRune(char) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
runesStr := text.ToString()
|
||||
if !caseSensitive {
|
||||
runesStr = strings.ToLower(runesStr)
|
||||
}
|
||||
match = runesStr == string(pattern)
|
||||
}
|
||||
if runesStr == string(pattern) {
|
||||
if match {
|
||||
return Result{0, lenPattern, (scoreMatch+bonusBoundary)*lenPattern +
|
||||
(bonusFirstCharMultiplier-1)*bonusBoundary}, nil
|
||||
}
|
||||
|
@@ -10,10 +10,14 @@ import (
|
||||
)
|
||||
|
||||
func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) {
|
||||
assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score)
|
||||
}
|
||||
|
||||
func assertMatch2(t *testing.T, fun Algo, caseSensitive, normalize, forward bool, input, pattern string, sidx int, eidx int, score int) {
|
||||
if !caseSensitive {
|
||||
pattern = strings.ToLower(pattern)
|
||||
}
|
||||
res, pos := fun(caseSensitive, forward, util.RunesToChars([]rune(input)), []rune(pattern), true, nil)
|
||||
res, pos := fun(caseSensitive, normalize, forward, util.RunesToChars([]rune(input)), []rune(pattern), true, nil)
|
||||
var start, end int
|
||||
if pos == nil || len(*pos) == 0 {
|
||||
start = res.Start
|
||||
@@ -156,6 +160,21 @@ func TestEmptyPattern(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalize(t *testing.T) {
|
||||
caseSensitive := false
|
||||
normalize := true
|
||||
forward := true
|
||||
test := func(input, pattern string, sidx, eidx, score int, funs ...Algo) {
|
||||
for _, fun := range funs {
|
||||
assertMatch2(t, fun, caseSensitive, normalize, forward,
|
||||
input, pattern, sidx, eidx, score)
|
||||
}
|
||||
}
|
||||
test("Só Danço Samba", "So", 0, 2, 56, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive)
|
||||
test("Só Danço Samba", "sodc", 0, 7, 89, FuzzyMatchV1, FuzzyMatchV2)
|
||||
test("Danço", "danco", 0, 5, 128, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch)
|
||||
}
|
||||
|
||||
func TestLongString(t *testing.T) {
|
||||
bytes := make([]byte, math.MaxUint16*2)
|
||||
for i := range bytes {
|
||||
|
424
src/algo/normalize.go
Normal file
424
src/algo/normalize.go
Normal file
@@ -0,0 +1,424 @@
|
||||
// Normalization of latin script letters
|
||||
// Reference: http://www.unicode.org/Public/UCD/latest/ucd/Index.txt
|
||||
|
||||
package algo
|
||||
|
||||
var normalized map[rune]rune = map[rune]rune{
|
||||
0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER
|
||||
0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER
|
||||
0x00E2: 'a', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x00E4: 'a', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x0227: 'a', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1EA1: 'a', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0201: 'a', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
|
||||
0x00E0: 'a', // WITH GRAVE, LATIN SMALL LETTER
|
||||
0x1EA3: 'a', // WITH HOOK ABOVE, LATIN SMALL LETTER
|
||||
0x0203: 'a', // WITH INVERTED BREVE, LATIN SMALL LETTER
|
||||
0x0101: 'a', // WITH MACRON, LATIN SMALL LETTER
|
||||
0x0105: 'a', // WITH OGONEK, LATIN SMALL LETTER
|
||||
0x1E9A: 'a', // WITH RIGHT HALF RING, LATIN SMALL LETTER
|
||||
0x00E5: 'a', // WITH RING ABOVE, LATIN SMALL LETTER
|
||||
0x1E01: 'a', // WITH RING BELOW, LATIN SMALL LETTER
|
||||
0x00E3: 'a', // WITH TILDE, LATIN SMALL LETTER
|
||||
0x0363: 'a', // , COMBINING LATIN SMALL LETTER
|
||||
0x0250: 'a', // , LATIN SMALL LETTER TURNED
|
||||
0x1E03: 'b', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E05: 'b', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0253: 'b', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x1E07: 'b', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x0180: 'b', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x0183: 'b', // WITH TOPBAR, LATIN SMALL LETTER
|
||||
0x0107: 'c', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x010D: 'c', // WITH CARON, LATIN SMALL LETTER
|
||||
0x00E7: 'c', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x0109: 'c', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x0255: 'c', // WITH CURL, LATIN SMALL LETTER
|
||||
0x010B: 'c', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x0188: 'c', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x023C: 'c', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x0368: 'c', // , COMBINING LATIN SMALL LETTER
|
||||
0x0297: 'c', // , LATIN LETTER STRETCHED
|
||||
0x2184: 'c', // , LATIN SMALL LETTER REVERSED
|
||||
0x010F: 'd', // WITH CARON, LATIN SMALL LETTER
|
||||
0x1E11: 'd', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x1E13: 'd', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
|
||||
0x0221: 'd', // WITH CURL, LATIN SMALL LETTER
|
||||
0x1E0B: 'd', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E0D: 'd', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0257: 'd', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x1E0F: 'd', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x0111: 'd', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x0256: 'd', // WITH TAIL, LATIN SMALL LETTER
|
||||
0x018C: 'd', // WITH TOPBAR, LATIN SMALL LETTER
|
||||
0x0369: 'd', // , COMBINING LATIN SMALL LETTER
|
||||
0x00E9: 'e', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x0115: 'e', // WITH BREVE, LATIN SMALL LETTER
|
||||
0x011B: 'e', // WITH CARON, LATIN SMALL LETTER
|
||||
0x0229: 'e', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x1E19: 'e', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
|
||||
0x00EA: 'e', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x00EB: 'e', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x0117: 'e', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1EB9: 'e', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0205: 'e', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
|
||||
0x00E8: 'e', // WITH GRAVE, LATIN SMALL LETTER
|
||||
0x1EBB: 'e', // WITH HOOK ABOVE, LATIN SMALL LETTER
|
||||
0x025D: 'e', // WITH HOOK, LATIN SMALL LETTER REVERSED OPEN
|
||||
0x0207: 'e', // WITH INVERTED BREVE, LATIN SMALL LETTER
|
||||
0x0113: 'e', // WITH MACRON, LATIN SMALL LETTER
|
||||
0x0119: 'e', // WITH OGONEK, LATIN SMALL LETTER
|
||||
0x0247: 'e', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x1E1B: 'e', // WITH TILDE BELOW, LATIN SMALL LETTER
|
||||
0x1EBD: 'e', // WITH TILDE, LATIN SMALL LETTER
|
||||
0x0364: 'e', // , COMBINING LATIN SMALL LETTER
|
||||
0x029A: 'e', // , LATIN SMALL LETTER CLOSED OPEN
|
||||
0x025E: 'e', // , LATIN SMALL LETTER CLOSED REVERSED OPEN
|
||||
0x025B: 'e', // , LATIN SMALL LETTER OPEN
|
||||
0x0258: 'e', // , LATIN SMALL LETTER REVERSED
|
||||
0x025C: 'e', // , LATIN SMALL LETTER REVERSED OPEN
|
||||
0x01DD: 'e', // , LATIN SMALL LETTER TURNED
|
||||
0x1D08: 'e', // , LATIN SMALL LETTER TURNED OPEN
|
||||
0x1E1F: 'f', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x0192: 'f', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x01F5: 'g', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x011F: 'g', // WITH BREVE, LATIN SMALL LETTER
|
||||
0x01E7: 'g', // WITH CARON, LATIN SMALL LETTER
|
||||
0x0123: 'g', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x011D: 'g', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x0121: 'g', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x0260: 'g', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x1E21: 'g', // WITH MACRON, LATIN SMALL LETTER
|
||||
0x01E5: 'g', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x0261: 'g', // , LATIN SMALL LETTER SCRIPT
|
||||
0x1E2B: 'h', // WITH BREVE BELOW, LATIN SMALL LETTER
|
||||
0x021F: 'h', // WITH CARON, LATIN SMALL LETTER
|
||||
0x1E29: 'h', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x0125: 'h', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x1E27: 'h', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x1E23: 'h', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E25: 'h', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x02AE: 'h', // WITH FISHHOOK, LATIN SMALL LETTER TURNED
|
||||
0x0266: 'h', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x1E96: 'h', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x0127: 'h', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x036A: 'h', // , COMBINING LATIN SMALL LETTER
|
||||
0x0265: 'h', // , LATIN SMALL LETTER TURNED
|
||||
0x2095: 'h', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x00ED: 'i', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x012D: 'i', // WITH BREVE, LATIN SMALL LETTER
|
||||
0x01D0: 'i', // WITH CARON, LATIN SMALL LETTER
|
||||
0x00EE: 'i', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x00EF: 'i', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x1ECB: 'i', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0209: 'i', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
|
||||
0x00EC: 'i', // WITH GRAVE, LATIN SMALL LETTER
|
||||
0x1EC9: 'i', // WITH HOOK ABOVE, LATIN SMALL LETTER
|
||||
0x020B: 'i', // WITH INVERTED BREVE, LATIN SMALL LETTER
|
||||
0x012B: 'i', // WITH MACRON, LATIN SMALL LETTER
|
||||
0x012F: 'i', // WITH OGONEK, LATIN SMALL LETTER
|
||||
0x0268: 'i', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x1E2D: 'i', // WITH TILDE BELOW, LATIN SMALL LETTER
|
||||
0x0129: 'i', // WITH TILDE, LATIN SMALL LETTER
|
||||
0x0365: 'i', // , COMBINING LATIN SMALL LETTER
|
||||
0x0131: 'i', // , LATIN SMALL LETTER DOTLESS
|
||||
0x1D09: 'i', // , LATIN SMALL LETTER TURNED
|
||||
0x1D62: 'i', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x2071: 'i', // , SUPERSCRIPT LATIN SMALL LETTER
|
||||
0x01F0: 'j', // WITH CARON, LATIN SMALL LETTER
|
||||
0x0135: 'j', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x029D: 'j', // WITH CROSSED-TAIL, LATIN SMALL LETTER
|
||||
0x0249: 'j', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x025F: 'j', // WITH STROKE, LATIN SMALL LETTER DOTLESS
|
||||
0x0237: 'j', // , LATIN SMALL LETTER DOTLESS
|
||||
0x1E31: 'k', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x01E9: 'k', // WITH CARON, LATIN SMALL LETTER
|
||||
0x0137: 'k', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x1E33: 'k', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0199: 'k', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x1E35: 'k', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x029E: 'k', // , LATIN SMALL LETTER TURNED
|
||||
0x2096: 'k', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x013A: 'l', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x019A: 'l', // WITH BAR, LATIN SMALL LETTER
|
||||
0x026C: 'l', // WITH BELT, LATIN SMALL LETTER
|
||||
0x013E: 'l', // WITH CARON, LATIN SMALL LETTER
|
||||
0x013C: 'l', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x1E3D: 'l', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
|
||||
0x0234: 'l', // WITH CURL, LATIN SMALL LETTER
|
||||
0x1E37: 'l', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x1E3B: 'l', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x0140: 'l', // WITH MIDDLE DOT, LATIN SMALL LETTER
|
||||
0x026B: 'l', // WITH MIDDLE TILDE, LATIN SMALL LETTER
|
||||
0x026D: 'l', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
|
||||
0x0142: 'l', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x2097: 'l', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x1E3F: 'm', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x1E41: 'm', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E43: 'm', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0271: 'm', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x0270: 'm', // WITH LONG LEG, LATIN SMALL LETTER TURNED
|
||||
0x036B: 'm', // , COMBINING LATIN SMALL LETTER
|
||||
0x1D1F: 'm', // , LATIN SMALL LETTER SIDEWAYS TURNED
|
||||
0x026F: 'm', // , LATIN SMALL LETTER TURNED
|
||||
0x2098: 'm', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x0144: 'n', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x0148: 'n', // WITH CARON, LATIN SMALL LETTER
|
||||
0x0146: 'n', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x1E4B: 'n', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
|
||||
0x0235: 'n', // WITH CURL, LATIN SMALL LETTER
|
||||
0x1E45: 'n', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E47: 'n', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x01F9: 'n', // WITH GRAVE, LATIN SMALL LETTER
|
||||
0x0272: 'n', // WITH LEFT HOOK, LATIN SMALL LETTER
|
||||
0x1E49: 'n', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x019E: 'n', // WITH LONG RIGHT LEG, LATIN SMALL LETTER
|
||||
0x0273: 'n', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
|
||||
0x00F1: 'n', // WITH TILDE, LATIN SMALL LETTER
|
||||
0x2099: 'n', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x00F3: 'o', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x014F: 'o', // WITH BREVE, LATIN SMALL LETTER
|
||||
0x01D2: 'o', // WITH CARON, LATIN SMALL LETTER
|
||||
0x00F4: 'o', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x00F6: 'o', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x022F: 'o', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1ECD: 'o', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0151: 'o', // WITH DOUBLE ACUTE, LATIN SMALL LETTER
|
||||
0x020D: 'o', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
|
||||
0x00F2: 'o', // WITH GRAVE, LATIN SMALL LETTER
|
||||
0x1ECF: 'o', // WITH HOOK ABOVE, LATIN SMALL LETTER
|
||||
0x01A1: 'o', // WITH HORN, LATIN SMALL LETTER
|
||||
0x020F: 'o', // WITH INVERTED BREVE, LATIN SMALL LETTER
|
||||
0x014D: 'o', // WITH MACRON, LATIN SMALL LETTER
|
||||
0x01EB: 'o', // WITH OGONEK, LATIN SMALL LETTER
|
||||
0x00F8: 'o', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x1D13: 'o', // WITH STROKE, LATIN SMALL LETTER SIDEWAYS
|
||||
0x00F5: 'o', // WITH TILDE, LATIN SMALL LETTER
|
||||
0x0366: 'o', // , COMBINING LATIN SMALL LETTER
|
||||
0x0275: 'o', // , LATIN SMALL LETTER BARRED
|
||||
0x1D17: 'o', // , LATIN SMALL LETTER BOTTOM HALF
|
||||
0x0254: 'o', // , LATIN SMALL LETTER OPEN
|
||||
0x1D11: 'o', // , LATIN SMALL LETTER SIDEWAYS
|
||||
0x1D12: 'o', // , LATIN SMALL LETTER SIDEWAYS OPEN
|
||||
0x1D16: 'o', // , LATIN SMALL LETTER TOP HALF
|
||||
0x1E55: 'p', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x1E57: 'p', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x01A5: 'p', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x209A: 'p', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x024B: 'q', // WITH HOOK TAIL, LATIN SMALL LETTER
|
||||
0x02A0: 'q', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x0155: 'r', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x0159: 'r', // WITH CARON, LATIN SMALL LETTER
|
||||
0x0157: 'r', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x1E59: 'r', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E5B: 'r', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0211: 'r', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
|
||||
0x027E: 'r', // WITH FISHHOOK, LATIN SMALL LETTER
|
||||
0x027F: 'r', // WITH FISHHOOK, LATIN SMALL LETTER REVERSED
|
||||
0x027B: 'r', // WITH HOOK, LATIN SMALL LETTER TURNED
|
||||
0x0213: 'r', // WITH INVERTED BREVE, LATIN SMALL LETTER
|
||||
0x1E5F: 'r', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x027C: 'r', // WITH LONG LEG, LATIN SMALL LETTER
|
||||
0x027A: 'r', // WITH LONG LEG, LATIN SMALL LETTER TURNED
|
||||
0x024D: 'r', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x027D: 'r', // WITH TAIL, LATIN SMALL LETTER
|
||||
0x036C: 'r', // , COMBINING LATIN SMALL LETTER
|
||||
0x0279: 'r', // , LATIN SMALL LETTER TURNED
|
||||
0x1D63: 'r', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x015B: 's', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x0161: 's', // WITH CARON, LATIN SMALL LETTER
|
||||
0x015F: 's', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x015D: 's', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x0219: 's', // WITH COMMA BELOW, LATIN SMALL LETTER
|
||||
0x1E61: 's', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E9B: 's', // WITH DOT ABOVE, LATIN SMALL LETTER LONG
|
||||
0x1E63: 's', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0282: 's', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x023F: 's', // WITH SWASH TAIL, LATIN SMALL LETTER
|
||||
0x017F: 's', // , LATIN SMALL LETTER LONG
|
||||
0x00DF: 's', // , LATIN SMALL LETTER SHARP
|
||||
0x209B: 's', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x0165: 't', // WITH CARON, LATIN SMALL LETTER
|
||||
0x0163: 't', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x1E71: 't', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
|
||||
0x021B: 't', // WITH COMMA BELOW, LATIN SMALL LETTER
|
||||
0x0236: 't', // WITH CURL, LATIN SMALL LETTER
|
||||
0x1E97: 't', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x1E6B: 't', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E6D: 't', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x01AD: 't', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x1E6F: 't', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x01AB: 't', // WITH PALATAL HOOK, LATIN SMALL LETTER
|
||||
0x0288: 't', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
|
||||
0x0167: 't', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x036D: 't', // , COMBINING LATIN SMALL LETTER
|
||||
0x0287: 't', // , LATIN SMALL LETTER TURNED
|
||||
0x209C: 't', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x0289: 'u', // BAR, LATIN SMALL LETTER
|
||||
0x00FA: 'u', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x016D: 'u', // WITH BREVE, LATIN SMALL LETTER
|
||||
0x01D4: 'u', // WITH CARON, LATIN SMALL LETTER
|
||||
0x1E77: 'u', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
|
||||
0x00FB: 'u', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x1E73: 'u', // WITH DIAERESIS BELOW, LATIN SMALL LETTER
|
||||
0x00FC: 'u', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x1EE5: 'u', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0171: 'u', // WITH DOUBLE ACUTE, LATIN SMALL LETTER
|
||||
0x0215: 'u', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
|
||||
0x00F9: 'u', // WITH GRAVE, LATIN SMALL LETTER
|
||||
0x1EE7: 'u', // WITH HOOK ABOVE, LATIN SMALL LETTER
|
||||
0x01B0: 'u', // WITH HORN, LATIN SMALL LETTER
|
||||
0x0217: 'u', // WITH INVERTED BREVE, LATIN SMALL LETTER
|
||||
0x016B: 'u', // WITH MACRON, LATIN SMALL LETTER
|
||||
0x0173: 'u', // WITH OGONEK, LATIN SMALL LETTER
|
||||
0x016F: 'u', // WITH RING ABOVE, LATIN SMALL LETTER
|
||||
0x1E75: 'u', // WITH TILDE BELOW, LATIN SMALL LETTER
|
||||
0x0169: 'u', // WITH TILDE, LATIN SMALL LETTER
|
||||
0x0367: 'u', // , COMBINING LATIN SMALL LETTER
|
||||
0x1D1D: 'u', // , LATIN SMALL LETTER SIDEWAYS
|
||||
0x1D1E: 'u', // , LATIN SMALL LETTER SIDEWAYS DIAERESIZED
|
||||
0x1D64: 'u', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x1E7F: 'v', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x028B: 'v', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x1E7D: 'v', // WITH TILDE, LATIN SMALL LETTER
|
||||
0x036E: 'v', // , COMBINING LATIN SMALL LETTER
|
||||
0x028C: 'v', // , LATIN SMALL LETTER TURNED
|
||||
0x1D65: 'v', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x1E83: 'w', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x0175: 'w', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x1E85: 'w', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x1E87: 'w', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E89: 'w', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x1E81: 'w', // WITH GRAVE, LATIN SMALL LETTER
|
||||
0x1E98: 'w', // WITH RING ABOVE, LATIN SMALL LETTER
|
||||
0x028D: 'w', // , LATIN SMALL LETTER TURNED
|
||||
0x1E8D: 'x', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x1E8B: 'x', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x036F: 'x', // , COMBINING LATIN SMALL LETTER
|
||||
0x00FD: 'y', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x0177: 'y', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x00FF: 'y', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x1E8F: 'y', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1EF5: 'y', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x1EF3: 'y', // WITH GRAVE, LATIN SMALL LETTER
|
||||
0x1EF7: 'y', // WITH HOOK ABOVE, LATIN SMALL LETTER
|
||||
0x01B4: 'y', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x0233: 'y', // WITH MACRON, LATIN SMALL LETTER
|
||||
0x1E99: 'y', // WITH RING ABOVE, LATIN SMALL LETTER
|
||||
0x024F: 'y', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x1EF9: 'y', // WITH TILDE, LATIN SMALL LETTER
|
||||
0x028E: 'y', // , LATIN SMALL LETTER TURNED
|
||||
0x017A: 'z', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x017E: 'z', // WITH CARON, LATIN SMALL LETTER
|
||||
0x1E91: 'z', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x0291: 'z', // WITH CURL, LATIN SMALL LETTER
|
||||
0x017C: 'z', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E93: 'z', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0225: 'z', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x1E95: 'z', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x0290: 'z', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
|
||||
0x01B6: 'z', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x0240: 'z', // WITH SWASH TAIL, LATIN SMALL LETTER
|
||||
0x0251: 'a', // , latin small letter script
|
||||
0x00C1: 'A', // WITH ACUTE, LATIN CAPITAL LETTER
|
||||
0x00C2: 'A', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
|
||||
0x00C4: 'A', // WITH DIAERESIS, LATIN CAPITAL LETTER
|
||||
0x00C0: 'A', // WITH GRAVE, LATIN CAPITAL LETTER
|
||||
0x00C5: 'A', // WITH RING ABOVE, LATIN CAPITAL LETTER
|
||||
0x023A: 'A', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x00C3: 'A', // WITH TILDE, LATIN CAPITAL LETTER
|
||||
0x1D00: 'A', // , LATIN LETTER SMALL CAPITAL
|
||||
0x0181: 'B', // WITH HOOK, LATIN CAPITAL LETTER
|
||||
0x0243: 'B', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x0299: 'B', // , LATIN LETTER SMALL CAPITAL
|
||||
0x1D03: 'B', // , LATIN LETTER SMALL CAPITAL BARRED
|
||||
0x00C7: 'C', // WITH CEDILLA, LATIN CAPITAL LETTER
|
||||
0x023B: 'C', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x1D04: 'C', // , LATIN LETTER SMALL CAPITAL
|
||||
0x018A: 'D', // WITH HOOK, LATIN CAPITAL LETTER
|
||||
0x0189: 'D', // , LATIN CAPITAL LETTER AFRICAN
|
||||
0x1D05: 'D', // , LATIN LETTER SMALL CAPITAL
|
||||
0x00C9: 'E', // WITH ACUTE, LATIN CAPITAL LETTER
|
||||
0x00CA: 'E', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
|
||||
0x00CB: 'E', // WITH DIAERESIS, LATIN CAPITAL LETTER
|
||||
0x00C8: 'E', // WITH GRAVE, LATIN CAPITAL LETTER
|
||||
0x0246: 'E', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x0190: 'E', // , LATIN CAPITAL LETTER OPEN
|
||||
0x018E: 'E', // , LATIN CAPITAL LETTER REVERSED
|
||||
0x1D07: 'E', // , LATIN LETTER SMALL CAPITAL
|
||||
0x0193: 'G', // WITH HOOK, LATIN CAPITAL LETTER
|
||||
0x029B: 'G', // WITH HOOK, LATIN LETTER SMALL CAPITAL
|
||||
0x0262: 'G', // , LATIN LETTER SMALL CAPITAL
|
||||
0x029C: 'H', // , LATIN LETTER SMALL CAPITAL
|
||||
0x00CD: 'I', // WITH ACUTE, LATIN CAPITAL LETTER
|
||||
0x00CE: 'I', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
|
||||
0x00CF: 'I', // WITH DIAERESIS, LATIN CAPITAL LETTER
|
||||
0x0130: 'I', // WITH DOT ABOVE, LATIN CAPITAL LETTER
|
||||
0x00CC: 'I', // WITH GRAVE, LATIN CAPITAL LETTER
|
||||
0x0197: 'I', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x026A: 'I', // , LATIN LETTER SMALL CAPITAL
|
||||
0x0248: 'J', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x1D0A: 'J', // , LATIN LETTER SMALL CAPITAL
|
||||
0x1D0B: 'K', // , LATIN LETTER SMALL CAPITAL
|
||||
0x023D: 'L', // WITH BAR, LATIN CAPITAL LETTER
|
||||
0x1D0C: 'L', // WITH STROKE, LATIN LETTER SMALL CAPITAL
|
||||
0x029F: 'L', // , LATIN LETTER SMALL CAPITAL
|
||||
0x019C: 'M', // , LATIN CAPITAL LETTER TURNED
|
||||
0x1D0D: 'M', // , LATIN LETTER SMALL CAPITAL
|
||||
0x019D: 'N', // WITH LEFT HOOK, LATIN CAPITAL LETTER
|
||||
0x0220: 'N', // WITH LONG RIGHT LEG, LATIN CAPITAL LETTER
|
||||
0x00D1: 'N', // WITH TILDE, LATIN CAPITAL LETTER
|
||||
0x0274: 'N', // , LATIN LETTER SMALL CAPITAL
|
||||
0x1D0E: 'N', // , LATIN LETTER SMALL CAPITAL REVERSED
|
||||
0x00D3: 'O', // WITH ACUTE, LATIN CAPITAL LETTER
|
||||
0x00D4: 'O', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
|
||||
0x00D6: 'O', // WITH DIAERESIS, LATIN CAPITAL LETTER
|
||||
0x00D2: 'O', // WITH GRAVE, LATIN CAPITAL LETTER
|
||||
0x019F: 'O', // WITH MIDDLE TILDE, LATIN CAPITAL LETTER
|
||||
0x00D8: 'O', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x00D5: 'O', // WITH TILDE, LATIN CAPITAL LETTER
|
||||
0x0186: 'O', // , LATIN CAPITAL LETTER OPEN
|
||||
0x1D0F: 'O', // , LATIN LETTER SMALL CAPITAL
|
||||
0x1D10: 'O', // , LATIN LETTER SMALL CAPITAL OPEN
|
||||
0x1D18: 'P', // , LATIN LETTER SMALL CAPITAL
|
||||
0x024A: 'Q', // WITH HOOK TAIL, LATIN CAPITAL LETTER SMALL
|
||||
0x024C: 'R', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x0280: 'R', // , LATIN LETTER SMALL CAPITAL
|
||||
0x0281: 'R', // , LATIN LETTER SMALL CAPITAL INVERTED
|
||||
0x1D19: 'R', // , LATIN LETTER SMALL CAPITAL REVERSED
|
||||
0x1D1A: 'R', // , LATIN LETTER SMALL CAPITAL TURNED
|
||||
0x023E: 'T', // WITH DIAGONAL STROKE, LATIN CAPITAL LETTER
|
||||
0x01AE: 'T', // WITH RETROFLEX HOOK, LATIN CAPITAL LETTER
|
||||
0x1D1B: 'T', // , LATIN LETTER SMALL CAPITAL
|
||||
0x0244: 'U', // BAR, LATIN CAPITAL LETTER
|
||||
0x00DA: 'U', // WITH ACUTE, LATIN CAPITAL LETTER
|
||||
0x00DB: 'U', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
|
||||
0x00DC: 'U', // WITH DIAERESIS, LATIN CAPITAL LETTER
|
||||
0x00D9: 'U', // WITH GRAVE, LATIN CAPITAL LETTER
|
||||
0x1D1C: 'U', // , LATIN LETTER SMALL CAPITAL
|
||||
0x01B2: 'V', // WITH HOOK, LATIN CAPITAL LETTER
|
||||
0x0245: 'V', // , LATIN CAPITAL LETTER TURNED
|
||||
0x1D20: 'V', // , LATIN LETTER SMALL CAPITAL
|
||||
0x1D21: 'W', // , LATIN LETTER SMALL CAPITAL
|
||||
0x00DD: 'Y', // WITH ACUTE, LATIN CAPITAL LETTER
|
||||
0x0178: 'Y', // WITH DIAERESIS, LATIN CAPITAL LETTER
|
||||
0x024E: 'Y', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x028F: 'Y', // , LATIN LETTER SMALL CAPITAL
|
||||
0x1D22: 'Z', // , LATIN LETTER SMALL CAPITAL
|
||||
}
|
||||
|
||||
// NormalizeRunes normalizes latin script letters
|
||||
func NormalizeRunes(runes []rune) []rune {
|
||||
ret := make([]rune, len(runes))
|
||||
copy(ret, runes)
|
||||
for idx, r := range runes {
|
||||
if r < 0x00C0 || r > 0x2184 {
|
||||
continue
|
||||
}
|
||||
n := normalized[r]
|
||||
if n > 0 {
|
||||
ret[idx] = normalized[r]
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
57
src/ansi.go
57
src/ansi.go
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/junegunn/fzf/src/curses"
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
)
|
||||
|
||||
type ansiOffset struct {
|
||||
@@ -16,9 +16,9 @@ type ansiOffset struct {
|
||||
}
|
||||
|
||||
type ansiState struct {
|
||||
fg int
|
||||
bg int
|
||||
attr curses.Attr
|
||||
fg tui.Color
|
||||
bg tui.Color
|
||||
attr tui.Attr
|
||||
}
|
||||
|
||||
func (s *ansiState) colored() bool {
|
||||
@@ -35,7 +35,16 @@ func (s *ansiState) equals(t *ansiState) bool {
|
||||
var ansiRegex *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
ansiRegex = regexp.MustCompile("\x1b.[0-9;]*.")
|
||||
/*
|
||||
References:
|
||||
- https://github.com/gnachman/iTerm2
|
||||
- http://ascii-table.com/ansi-escape-sequences.php
|
||||
- http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
||||
- http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
|
||||
*/
|
||||
// The following regular expression will include not all but most of the
|
||||
// frequently used ANSI sequences
|
||||
ansiRegex = regexp.MustCompile("\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x0e\x0f]|.\x08")
|
||||
}
|
||||
|
||||
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
|
||||
@@ -100,7 +109,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
||||
} else {
|
||||
state = &ansiState{prevState.fg, prevState.bg, prevState.attr}
|
||||
}
|
||||
if ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
||||
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
||||
return state
|
||||
}
|
||||
|
||||
@@ -134,40 +143,56 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
||||
case 49:
|
||||
state.bg = -1
|
||||
case 1:
|
||||
state.attr = curses.Bold
|
||||
state.attr = state.attr | tui.Bold
|
||||
case 2:
|
||||
state.attr = curses.Dim
|
||||
state.attr = state.attr | tui.Dim
|
||||
case 3:
|
||||
state.attr = state.attr | tui.Italic
|
||||
case 4:
|
||||
state.attr = curses.Underline
|
||||
state.attr = state.attr | tui.Underline
|
||||
case 5:
|
||||
state.attr = curses.Blink
|
||||
state.attr = state.attr | tui.Blink
|
||||
case 7:
|
||||
state.attr = curses.Reverse
|
||||
state.attr = 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 = num - 90 + 8
|
||||
state.fg = tui.Color(num - 90 + 8)
|
||||
} else if num >= 100 && num <= 107 {
|
||||
state.bg = num - 100 + 8
|
||||
state.bg = tui.Color(num - 100 + 8)
|
||||
}
|
||||
}
|
||||
case 1:
|
||||
switch num {
|
||||
case 2:
|
||||
state256 = 10 // MAGIC
|
||||
case 5:
|
||||
state256++
|
||||
default:
|
||||
state256 = 0
|
||||
}
|
||||
case 2:
|
||||
*ptr = num
|
||||
*ptr = tui.Color(num)
|
||||
state256 = 0
|
||||
case 10:
|
||||
*ptr = tui.Color(1<<24) | tui.Color(num<<16)
|
||||
state256++
|
||||
case 11:
|
||||
*ptr = *ptr | tui.Color(num<<8)
|
||||
state256++
|
||||
case 12:
|
||||
*ptr = *ptr | tui.Color(num)
|
||||
state256 = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
if state256 > 0 {
|
||||
*ptr = -1
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
@@ -4,14 +4,14 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/curses"
|
||||
"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) {
|
||||
var attr curses.Attr
|
||||
assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) {
|
||||
var attr tui.Attr
|
||||
if bold {
|
||||
attr = curses.Bold
|
||||
attr = tui.Bold
|
||||
}
|
||||
if offset.offset[0] != b || offset.offset[1] != e ||
|
||||
offset.color.fg != fg || offset.color.bg != bg || offset.color.attr != attr {
|
||||
@@ -26,7 +26,7 @@ func TestExtractColor(t *testing.T) {
|
||||
output, ansiOffsets, newState := extractColor(src, state, nil)
|
||||
state = newState
|
||||
if output != "hello world" {
|
||||
t.Errorf("Invalid output: {}", output)
|
||||
t.Errorf("Invalid output: %s %s", output, []rune(output))
|
||||
}
|
||||
fmt.Println(src, ansiOffsets, clean)
|
||||
assertion(ansiOffsets, state)
|
||||
@@ -56,7 +56,7 @@ func TestExtractColor(t *testing.T) {
|
||||
})
|
||||
|
||||
state = nil
|
||||
src = "\x1b[1mhello \x1b[mworld"
|
||||
src = "\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d"
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
|
@@ -8,14 +8,13 @@ import (
|
||||
|
||||
const (
|
||||
// Current version
|
||||
version = "0.15.5"
|
||||
version = "0.16.4"
|
||||
|
||||
// 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
8
src/constants_unix.go
Normal file
@@ -0,0 +1,8 @@
|
||||
// +build !windows
|
||||
|
||||
package fzf
|
||||
|
||||
const (
|
||||
// Reader
|
||||
defaultCommand = `find -L . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
|
||||
)
|
8
src/constants_windows.go
Normal file
8
src/constants_windows.go
Normal file
@@ -0,0 +1,8 @@
|
||||
// +build windows
|
||||
|
||||
package fzf
|
||||
|
||||
const (
|
||||
// Reader
|
||||
defaultCommand = `dir /s/b`
|
||||
)
|
@@ -3,7 +3,7 @@ Package fzf implements fzf, a command-line fuzzy finder.
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Junegunn Choi
|
||||
Copyright (c) 2017 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -143,7 +143,7 @@ func Run(opts *Options) {
|
||||
}
|
||||
patternBuilder := func(runes []rune) *Pattern {
|
||||
return BuildPattern(
|
||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, forward,
|
||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward,
|
||||
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
|
||||
}
|
||||
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
|
||||
|
@@ -1,714 +0,0 @@
|
||||
package curses
|
||||
|
||||
/*
|
||||
#include <ncurses.h>
|
||||
#include <locale.h>
|
||||
#cgo !static LDFLAGS: -lncurses
|
||||
#cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl
|
||||
#cgo android static LDFLAGS: -l:libncurses.a -fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch
|
||||
|
||||
SCREEN *c_newterm () {
|
||||
return newterm(NULL, stderr, stdin);
|
||||
}
|
||||
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
Bold = C.A_BOLD
|
||||
Dim = C.A_DIM
|
||||
Blink = C.A_BLINK
|
||||
Reverse = C.A_REVERSE
|
||||
Underline = C.A_UNDERLINE
|
||||
)
|
||||
|
||||
type Attr C.int
|
||||
|
||||
// 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'
|
||||
)
|
||||
|
||||
// Pallete
|
||||
const (
|
||||
_ = iota
|
||||
ColNormal
|
||||
ColPrompt
|
||||
ColMatch
|
||||
ColCurrent
|
||||
ColCurrentMatch
|
||||
ColSpinner
|
||||
ColInfo
|
||||
ColCursor
|
||||
ColSelected
|
||||
ColHeader
|
||||
ColBorder
|
||||
ColUser // Should be the last entry
|
||||
)
|
||||
|
||||
const (
|
||||
doubleClickDuration = 500 * time.Millisecond
|
||||
colDefault = -1
|
||||
colUndefined = -2
|
||||
)
|
||||
|
||||
type ColorTheme struct {
|
||||
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 bool
|
||||
_colorFn func(int, Attr) C.int
|
||||
_colorMap map[int]int
|
||||
_prevDownTime time.Time
|
||||
_clickY []int
|
||||
_screen *C.SCREEN
|
||||
Default16 *ColorTheme
|
||||
Dark256 *ColorTheme
|
||||
Light256 *ColorTheme
|
||||
)
|
||||
|
||||
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 _color {
|
||||
C.wbkgd(win, C.chtype(C.COLOR_PAIR(ColNormal)))
|
||||
}
|
||||
if border {
|
||||
attr := _colorFn(ColBorder, 0)
|
||||
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{
|
||||
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{
|
||||
Fg: colDefault,
|
||||
Bg: colDefault,
|
||||
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{
|
||||
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 attrColored(pair int, 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 int, a Attr) C.int {
|
||||
var attr C.int
|
||||
switch pair {
|
||||
case ColCurrent:
|
||||
if 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 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 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 Init(theme *ColorTheme, black bool, mouse bool) {
|
||||
{
|
||||
in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
panic("Failed to open /dev/tty")
|
||||
}
|
||||
_in = in
|
||||
// Break STDIN
|
||||
// syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd()))
|
||||
}
|
||||
|
||||
C.setlocale(C.LC_ALL, C.CString(""))
|
||||
_screen = C.c_newterm()
|
||||
if _screen == nil {
|
||||
fmt.Println("Invalid $TERM: " + os.Getenv("TERM"))
|
||||
os.Exit(2)
|
||||
}
|
||||
C.set_term(_screen)
|
||||
if mouse {
|
||||
C.mousemask(C.ALL_MOUSE_EVENTS, nil)
|
||||
}
|
||||
C.noecho()
|
||||
C.raw() // stty dsusp undef
|
||||
|
||||
_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)
|
||||
C.bkgd(C.chtype(C.COLOR_PAIR(ColNormal)))
|
||||
_colorFn = attrColored
|
||||
} else {
|
||||
_colorFn = attrMono
|
||||
}
|
||||
}
|
||||
|
||||
func override(baseTheme *ColorTheme, theme *ColorTheme) {
|
||||
o := func(a int16, b int16) int16 {
|
||||
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)
|
||||
}
|
||||
|
||||
func initPairs(baseTheme *ColorTheme, theme *ColorTheme, black bool) {
|
||||
if black {
|
||||
theme.Bg = C.COLOR_BLACK
|
||||
}
|
||||
// Updates theme
|
||||
override(baseTheme, theme)
|
||||
|
||||
C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg))
|
||||
initPair := func(group C.short, fg int16, bg int16) {
|
||||
C.init_pair(group, C.short(fg), C.short(bg))
|
||||
}
|
||||
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 Close() {
|
||||
C.endwin()
|
||||
C.delscreen(_screen)
|
||||
}
|
||||
|
||||
func GetBytes() []byte {
|
||||
c := getch(false)
|
||||
_buf = append(_buf, byte(c))
|
||||
|
||||
for {
|
||||
c = getch(true)
|
||||
if c == -1 {
|
||||
break
|
||||
}
|
||||
_buf = append(_buf, byte(c))
|
||||
}
|
||||
|
||||
return _buf
|
||||
}
|
||||
|
||||
// 27 (91 79) 77 type x y
|
||||
func mouseSequence(sz *int) Event {
|
||||
if len(_buf) < 6 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 6
|
||||
switch _buf[3] {
|
||||
case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl
|
||||
35, 39, 43, 51: // mouse-up / shift / cmd / ctrl
|
||||
mod := _buf[3] >= 36
|
||||
down := _buf[3]%2 == 0
|
||||
x := int(_buf[4] - 33)
|
||||
y := int(_buf[5] - 33)
|
||||
double := false
|
||||
if down {
|
||||
now := time.Now()
|
||||
if now.Sub(_prevDownTime) < doubleClickDuration {
|
||||
_clickY = append(_clickY, y)
|
||||
} else {
|
||||
_clickY = []int{y}
|
||||
}
|
||||
_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}}
|
||||
case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl
|
||||
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
|
||||
mod := _buf[3] >= 100
|
||||
s := 1 - int(_buf[3]%2)*2
|
||||
x := int(_buf[4] - 33)
|
||||
y := int(_buf[5] - 33)
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}}
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
func escSequence(sz *int) Event {
|
||||
if len(_buf) < 2 {
|
||||
return Event{ESC, 0, nil}
|
||||
}
|
||||
*sz = 2
|
||||
switch _buf[1] {
|
||||
case 13:
|
||||
return Event{AltEnter, 0, nil}
|
||||
case 32:
|
||||
return Event{AltSpace, 0, nil}
|
||||
case 47:
|
||||
return Event{AltSlash, 0, nil}
|
||||
case 98:
|
||||
return Event{AltB, 0, nil}
|
||||
case 100:
|
||||
return Event{AltD, 0, nil}
|
||||
case 102:
|
||||
return Event{AltF, 0, nil}
|
||||
case 127:
|
||||
return Event{AltBS, 0, nil}
|
||||
case 91, 79:
|
||||
if len(_buf) < 3 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 3
|
||||
switch _buf[2] {
|
||||
case 68:
|
||||
return Event{Left, 0, nil}
|
||||
case 67:
|
||||
return Event{Right, 0, nil}
|
||||
case 66:
|
||||
return Event{Down, 0, nil}
|
||||
case 65:
|
||||
return Event{Up, 0, nil}
|
||||
case 90:
|
||||
return Event{BTab, 0, nil}
|
||||
case 72:
|
||||
return Event{Home, 0, nil}
|
||||
case 70:
|
||||
return Event{End, 0, nil}
|
||||
case 77:
|
||||
return mouseSequence(sz)
|
||||
case 80:
|
||||
return Event{F1, 0, nil}
|
||||
case 81:
|
||||
return Event{F2, 0, nil}
|
||||
case 82:
|
||||
return Event{F3, 0, nil}
|
||||
case 83:
|
||||
return Event{F4, 0, nil}
|
||||
case 49, 50, 51, 52, 53, 54:
|
||||
if len(_buf) < 4 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 4
|
||||
switch _buf[2] {
|
||||
case 50:
|
||||
if len(_buf) == 5 && _buf[4] == 126 {
|
||||
*sz = 5
|
||||
switch _buf[3] {
|
||||
case 48:
|
||||
return Event{F9, 0, nil}
|
||||
case 49:
|
||||
return Event{F10, 0, nil}
|
||||
}
|
||||
}
|
||||
// Bracketed paste mode \e[200~ / \e[201
|
||||
if _buf[3] == 48 && (_buf[4] == 48 || _buf[4] == 49) && _buf[5] == 126 {
|
||||
*sz = 6
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
return Event{Invalid, 0, nil} // INS
|
||||
case 51:
|
||||
return Event{Del, 0, nil}
|
||||
case 52:
|
||||
return Event{End, 0, nil}
|
||||
case 53:
|
||||
return Event{PgUp, 0, nil}
|
||||
case 54:
|
||||
return Event{PgDn, 0, nil}
|
||||
case 49:
|
||||
switch _buf[3] {
|
||||
case 126:
|
||||
return Event{Home, 0, nil}
|
||||
case 53, 55, 56, 57:
|
||||
if len(_buf) == 5 && _buf[4] == 126 {
|
||||
*sz = 5
|
||||
switch _buf[3] {
|
||||
case 53:
|
||||
return Event{F5, 0, nil}
|
||||
case 55:
|
||||
return Event{F6, 0, nil}
|
||||
case 56:
|
||||
return Event{F7, 0, nil}
|
||||
case 57:
|
||||
return Event{F8, 0, nil}
|
||||
}
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
case 59:
|
||||
if len(_buf) != 6 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 6
|
||||
switch _buf[4] {
|
||||
case 50:
|
||||
switch _buf[5] {
|
||||
case 68:
|
||||
return Event{Home, 0, nil}
|
||||
case 67:
|
||||
return Event{End, 0, nil}
|
||||
}
|
||||
case 53:
|
||||
switch _buf[5] {
|
||||
case 68:
|
||||
return Event{SLeft, 0, nil}
|
||||
case 67:
|
||||
return Event{SRight, 0, nil}
|
||||
}
|
||||
} // _buf[4]
|
||||
} // _buf[3]
|
||||
} // _buf[2]
|
||||
} // _buf[2]
|
||||
} // _buf[1]
|
||||
if _buf[1] >= 'a' && _buf[1] <= 'z' {
|
||||
return Event{AltA + int(_buf[1]) - 'a', 0, nil}
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
func GetChar() Event {
|
||||
if len(_buf) == 0 {
|
||||
_buf = GetBytes()
|
||||
}
|
||||
if len(_buf) == 0 {
|
||||
panic("Empty _buffer")
|
||||
}
|
||||
|
||||
sz := 1
|
||||
defer func() {
|
||||
_buf = _buf[sz:]
|
||||
}()
|
||||
|
||||
switch _buf[0] {
|
||||
case CtrlC:
|
||||
return Event{CtrlC, 0, nil}
|
||||
case CtrlG:
|
||||
return Event{CtrlG, 0, nil}
|
||||
case CtrlQ:
|
||||
return Event{CtrlQ, 0, nil}
|
||||
case 127:
|
||||
return Event{BSpace, 0, nil}
|
||||
case ESC:
|
||||
return escSequence(&sz)
|
||||
}
|
||||
|
||||
// CTRL-A ~ CTRL-Z
|
||||
if _buf[0] <= CtrlZ {
|
||||
return Event{int(_buf[0]), 0, nil}
|
||||
}
|
||||
r, rsz := utf8.DecodeRune(_buf)
|
||||
if r == utf8.RuneError {
|
||||
return Event{ESC, 0, nil}
|
||||
}
|
||||
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, 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()
|
||||
}
|
||||
|
||||
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, 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 (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
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
package curses
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPairFor(t *testing.T) {
|
||||
if PairFor(30, 50) != PairFor(30, 50) {
|
||||
t.Fail()
|
||||
}
|
||||
if PairFor(-1, 10) != PairFor(-1, 10) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
18
src/deps
Executable file
18
src/deps
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ -z "$GOPATH" ]; then
|
||||
echo '$GOPATH not defined'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
reset() (
|
||||
cd "$GOPATH/src/$1"
|
||||
export GIT_DIR="$(pwd)/.git"
|
||||
[ "$(git rev-parse HEAD)" = "$2" ] ||
|
||||
(git fetch && git reset --hard "$2")
|
||||
)
|
||||
|
||||
reset github.com/junegunn/go-isatty 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8
|
||||
reset github.com/junegunn/go-runewidth 14207d285c6c197daabb5c9793d63e7af9ab2d50
|
||||
reset github.com/junegunn/go-shellwords 33bd8f1ebe16d6e5eb688cc885749a63059e9167
|
||||
reset golang.org/x/crypto abc5fa7ad02123a41f02bf1391c9760f7586e608
|
@@ -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))
|
||||
}
|
||||
|
549
src/options.go
549
src/options.go
@@ -9,7 +9,8 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/junegunn/fzf/src/algo"
|
||||
"github.com/junegunn/fzf/src/curses"
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
|
||||
"github.com/junegunn/go-shellwords"
|
||||
)
|
||||
@@ -23,6 +24,7 @@ const usage = `usage: fzf [options]
|
||||
--algo=TYPE Fuzzy matching algorithm: [v1|v2] (default: v2)
|
||||
-i Case-insensitive match (default: smart-case match)
|
||||
+i Case-sensitive match
|
||||
--literal Do not normalize latin script letters before matching
|
||||
-n, --nth=N[,..] Comma-separated list of field index expressions
|
||||
for limiting search scope. Each can be a non-zero
|
||||
integer or a range expression ([BEGIN]..[END]).
|
||||
@@ -43,10 +45,16 @@ const usage = `usage: fzf [options]
|
||||
--no-hscroll Disable horizontal scroll
|
||||
--hscroll-off=COL Number of screen columns to keep to the right of the
|
||||
highlighted substring (default: 10)
|
||||
--filepath-word Make word-wise movements respect path separators
|
||||
--jump-labels=CHARS Label characters for jump and jump-accept
|
||||
|
||||
Layout
|
||||
--height=HEIGHT[%] Display fzf window below the cursor with the given
|
||||
height instead of using fullscreen
|
||||
--min-height=HEIGHT Minimum height when --height is given in percent
|
||||
(default: 10)
|
||||
--reverse Reverse orientation
|
||||
--border Draw border above and below the finder
|
||||
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
|
||||
--inline-info Display finder info inline with the query
|
||||
--prompt=STR Input prompt (default: '> ')
|
||||
@@ -57,6 +65,7 @@ const usage = `usage: fzf [options]
|
||||
--ansi Enable processing of ANSI color codes
|
||||
--tabstop=SPACES Number of spaces for a tab character (default: 8)
|
||||
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
|
||||
--no-bold Do not use bold text
|
||||
|
||||
History
|
||||
--history=FILE History file
|
||||
@@ -65,7 +74,7 @@ const usage = `usage: fzf [options]
|
||||
Preview
|
||||
--preview=COMMAND Command to preview highlighted line ({})
|
||||
--preview-window=OPT Preview window layout (default: right:50%)
|
||||
[up|down|left|right][:SIZE[%]][:hidden]
|
||||
[up|down|left|right][:SIZE[%]][:wrap][:hidden]
|
||||
|
||||
Scripting
|
||||
-q, --query=STR Start the finder with the given query
|
||||
@@ -74,6 +83,8 @@ const usage = `usage: fzf [options]
|
||||
-f, --filter=STR Filter mode. Do not start interactive finder.
|
||||
--print-query Print query as the first line
|
||||
--expect=KEYS Comma-separated list of keys to complete fzf
|
||||
--read0 Read input delimited by ASCII NUL characters
|
||||
--print0 Print output delimited by ASCII NUL characters
|
||||
--sync Synchronous search for multi-staged filtering
|
||||
|
||||
Environment variables
|
||||
@@ -125,6 +136,7 @@ type previewOpts struct {
|
||||
position windowPosition
|
||||
size sizeSpec
|
||||
hidden bool
|
||||
wrap bool
|
||||
}
|
||||
|
||||
// Options stores the values of command-line options
|
||||
@@ -133,6 +145,7 @@ type Options struct {
|
||||
FuzzyAlgo algo.Algo
|
||||
Extended bool
|
||||
Case Case
|
||||
Normalize bool
|
||||
Nth []Range
|
||||
WithNth []Range
|
||||
Delimiter Delimiter
|
||||
@@ -142,12 +155,16 @@ type Options struct {
|
||||
Multi bool
|
||||
Ansi bool
|
||||
Mouse bool
|
||||
Theme *curses.ColorTheme
|
||||
Theme *tui.ColorTheme
|
||||
Black bool
|
||||
Bold bool
|
||||
Height sizeSpec
|
||||
MinHeight int
|
||||
Reverse bool
|
||||
Cycle bool
|
||||
Hscroll bool
|
||||
HscrollOff int
|
||||
FileWord bool
|
||||
InlineInfo bool
|
||||
JumpLabels string
|
||||
Prompt string
|
||||
@@ -157,8 +174,7 @@ type Options struct {
|
||||
Filter *string
|
||||
ToggleSort bool
|
||||
Expect map[int]string
|
||||
Keymap map[int]actionType
|
||||
Execmap map[int]string
|
||||
Keymap map[int][]action
|
||||
Preview previewOpts
|
||||
PrintQuery bool
|
||||
ReadZero bool
|
||||
@@ -168,6 +184,7 @@ type Options struct {
|
||||
Header []string
|
||||
HeaderLines int
|
||||
Margin [4]sizeSpec
|
||||
Bordered bool
|
||||
Tabstop int
|
||||
Version bool
|
||||
}
|
||||
@@ -178,6 +195,7 @@ func defaultOptions() *Options {
|
||||
FuzzyAlgo: algo.FuzzyMatchV2,
|
||||
Extended: true,
|
||||
Case: CaseSmart,
|
||||
Normalize: true,
|
||||
Nth: make([]Range, 0),
|
||||
WithNth: make([]Range, 0),
|
||||
Delimiter: Delimiter{},
|
||||
@@ -187,12 +205,15 @@ func defaultOptions() *Options {
|
||||
Multi: false,
|
||||
Ansi: false,
|
||||
Mouse: true,
|
||||
Theme: curses.EmptyTheme(),
|
||||
Theme: tui.EmptyTheme(),
|
||||
Black: false,
|
||||
Bold: true,
|
||||
MinHeight: 10,
|
||||
Reverse: false,
|
||||
Cycle: false,
|
||||
Hscroll: true,
|
||||
HscrollOff: 10,
|
||||
FileWord: false,
|
||||
InlineInfo: false,
|
||||
JumpLabels: defaultJumpLabels,
|
||||
Prompt: "> ",
|
||||
@@ -202,9 +223,8 @@ func defaultOptions() *Options {
|
||||
Filter: nil,
|
||||
ToggleSort: false,
|
||||
Expect: make(map[int]string),
|
||||
Keymap: make(map[int]actionType),
|
||||
Execmap: make(map[int]string),
|
||||
Preview: previewOpts{"", posRight, sizeSpec{50, true}, false},
|
||||
Keymap: make(map[int][]action),
|
||||
Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false},
|
||||
PrintQuery: false,
|
||||
ReadZero: false,
|
||||
Printer: func(str string) { fmt.Println(str) },
|
||||
@@ -327,6 +347,10 @@ func isAlphabet(char uint8) bool {
|
||||
return char >= 'a' && char <= 'z'
|
||||
}
|
||||
|
||||
func isNumeric(char uint8) bool {
|
||||
return char >= '0' && char <= '9'
|
||||
}
|
||||
|
||||
func parseAlgo(str string) algo.Algo {
|
||||
switch str {
|
||||
case "v1":
|
||||
@@ -358,60 +382,68 @@ 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 "ctrl-space":
|
||||
chord = tui.CtrlSpace
|
||||
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
|
||||
case "f11":
|
||||
chord = tui.F11
|
||||
case "f12":
|
||||
chord = tui.F12
|
||||
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) == 5 && strings.HasPrefix(lkey, "alt-") && isNumeric(lkey[4]) {
|
||||
chord = tui.Alt0 + int(lkey[4]) - '0'
|
||||
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' {
|
||||
chord = 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 +490,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 +498,17 @@ 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)
|
||||
rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$")
|
||||
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:
|
||||
@@ -491,11 +524,17 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme
|
||||
if len(pair) != 2 {
|
||||
fail()
|
||||
}
|
||||
ansi32, err := strconv.Atoi(pair[1])
|
||||
if err != nil || ansi32 < -1 || ansi32 > 255 {
|
||||
fail()
|
||||
|
||||
var ansi tui.Color
|
||||
if rrggbb.MatchString(pair[1]) {
|
||||
ansi = tui.HexToColor(pair[1])
|
||||
} else {
|
||||
ansi32, err := strconv.Atoi(pair[1])
|
||||
if err != nil || ansi32 < -1 || ansi32 > 255 {
|
||||
fail()
|
||||
}
|
||||
ansi = tui.Color(ansi32)
|
||||
}
|
||||
ansi := int16(ansi32)
|
||||
switch pair[0] {
|
||||
case "fg":
|
||||
theme.Fg = ansi
|
||||
@@ -543,23 +582,32 @@ func firstKey(keymap map[int]string) int {
|
||||
const (
|
||||
escapedColon = 0
|
||||
escapedComma = 1
|
||||
escapedPlus = 2
|
||||
)
|
||||
|
||||
func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) {
|
||||
if executeRegexp == nil {
|
||||
// Backreferences are not supported.
|
||||
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
|
||||
executeRegexp = regexp.MustCompile(
|
||||
"(?s):execute(-multi)?:.*|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
|
||||
}
|
||||
func init() {
|
||||
// Backreferences are not supported.
|
||||
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
|
||||
executeRegexp = regexp.MustCompile(
|
||||
"(?si):(execute(?:-multi|-silent)?):.+|:(execute(?:-multi|-silent)?)(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
|
||||
}
|
||||
|
||||
func parseKeymap(keymap map[int][]action, str string) {
|
||||
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
|
||||
if strings.HasPrefix(src, ":execute-multi") {
|
||||
return ":execute-multi(" + strings.Repeat(" ", len(src)-len(":execute-multi()")) + ")"
|
||||
prefix := ":execute"
|
||||
if src[len(prefix)] == '-' {
|
||||
c := src[len(prefix)+1]
|
||||
if c == 's' || c == 'S' {
|
||||
prefix += "-silent"
|
||||
} else {
|
||||
prefix += "-multi"
|
||||
}
|
||||
}
|
||||
return ":execute(" + strings.Repeat(" ", len(src)-len(":execute()")) + ")"
|
||||
return prefix + "(" + strings.Repeat(" ", len(src)-len(prefix)-2) + ")"
|
||||
})
|
||||
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
|
||||
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
|
||||
masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1)
|
||||
|
||||
idx := 0
|
||||
for _, pairStr := range strings.Split(masked, ",") {
|
||||
@@ -572,150 +620,177 @@ 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 if len(pair[0]) == 1 && pair[0][0] == escapedPlus {
|
||||
key = '+' + tui.AltZ
|
||||
} else {
|
||||
keys := parseKeyChords(pair[0], "key name required")
|
||||
key = firstKey(keys)
|
||||
}
|
||||
|
||||
act := origPairStr[len(pair[0])+1 : len(origPairStr)]
|
||||
actLower := strings.ToLower(act)
|
||||
switch actLower {
|
||||
case "ignore":
|
||||
keymap[key] = actIgnore
|
||||
case "beginning-of-line":
|
||||
keymap[key] = actBeginningOfLine
|
||||
case "abort":
|
||||
keymap[key] = actAbort
|
||||
case "accept":
|
||||
keymap[key] = actAccept
|
||||
case "print-query":
|
||||
keymap[key] = actPrintQuery
|
||||
case "backward-char":
|
||||
keymap[key] = actBackwardChar
|
||||
case "backward-delete-char":
|
||||
keymap[key] = actBackwardDeleteChar
|
||||
case "backward-word":
|
||||
keymap[key] = actBackwardWord
|
||||
case "clear-screen":
|
||||
keymap[key] = actClearScreen
|
||||
case "delete-char":
|
||||
keymap[key] = actDeleteChar
|
||||
case "delete-char/eof":
|
||||
keymap[key] = actDeleteCharEOF
|
||||
case "end-of-line":
|
||||
keymap[key] = actEndOfLine
|
||||
case "cancel":
|
||||
keymap[key] = actCancel
|
||||
case "forward-char":
|
||||
keymap[key] = actForwardChar
|
||||
case "forward-word":
|
||||
keymap[key] = actForwardWord
|
||||
case "jump":
|
||||
keymap[key] = actJump
|
||||
case "jump-accept":
|
||||
keymap[key] = actJumpAccept
|
||||
case "kill-line":
|
||||
keymap[key] = actKillLine
|
||||
case "kill-word":
|
||||
keymap[key] = actKillWord
|
||||
case "unix-line-discard", "line-discard":
|
||||
keymap[key] = actUnixLineDiscard
|
||||
case "unix-word-rubout", "word-rubout":
|
||||
keymap[key] = actUnixWordRubout
|
||||
case "yank":
|
||||
keymap[key] = actYank
|
||||
case "backward-kill-word":
|
||||
keymap[key] = actBackwardKillWord
|
||||
case "toggle-down":
|
||||
keymap[key] = actToggleDown
|
||||
case "toggle-up":
|
||||
keymap[key] = actToggleUp
|
||||
case "toggle-in":
|
||||
keymap[key] = actToggleIn
|
||||
case "toggle-out":
|
||||
keymap[key] = actToggleOut
|
||||
case "toggle-all":
|
||||
keymap[key] = actToggleAll
|
||||
case "select-all":
|
||||
keymap[key] = actSelectAll
|
||||
case "deselect-all":
|
||||
keymap[key] = actDeselectAll
|
||||
case "toggle":
|
||||
keymap[key] = actToggle
|
||||
case "down":
|
||||
keymap[key] = actDown
|
||||
case "up":
|
||||
keymap[key] = actUp
|
||||
case "page-up":
|
||||
keymap[key] = actPageUp
|
||||
case "page-down":
|
||||
keymap[key] = actPageDown
|
||||
case "previous-history":
|
||||
keymap[key] = actPreviousHistory
|
||||
case "next-history":
|
||||
keymap[key] = actNextHistory
|
||||
case "toggle-preview":
|
||||
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
|
||||
if strings.HasPrefix(actLower, "execute-multi") {
|
||||
keymap[key] = actExecuteMulti
|
||||
offset = len("execute-multi")
|
||||
idx2 := len(pair[0]) + 1
|
||||
specs := strings.Split(pair[1], "+")
|
||||
actions := make([]action, 0, len(specs))
|
||||
appendAction := func(types ...actionType) {
|
||||
actions = append(actions, toActions(types...)...)
|
||||
}
|
||||
prevSpec := ""
|
||||
for specIndex, maskedSpec := range specs {
|
||||
spec := origPairStr[idx2 : idx2+len(maskedSpec)]
|
||||
idx2 += len(maskedSpec) + 1
|
||||
spec = prevSpec + spec
|
||||
specLower := strings.ToLower(spec)
|
||||
switch specLower {
|
||||
case "ignore":
|
||||
appendAction(actIgnore)
|
||||
case "beginning-of-line":
|
||||
appendAction(actBeginningOfLine)
|
||||
case "abort":
|
||||
appendAction(actAbort)
|
||||
case "accept":
|
||||
appendAction(actAccept)
|
||||
case "print-query":
|
||||
appendAction(actPrintQuery)
|
||||
case "backward-char":
|
||||
appendAction(actBackwardChar)
|
||||
case "backward-delete-char":
|
||||
appendAction(actBackwardDeleteChar)
|
||||
case "backward-word":
|
||||
appendAction(actBackwardWord)
|
||||
case "clear-screen":
|
||||
appendAction(actClearScreen)
|
||||
case "delete-char":
|
||||
appendAction(actDeleteChar)
|
||||
case "delete-char/eof":
|
||||
appendAction(actDeleteCharEOF)
|
||||
case "end-of-line":
|
||||
appendAction(actEndOfLine)
|
||||
case "cancel":
|
||||
appendAction(actCancel)
|
||||
case "forward-char":
|
||||
appendAction(actForwardChar)
|
||||
case "forward-word":
|
||||
appendAction(actForwardWord)
|
||||
case "jump":
|
||||
appendAction(actJump)
|
||||
case "jump-accept":
|
||||
appendAction(actJumpAccept)
|
||||
case "kill-line":
|
||||
appendAction(actKillLine)
|
||||
case "kill-word":
|
||||
appendAction(actKillWord)
|
||||
case "unix-line-discard", "line-discard":
|
||||
appendAction(actUnixLineDiscard)
|
||||
case "unix-word-rubout", "word-rubout":
|
||||
appendAction(actUnixWordRubout)
|
||||
case "yank":
|
||||
appendAction(actYank)
|
||||
case "backward-kill-word":
|
||||
appendAction(actBackwardKillWord)
|
||||
case "toggle-down":
|
||||
appendAction(actToggle, actDown)
|
||||
case "toggle-up":
|
||||
appendAction(actToggle, actUp)
|
||||
case "toggle-in":
|
||||
appendAction(actToggleIn)
|
||||
case "toggle-out":
|
||||
appendAction(actToggleOut)
|
||||
case "toggle-all":
|
||||
appendAction(actToggleAll)
|
||||
case "select-all":
|
||||
appendAction(actSelectAll)
|
||||
case "deselect-all":
|
||||
appendAction(actDeselectAll)
|
||||
case "toggle":
|
||||
appendAction(actToggle)
|
||||
case "down":
|
||||
appendAction(actDown)
|
||||
case "up":
|
||||
appendAction(actUp)
|
||||
case "page-up":
|
||||
appendAction(actPageUp)
|
||||
case "page-down":
|
||||
appendAction(actPageDown)
|
||||
case "half-page-up":
|
||||
appendAction(actHalfPageUp)
|
||||
case "half-page-down":
|
||||
appendAction(actHalfPageDown)
|
||||
case "previous-history":
|
||||
appendAction(actPreviousHistory)
|
||||
case "next-history":
|
||||
appendAction(actNextHistory)
|
||||
case "toggle-preview":
|
||||
appendAction(actTogglePreview)
|
||||
case "toggle-sort":
|
||||
appendAction(actToggleSort)
|
||||
case "preview-up":
|
||||
appendAction(actPreviewUp)
|
||||
case "preview-down":
|
||||
appendAction(actPreviewDown)
|
||||
case "preview-page-up":
|
||||
appendAction(actPreviewPageUp)
|
||||
case "preview-page-down":
|
||||
appendAction(actPreviewPageDown)
|
||||
default:
|
||||
t := isExecuteAction(specLower)
|
||||
if t == actIgnore {
|
||||
errorExit("unknown action: " + spec)
|
||||
} else {
|
||||
keymap[key] = actExecute
|
||||
offset = len("execute")
|
||||
var offset int
|
||||
switch t {
|
||||
case actExecuteSilent:
|
||||
offset = len("execute-silent")
|
||||
case actExecuteMulti:
|
||||
offset = len("execute-multi")
|
||||
default:
|
||||
offset = len("execute")
|
||||
}
|
||||
if spec[offset] == ':' {
|
||||
if specIndex == len(specs)-1 {
|
||||
actions = append(actions, action{t: t, a: spec[offset+1:]})
|
||||
} else {
|
||||
prevSpec = spec + "+"
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
actions = append(actions, action{t: t, a: spec[offset+1 : len(spec)-1]})
|
||||
}
|
||||
}
|
||||
if act[offset] == ':' {
|
||||
execmap[key] = act[offset+1:]
|
||||
} else {
|
||||
execmap[key] = act[offset+1 : len(act)-1]
|
||||
}
|
||||
} else {
|
||||
errorExit("unknown action: " + act)
|
||||
}
|
||||
prevSpec = ""
|
||||
}
|
||||
keymap[key] = actions
|
||||
}
|
||||
}
|
||||
|
||||
func isExecuteAction(str string) bool {
|
||||
if !strings.HasPrefix(str, "execute") || len(str) < len("execute()") {
|
||||
return false
|
||||
func isExecuteAction(str string) actionType {
|
||||
matches := executeRegexp.FindAllStringSubmatch(":"+str, -1)
|
||||
if matches == nil || len(matches) != 1 {
|
||||
return actIgnore
|
||||
}
|
||||
b := str[len("execute")]
|
||||
if strings.HasPrefix(str, "execute-multi") {
|
||||
if len(str) < len("execute-multi()") {
|
||||
return false
|
||||
}
|
||||
b = str[len("execute-multi")]
|
||||
prefix := matches[0][1]
|
||||
if len(prefix) == 0 {
|
||||
prefix = matches[0][2]
|
||||
}
|
||||
e := str[len(str)-1]
|
||||
if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' ||
|
||||
b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") {
|
||||
return true
|
||||
switch prefix {
|
||||
case "execute":
|
||||
return actExecute
|
||||
case "execute-silent":
|
||||
return actExecuteSilent
|
||||
case "execute-multi":
|
||||
return actExecuteMulti
|
||||
}
|
||||
return false
|
||||
return actIgnore
|
||||
}
|
||||
|
||||
func parseToggleSort(keymap map[int]actionType, str string) {
|
||||
func parseToggleSort(keymap map[int][]action, str string) {
|
||||
keys := parseKeyChords(str, "key name required")
|
||||
if len(keys) != 1 {
|
||||
errorExit("multiple keys specified")
|
||||
}
|
||||
keymap[firstKey(keys)] = actToggleSort
|
||||
keymap[firstKey(keys)] = toActions(actToggleSort)
|
||||
}
|
||||
|
||||
func strLines(str string) []string {
|
||||
@@ -746,40 +821,52 @@ func parseSize(str string, maxPercent float64, label string) sizeSpec {
|
||||
return sizeSpec{val, percent}
|
||||
}
|
||||
|
||||
func parseHeight(str string) sizeSpec {
|
||||
if util.IsWindows() {
|
||||
errorExit("--height options is currently not supported on Windows")
|
||||
}
|
||||
size := parseSize(str, 100, "height")
|
||||
return size
|
||||
}
|
||||
|
||||
func parsePreviewWindow(opts *previewOpts, input string) {
|
||||
layout := input
|
||||
// Default
|
||||
opts.position = posRight
|
||||
opts.size = sizeSpec{50, true}
|
||||
opts.hidden = false
|
||||
if strings.HasSuffix(layout, ":hidden") {
|
||||
opts.hidden = true
|
||||
layout = strings.TrimSuffix(layout, ":hidden")
|
||||
}
|
||||
opts.wrap = false
|
||||
|
||||
tokens := strings.Split(layout, ":")
|
||||
if len(tokens) == 0 || len(tokens) > 2 {
|
||||
errorExit("invalid window layout: " + input)
|
||||
}
|
||||
|
||||
if len(tokens) > 1 {
|
||||
opts.size = parseSize(tokens[1], 99, "window size")
|
||||
} else {
|
||||
opts.size = sizeSpec{50, true}
|
||||
tokens := strings.Split(input, ":")
|
||||
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
|
||||
for _, token := range tokens {
|
||||
switch token {
|
||||
case "hidden":
|
||||
opts.hidden = true
|
||||
case "wrap":
|
||||
opts.wrap = true
|
||||
case "up", "top":
|
||||
opts.position = posUp
|
||||
case "down", "bottom":
|
||||
opts.position = posDown
|
||||
case "left":
|
||||
opts.position = posLeft
|
||||
case "right":
|
||||
opts.position = posRight
|
||||
default:
|
||||
if sizeRegex.MatchString(token) {
|
||||
opts.size = parseSize(token, 99, "window size")
|
||||
} else {
|
||||
errorExit("invalid preview window layout: " + input)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !opts.size.percent && opts.size.size > 0 {
|
||||
// Adjust size for border
|
||||
opts.size.size += 2
|
||||
}
|
||||
|
||||
switch tokens[0] {
|
||||
case "up":
|
||||
opts.position = posUp
|
||||
case "down":
|
||||
opts.position = posDown
|
||||
case "left":
|
||||
opts.position = posLeft
|
||||
case "right":
|
||||
opts.position = posRight
|
||||
default:
|
||||
errorExit("invalid window position: " + input)
|
||||
// And padding
|
||||
if opts.position == posLeft || opts.position == posRight {
|
||||
opts.size.size += 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -857,6 +944,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
case "-f", "--filter":
|
||||
filter := nextString(allArgs, &i, "query string required")
|
||||
opts.Filter = &filter
|
||||
case "--literal":
|
||||
opts.Normalize = false
|
||||
case "--no-literal":
|
||||
opts.Normalize = true
|
||||
case "--algo":
|
||||
opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)"))
|
||||
case "--expect":
|
||||
@@ -864,11 +955,11 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
case "--tiebreak":
|
||||
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
|
||||
case "--bind":
|
||||
parseKeymap(opts.Keymap, opts.Execmap, nextString(allArgs, &i, "bind expression required"))
|
||||
parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"))
|
||||
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)
|
||||
}
|
||||
@@ -905,11 +996,15 @@ 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":
|
||||
opts.Black = false
|
||||
case "--bold":
|
||||
opts.Bold = true
|
||||
case "--no-bold":
|
||||
opts.Bold = false
|
||||
case "--reverse":
|
||||
opts.Reverse = true
|
||||
case "--no-reverse":
|
||||
@@ -924,6 +1019,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.Hscroll = false
|
||||
case "--hscroll-off":
|
||||
opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required")
|
||||
case "--filepath-word":
|
||||
opts.FileWord = true
|
||||
case "--no-filepath-word":
|
||||
opts.FileWord = false
|
||||
case "--inline-info":
|
||||
opts.InlineInfo = true
|
||||
case "--no-inline-info":
|
||||
@@ -980,9 +1079,19 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.Preview.command = ""
|
||||
case "--preview-window":
|
||||
parsePreviewWindow(&opts.Preview,
|
||||
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]]"))
|
||||
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:wrap][:hidden]"))
|
||||
case "--height":
|
||||
opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]"))
|
||||
case "--min-height":
|
||||
opts.MinHeight = nextInt(allArgs, &i, "height required: HEIGHT")
|
||||
case "--no-height":
|
||||
opts.Height = sizeSpec{}
|
||||
case "--no-margin":
|
||||
opts.Margin = defaultMargin()
|
||||
case "--no-border":
|
||||
opts.Bordered = false
|
||||
case "--border":
|
||||
opts.Bordered = true
|
||||
case "--margin":
|
||||
opts.Margin = parseMargin(
|
||||
nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
|
||||
@@ -1007,6 +1116,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.WithNth = splitNth(value)
|
||||
} else if match, _ := optString(arg, "-s", "--sort="); match {
|
||||
opts.Sort = 1 // Don't care
|
||||
} else if match, value := optString(arg, "--height="); match {
|
||||
opts.Height = parseHeight(value)
|
||||
} else if match, value := optString(arg, "--min-height="); match {
|
||||
opts.MinHeight = atoi(value)
|
||||
} else if match, value := optString(arg, "--toggle-sort="); match {
|
||||
parseToggleSort(opts.Keymap, value)
|
||||
} else if match, value := optString(arg, "--expect="); match {
|
||||
@@ -1016,7 +1129,7 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
} else if match, value := optString(arg, "--color="); match {
|
||||
opts.Theme = parseTheme(opts.Theme, value)
|
||||
} else if match, value := optString(arg, "--bind="); match {
|
||||
parseKeymap(opts.Keymap, opts.Execmap, value)
|
||||
parseKeymap(opts.Keymap, value)
|
||||
} else if match, value := optString(arg, "--history="); match {
|
||||
setHistory(value)
|
||||
} else if match, value := optString(arg, "--history-size="); match {
|
||||
@@ -1071,21 +1184,23 @@ 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] = toActions(actPreviousHistory)
|
||||
}
|
||||
if _, prs := opts.Keymap[curses.CtrlN]; !prs {
|
||||
opts.Keymap[curses.CtrlN] = actNextHistory
|
||||
if _, prs := opts.Keymap[tui.CtrlN]; !prs {
|
||||
opts.Keymap[tui.CtrlN] = toActions(actNextHistory)
|
||||
}
|
||||
}
|
||||
|
||||
// Extend the default key map
|
||||
keymap := defaultKeymap()
|
||||
for key, act := range opts.Keymap {
|
||||
if act == actToggleSort {
|
||||
opts.ToggleSort = true
|
||||
for key, actions := range opts.Keymap {
|
||||
for _, act := range actions {
|
||||
if act.t == actToggleSort {
|
||||
opts.ToggleSort = true
|
||||
}
|
||||
}
|
||||
keymap[key] = act
|
||||
keymap[key] = actions
|
||||
}
|
||||
opts.Keymap = keymap
|
||||
|
||||
|
@@ -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,86 +192,88 @@ 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) {
|
||||
check := func(action actionType, expected actionType) {
|
||||
if action != expected {
|
||||
t.Errorf("%d != %d", action, expected)
|
||||
}
|
||||
}
|
||||
checkString := func(action string, expected string) {
|
||||
if action != expected {
|
||||
t.Errorf("%d != %d", action, expected)
|
||||
}
|
||||
}
|
||||
keymap := defaultKeymap()
|
||||
execmap := make(map[int]string)
|
||||
check(actBeginningOfLine, keymap[curses.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 := func(keyName int, arg1 string, types ...actionType) {
|
||||
if len(keymap[keyName]) != len(types) {
|
||||
t.Errorf("invalid number of actions (%d != %d)", len(types), len(keymap[keyName]))
|
||||
return
|
||||
}
|
||||
for idx, action := range keymap[keyName] {
|
||||
if types[idx] != action.t {
|
||||
t.Errorf("invalid action type (%d != %d)", types[idx], action.t)
|
||||
}
|
||||
}
|
||||
if len(arg1) > 0 && keymap[keyName][0].a != arg1 {
|
||||
t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[keyName][0].a)
|
||||
}
|
||||
}
|
||||
check(tui.CtrlA, "", actBeginningOfLine)
|
||||
parseKeymap(keymap,
|
||||
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
|
||||
"f1:execute(ls {})+abort,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
|
||||
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
|
||||
"x:Execute(foo+bar),X:execute/bar+baz/"+
|
||||
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
|
||||
check(tui.CtrlA, "", actKillLine)
|
||||
check(tui.CtrlB, "", actToggleSort, actUp, actDown)
|
||||
check(tui.AltZ+'c', "", actPageUp)
|
||||
check(tui.AltZ+',', "", actAbort)
|
||||
check(tui.AltZ+':', "", actAccept)
|
||||
check(tui.AltZ, "", actPageDown)
|
||||
check(tui.F1, "ls {}", actExecute, actAbort)
|
||||
check(tui.F2, "echo {}, {}, {}", actExecute)
|
||||
check(tui.F3, "echo '({})'", actExecute)
|
||||
check(tui.F4, "less {}", actExecute)
|
||||
check(tui.AltZ+'x', "foo+bar", actExecute)
|
||||
check(tui.AltZ+'X', "bar+baz", actExecute)
|
||||
check(tui.AltA, "echo (,),[,],/,:,;,%,{}", actExecuteMulti)
|
||||
check(tui.AltB, "echo (,),[,],/,:,@,%,{}", actExecute)
|
||||
check(tui.AltZ+'+', "++\nfoobar,Y:execute(baz)+up", actExecute)
|
||||
|
||||
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
|
||||
parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
|
||||
checkString("foobar", execmap[curses.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])])
|
||||
parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
|
||||
check(tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
|
||||
}
|
||||
|
||||
parseKeymap(keymap, execmap, "f1:abort")
|
||||
check(actAbort, keymap[curses.F1])
|
||||
parseKeymap(keymap, "f1:abort")
|
||||
check(tui.F1, "", actAbort)
|
||||
}
|
||||
|
||||
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 +286,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,23 +297,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: %v, %v", curses.Dark256, customized)
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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.")
|
||||
@@ -326,25 +329,27 @@ func TestDefaultCtrlNP(t *testing.T) {
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
postProcessOptions(opts)
|
||||
if opts.Keymap[key] != expected {
|
||||
if opts.Keymap[key][0].t != expected {
|
||||
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/fzf-history"
|
||||
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 {
|
||||
@@ -375,26 +380,37 @@ func TestPreviewOpts(t *testing.T) {
|
||||
opts := optsFor()
|
||||
if !(opts.Preview.command == "" &&
|
||||
opts.Preview.hidden == false &&
|
||||
opts.Preview.wrap == false &&
|
||||
opts.Preview.position == posRight &&
|
||||
opts.Preview.size.percent == true &&
|
||||
opts.Preview.size.size == 50) {
|
||||
t.Error()
|
||||
}
|
||||
opts = optsFor("--preview", "cat {}", "--preview-window=left:15:hidden")
|
||||
opts = optsFor("--preview", "cat {}", "--preview-window=left:15:hidden:wrap")
|
||||
if !(opts.Preview.command == "cat {}" &&
|
||||
opts.Preview.hidden == true &&
|
||||
opts.Preview.wrap == true &&
|
||||
opts.Preview.position == posLeft &&
|
||||
opts.Preview.size.percent == false &&
|
||||
opts.Preview.size.size == 15+2) {
|
||||
opts.Preview.size.size == 15+2+2) {
|
||||
t.Error(opts.Preview)
|
||||
}
|
||||
|
||||
opts = optsFor("--preview-window=left:15:hidden", "--preview-window=down")
|
||||
opts = optsFor("--preview-window=up:15:wrap:hidden", "--preview-window=down")
|
||||
if !(opts.Preview.command == "" &&
|
||||
opts.Preview.hidden == false &&
|
||||
opts.Preview.wrap == false &&
|
||||
opts.Preview.position == posDown &&
|
||||
opts.Preview.size.percent == true &&
|
||||
opts.Preview.size.size == 50) {
|
||||
t.Error(opts.Preview)
|
||||
}
|
||||
opts = optsFor("--preview-window=up:15:wrap:hidden")
|
||||
if !(opts.Preview.command == "" &&
|
||||
opts.Preview.hidden == true &&
|
||||
opts.Preview.wrap == true &&
|
||||
opts.Preview.position == posUp &&
|
||||
opts.Preview.size.percent == false &&
|
||||
opts.Preview.size.size == 15+2) {
|
||||
t.Error(opts.Preview)
|
||||
}
|
||||
}
|
||||
|
@@ -43,6 +43,7 @@ type Pattern struct {
|
||||
fuzzyAlgo algo.Algo
|
||||
extended bool
|
||||
caseSensitive bool
|
||||
normalize bool
|
||||
forward bool
|
||||
text []rune
|
||||
termSets []termSet
|
||||
@@ -75,7 +76,7 @@ func clearChunkCache() {
|
||||
}
|
||||
|
||||
// BuildPattern builds Pattern object from the given arguments
|
||||
func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, forward bool,
|
||||
func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
|
||||
cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||
|
||||
var asString string
|
||||
@@ -94,13 +95,13 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
||||
termSets := []termSet{}
|
||||
|
||||
if extended {
|
||||
termSets = parseTerms(fuzzy, caseMode, asString)
|
||||
termSets = parseTerms(fuzzy, caseMode, normalize, asString)
|
||||
Loop:
|
||||
for _, termSet := range termSets {
|
||||
for idx, term := range termSet {
|
||||
// If the query contains inverse search terms or OR operators,
|
||||
// we cannot cache the search scope
|
||||
if !cacheable || idx > 0 || term.inv {
|
||||
if !cacheable || idx > 0 || term.inv || !fuzzy && term.typ != termExact {
|
||||
cacheable = false
|
||||
break Loop
|
||||
}
|
||||
@@ -120,6 +121,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
||||
fuzzyAlgo: fuzzyAlgo,
|
||||
extended: extended,
|
||||
caseSensitive: caseSensitive,
|
||||
normalize: normalize,
|
||||
forward: forward,
|
||||
text: []rune(asString),
|
||||
termSets: termSets,
|
||||
@@ -138,7 +140,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
||||
return ptr
|
||||
}
|
||||
|
||||
func parseTerms(fuzzy bool, caseMode Case, str string) []termSet {
|
||||
func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
|
||||
tokens := _splitRegex.Split(str, -1)
|
||||
sets := []termSet{}
|
||||
set := termSet{}
|
||||
@@ -194,10 +196,14 @@ func parseTerms(fuzzy bool, caseMode Case, str string) []termSet {
|
||||
sets = append(sets, set)
|
||||
set = termSet{}
|
||||
}
|
||||
textRunes := []rune(text)
|
||||
if normalize {
|
||||
textRunes = algo.NormalizeRunes(textRunes)
|
||||
}
|
||||
set = append(set, term{
|
||||
typ: typ,
|
||||
inv: inv,
|
||||
text: []rune(text),
|
||||
text: textRunes,
|
||||
caseSensitive: caseSensitive,
|
||||
origText: origText})
|
||||
switchSet = true
|
||||
@@ -309,9 +315,9 @@ func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result,
|
||||
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, int, *[]int) {
|
||||
input := p.prepareInput(item)
|
||||
if p.fuzzy {
|
||||
return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.forward, p.text, withPos, slab)
|
||||
return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab)
|
||||
}
|
||||
return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text, withPos, slab)
|
||||
return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab)
|
||||
}
|
||||
|
||||
func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, int, *[]int) {
|
||||
@@ -330,7 +336,7 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
|
||||
matched := false
|
||||
for _, term := range termSet {
|
||||
pfun := p.procFun[term.typ]
|
||||
off, score, tLen, pos := p.iter(pfun, input, term.caseSensitive, p.forward, term.text, withPos, slab)
|
||||
off, score, tLen, pos := p.iter(pfun, input, term.caseSensitive, p.normalize, p.forward, term.text, withPos, slab)
|
||||
if sidx := off[0]; sidx >= 0 {
|
||||
if term.inv {
|
||||
continue
|
||||
@@ -378,9 +384,9 @@ func (p *Pattern) prepareInput(item *Item) []Token {
|
||||
return ret
|
||||
}
|
||||
|
||||
func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, int, *[]int) {
|
||||
func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, normalize bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, int, *[]int) {
|
||||
for _, part := range tokens {
|
||||
if res, pos := pfun(caseSensitive, forward, *part.text, pattern, withPos, slab); res.Start >= 0 {
|
||||
if res, pos := pfun(caseSensitive, normalize, forward, *part.text, pattern, withPos, slab); res.Start >= 0 {
|
||||
sidx := int32(res.Start) + part.prefixLength
|
||||
eidx := int32(res.End) + part.prefixLength
|
||||
if pos != nil {
|
||||
|
@@ -15,7 +15,7 @@ func init() {
|
||||
}
|
||||
|
||||
func TestParseTermsExtended(t *testing.T) {
|
||||
terms := parseTerms(true, CaseSmart,
|
||||
terms := parseTerms(true, CaseSmart, false,
|
||||
"| aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | | zzz$ | !ZZZ |")
|
||||
if len(terms) != 9 ||
|
||||
terms[0][0].typ != termFuzzy || terms[0][0].inv ||
|
||||
@@ -50,7 +50,7 @@ func TestParseTermsExtended(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseTermsExtendedExact(t *testing.T) {
|
||||
terms := parseTerms(false, CaseSmart,
|
||||
terms := parseTerms(false, CaseSmart, false,
|
||||
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
|
||||
if len(terms) != 8 ||
|
||||
terms[0][0].typ != termExact || terms[0][0].inv || len(terms[0][0].text) != 3 ||
|
||||
@@ -66,7 +66,7 @@ func TestParseTermsExtendedExact(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseTermsEmpty(t *testing.T) {
|
||||
terms := parseTerms(true, CaseSmart, "' $ ^ !' !^ !$")
|
||||
terms := parseTerms(true, CaseSmart, false, "' $ ^ !' !^ !$")
|
||||
if len(terms) != 0 {
|
||||
t.Errorf("%s", terms)
|
||||
}
|
||||
@@ -75,10 +75,10 @@ func TestParseTermsEmpty(t *testing.T) {
|
||||
func TestExact(t *testing.T) {
|
||||
defer clearPatternCache()
|
||||
clearPatternCache()
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, true, true,
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true,
|
||||
[]Range{}, Delimiter{}, []rune("'abc"))
|
||||
res, pos := algo.ExactMatchNaive(
|
||||
pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text, true, nil)
|
||||
pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text, true, nil)
|
||||
if res.Start != 7 || res.End != 10 {
|
||||
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
|
||||
}
|
||||
@@ -90,11 +90,11 @@ func TestExact(t *testing.T) {
|
||||
func TestEqual(t *testing.T) {
|
||||
defer clearPatternCache()
|
||||
clearPatternCache()
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("^AbC$"))
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("^AbC$"))
|
||||
|
||||
match := func(str string, sidxExpected int, eidxExpected int) {
|
||||
res, pos := algo.EqualMatch(
|
||||
pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text, true, nil)
|
||||
pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text, true, nil)
|
||||
if res.Start != sidxExpected || res.End != eidxExpected {
|
||||
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
|
||||
}
|
||||
@@ -109,17 +109,17 @@ func TestEqual(t *testing.T) {
|
||||
func TestCaseSensitivity(t *testing.T) {
|
||||
defer clearPatternCache()
|
||||
clearPatternCache()
|
||||
pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
clearPatternCache()
|
||||
pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
clearPatternCache()
|
||||
pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
clearPatternCache()
|
||||
pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
clearPatternCache()
|
||||
pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
clearPatternCache()
|
||||
pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
|
||||
if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
|
||||
string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
|
||||
@@ -132,7 +132,7 @@ func TestCaseSensitivity(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOrigTextAndTransformed(t *testing.T) {
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("jg"))
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg"))
|
||||
tokens := Tokenize(util.RunesToChars([]rune("junegunn")), Delimiter{})
|
||||
trans := Transform(tokens, []Range{Range{1, 1}})
|
||||
|
||||
@@ -167,7 +167,7 @@ func TestOrigTextAndTransformed(t *testing.T) {
|
||||
|
||||
func TestCacheKey(t *testing.T) {
|
||||
test := func(extended bool, patStr string, expected string, cacheable bool) {
|
||||
pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, true, true, []Range{}, Delimiter{}, []rune(patStr))
|
||||
pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune(patStr))
|
||||
if pat.CacheKey() != expected {
|
||||
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
|
||||
}
|
||||
@@ -186,3 +186,21 @@ func TestCacheKey(t *testing.T) {
|
||||
test(true, "foo | bar !baz", "", false)
|
||||
test(true, "| | | foo", "foo", true)
|
||||
}
|
||||
|
||||
func TestCacheable(t *testing.T) {
|
||||
test := func(fuzzy bool, str string, cacheable bool) {
|
||||
clearPatternCache()
|
||||
pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, true, []Range{}, Delimiter{}, []rune(str))
|
||||
if cacheable != pat.cacheable {
|
||||
t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable)
|
||||
}
|
||||
}
|
||||
test(true, "foo bar", true)
|
||||
test(true, "foo 'bar", true)
|
||||
test(true, "foo !bar", false)
|
||||
|
||||
test(false, "foo bar", true)
|
||||
test(false, "foo '", true)
|
||||
test(false, "foo 'bar", false)
|
||||
test(false, "foo !bar", false)
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -5,7 +5,7 @@ import (
|
||||
"sort"
|
||||
"unicode"
|
||||
|
||||
"github.com/junegunn/fzf/src/curses"
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
@@ -14,8 +14,8 @@ type Offset [2]int32
|
||||
|
||||
type colorOffset struct {
|
||||
offset [2]int32
|
||||
color int
|
||||
attr curses.Attr
|
||||
color tui.ColorPair
|
||||
attr tui.Attr
|
||||
index int32
|
||||
}
|
||||
|
||||
@@ -37,12 +37,14 @@ func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result {
|
||||
result := Result{item: item, rank: rank{index: item.index}}
|
||||
numChars := item.text.Length()
|
||||
minBegin := math.MaxUint16
|
||||
minEnd := math.MaxUint16
|
||||
maxEnd := 0
|
||||
validOffsetFound := false
|
||||
for _, offset := range offsets {
|
||||
b, e := int(offset[0]), int(offset[1])
|
||||
if b < e {
|
||||
minBegin = util.Min(b, minBegin)
|
||||
minEnd = util.Min(e, minEnd)
|
||||
maxEnd = util.Max(e, maxEnd)
|
||||
validOffsetFound = true
|
||||
}
|
||||
@@ -68,7 +70,7 @@ func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result {
|
||||
}
|
||||
}
|
||||
if criterion == byBegin {
|
||||
val = util.AsUint16(minBegin - whitePrefixLen)
|
||||
val = util.AsUint16(minEnd - whitePrefixLen)
|
||||
} else {
|
||||
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/trimLen)
|
||||
}
|
||||
@@ -92,7 +94,7 @@ func minRank() rank {
|
||||
return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
||||
}
|
||||
|
||||
func (result *Result) colorOffsets(matchOffsets []Offset, theme *curses.ColorTheme, color int, attr curses.Attr, 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
|
||||
@@ -147,25 +149,27 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *curses.ColorThe
|
||||
} else {
|
||||
ansi := itemColors[curr-1]
|
||||
fg := ansi.color.fg
|
||||
if fg == -1 {
|
||||
if current {
|
||||
fg = int(theme.Current)
|
||||
} else {
|
||||
fg = int(theme.Fg)
|
||||
}
|
||||
}
|
||||
bg := ansi.color.bg
|
||||
if bg == -1 {
|
||||
if current {
|
||||
bg = int(theme.DarkBg)
|
||||
} else {
|
||||
bg = int(theme.Bg)
|
||||
if theme != nil {
|
||||
if fg == -1 {
|
||||
if current {
|
||||
fg = theme.Current
|
||||
} else {
|
||||
fg = theme.Fg
|
||||
}
|
||||
}
|
||||
if bg == -1 {
|
||||
if current {
|
||||
bg = theme.DarkBg
|
||||
} else {
|
||||
bg = theme.Bg
|
||||
}
|
||||
}
|
||||
}
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)},
|
||||
color: curses.PairFor(fg, bg),
|
||||
attr: ansi.color.attr | attr})
|
||||
color: tui.NewColorPair(fg, bg),
|
||||
attr: ansi.color.attr.Merge(attr)})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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"
|
||||
)
|
||||
|
||||
@@ -98,26 +100,27 @@ func TestColorOffset(t *testing.T) {
|
||||
item: &Item{
|
||||
colors: &[]ansiOffset{
|
||||
ansiOffset{[2]int32{0, 20}, ansiState{1, 5, 0}},
|
||||
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, curses.Bold}},
|
||||
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, curses.Bold}}}}}
|
||||
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, curses.Dark256, 99, 0, true)
|
||||
assert := func(idx int, b int32, e int32, c int, bold bool) {
|
||||
var attr curses.Attr
|
||||
pair := tui.NewColorPair(99, 199)
|
||||
colors := item.colorOffsets(offsets, tui.Dark256, pair, tui.AttrRegular, true)
|
||||
assert := func(idx int, b int32, e int32, c tui.ColorPair, bold bool) {
|
||||
var attr tui.Attr
|
||||
if bold {
|
||||
attr = curses.Bold
|
||||
attr = tui.Bold
|
||||
}
|
||||
o := colors[idx]
|
||||
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.attr != attr {
|
||||
t.Error(o)
|
||||
}
|
||||
}
|
||||
assert(0, 0, 5, curses.ColUser, false)
|
||||
assert(1, 5, 15, 99, false)
|
||||
assert(2, 15, 20, curses.ColUser, false)
|
||||
assert(3, 22, 25, curses.ColUser+1, true)
|
||||
assert(4, 25, 35, 99, false)
|
||||
assert(5, 35, 40, curses.ColUser+2, true)
|
||||
assert(0, 0, 5, tui.NewColorPair(1, 5), false)
|
||||
assert(1, 5, 15, pair, false)
|
||||
assert(2, 15, 20, tui.NewColorPair(1, 5), false)
|
||||
assert(3, 22, 25, tui.NewColorPair(2, 6), true)
|
||||
assert(4, 25, 35, pair, false)
|
||||
assert(5, 35, 40, tui.NewColorPair(4, 8), true)
|
||||
}
|
||||
|
725
src/terminal.go
725
src/terminal.go
File diff suppressed because it is too large
Load Diff
@@ -14,8 +14,10 @@ func newItem(str string) *Item {
|
||||
}
|
||||
|
||||
func TestReplacePlaceholder(t *testing.T) {
|
||||
items1 := []*Item{newItem(" foo'bar \x1b[31mbaz\x1b[m")}
|
||||
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
|
||||
items1 := []*Item{item1, item1}
|
||||
items2 := []*Item{
|
||||
newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
||||
newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
||||
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
|
||||
|
||||
@@ -27,47 +29,65 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
}
|
||||
|
||||
// {}, preserve ansi
|
||||
result = replacePlaceholder("echo {}", false, Delimiter{}, "query", items1)
|
||||
result = replacePlaceholder("echo {}", false, Delimiter{}, false, "query", items1)
|
||||
check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'")
|
||||
|
||||
// {}, strip ansi
|
||||
result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items1)
|
||||
result = replacePlaceholder("echo {}", true, Delimiter{}, false, "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'")
|
||||
result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items2)
|
||||
check("echo 'foo'\\''bar baz'")
|
||||
|
||||
// {..}, strip leading whitespaces, preserve ansi
|
||||
result = replacePlaceholder("echo {..}", false, Delimiter{}, "query", items1)
|
||||
result = replacePlaceholder("echo {..}", false, Delimiter{}, false, "query", items1)
|
||||
check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'")
|
||||
|
||||
// {..}, strip leading whitespaces, strip ansi
|
||||
result = replacePlaceholder("echo {..}", true, Delimiter{}, "query", items1)
|
||||
result = replacePlaceholder("echo {..}", true, Delimiter{}, false, "query", items1)
|
||||
check("echo 'foo'\\''bar baz'")
|
||||
|
||||
// {q}
|
||||
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, "query", items1)
|
||||
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz' 'query'")
|
||||
|
||||
// {q}, multiple items
|
||||
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, "query 'string'", items2)
|
||||
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, false, "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)
|
||||
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, false, "query 'string'", items2)
|
||||
check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'")
|
||||
|
||||
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items1)
|
||||
check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
|
||||
|
||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, "query", items2)
|
||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items2)
|
||||
check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
|
||||
|
||||
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, false, "query", items2)
|
||||
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
|
||||
|
||||
// forcePlus
|
||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, true, "query", items2)
|
||||
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
|
||||
|
||||
// No match
|
||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, nil})
|
||||
check("echo /")
|
||||
|
||||
// No match, but with selections
|
||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, item1})
|
||||
check("echo /' foo'\\''bar baz'")
|
||||
|
||||
// String delimiter
|
||||
delim := "'"
|
||||
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, "query", items1)
|
||||
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz'/'foo'/'bar baz'")
|
||||
|
||||
// Regex delimiter
|
||||
regex := regexp.MustCompile("[oa]+")
|
||||
// foo'bar baz
|
||||
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, "query", items1)
|
||||
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
|
||||
}
|
||||
|
13
src/terminal_unix.go
Normal file
13
src/terminal_unix.go
Normal 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
11
src/terminal_windows.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// +build windows
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||
// TODO
|
||||
}
|
45
src/tui/dummy.go
Normal file
45
src/tui/dummy.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// +build !ncurses
|
||||
// +build !tcell
|
||||
// +build !windows
|
||||
|
||||
package tui
|
||||
|
||||
type Attr int
|
||||
|
||||
func HasFullscreenRenderer() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a Attr) Merge(b Attr) Attr {
|
||||
return a | b
|
||||
}
|
||||
|
||||
const (
|
||||
AttrRegular Attr = Attr(0)
|
||||
Bold = Attr(1)
|
||||
Dim = Attr(1 << 1)
|
||||
Italic = Attr(1 << 2)
|
||||
Underline = Attr(1 << 3)
|
||||
Blink = Attr(1 << 4)
|
||||
Blink2 = Attr(1 << 5)
|
||||
Reverse = Attr(1 << 6)
|
||||
)
|
||||
|
||||
func (r *FullscreenRenderer) Init() {}
|
||||
func (r *FullscreenRenderer) Pause() {}
|
||||
func (r *FullscreenRenderer) Clear() {}
|
||||
func (r *FullscreenRenderer) Refresh() {}
|
||||
func (r *FullscreenRenderer) Close() {}
|
||||
|
||||
func (r *FullscreenRenderer) Resume() bool { return false }
|
||||
func (r *FullscreenRenderer) DoesAutoWrap() bool { return false }
|
||||
func (r *FullscreenRenderer) IsOptimized() bool { return false }
|
||||
func (r *FullscreenRenderer) GetChar() Event { return Event{} }
|
||||
func (r *FullscreenRenderer) MaxX() int { return 0 }
|
||||
func (r *FullscreenRenderer) MaxY() int { return 0 }
|
||||
|
||||
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
|
||||
|
||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
|
||||
return nil
|
||||
}
|
874
src/tui/light.go
Normal file
874
src/tui/light.go
Normal file
@@ -0,0 +1,874 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultWidth = 80
|
||||
defaultHeight = 24
|
||||
|
||||
defaultEscDelay = 100
|
||||
escPollInterval = 5
|
||||
offsetPollTries = 10
|
||||
)
|
||||
|
||||
const consoleDevice string = "/dev/tty"
|
||||
|
||||
var offsetRegexp *regexp.Regexp = regexp.MustCompile("\x1b\\[([0-9]+);([0-9]+)R")
|
||||
|
||||
func openTtyIn() *os.File {
|
||||
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
panic("Failed to open " + consoleDevice)
|
||||
}
|
||||
return in
|
||||
}
|
||||
|
||||
func (r *LightRenderer) stderr(str string) {
|
||||
r.stderrInternal(str, true)
|
||||
}
|
||||
|
||||
// FIXME: Need better handling of non-displayable characters
|
||||
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
|
||||
bytes := []byte(str)
|
||||
runes := []rune{}
|
||||
for len(bytes) > 0 {
|
||||
r, sz := utf8.DecodeRune(bytes)
|
||||
if r == utf8.RuneError || r < 32 &&
|
||||
r != '\x1b' && (!allowNLCR || r != '\n' && r != '\r') {
|
||||
runes = append(runes, '?')
|
||||
} else {
|
||||
runes = append(runes, r)
|
||||
}
|
||||
bytes = bytes[sz:]
|
||||
}
|
||||
r.queued += string(runes)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) csi(code string) {
|
||||
r.stderr("\x1b[" + code)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) flush() {
|
||||
if len(r.queued) > 0 {
|
||||
fmt.Fprint(os.Stderr, r.queued)
|
||||
r.queued = ""
|
||||
}
|
||||
}
|
||||
|
||||
// Light renderer
|
||||
type LightRenderer struct {
|
||||
theme *ColorTheme
|
||||
mouse bool
|
||||
forceBlack bool
|
||||
prevDownTime time.Time
|
||||
clickY []int
|
||||
ttyin *os.File
|
||||
buffer []byte
|
||||
origState *terminal.State
|
||||
width int
|
||||
height int
|
||||
yoffset int
|
||||
tabstop int
|
||||
escDelay int
|
||||
fullscreen bool
|
||||
upOneLine bool
|
||||
queued string
|
||||
y int
|
||||
x int
|
||||
maxHeightFunc func(int) int
|
||||
}
|
||||
|
||||
type LightWindow struct {
|
||||
renderer *LightRenderer
|
||||
colored bool
|
||||
border BorderStyle
|
||||
top int
|
||||
left int
|
||||
width int
|
||||
height int
|
||||
posx int
|
||||
posy int
|
||||
tabstop int
|
||||
bg Color
|
||||
}
|
||||
|
||||
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, maxHeightFunc func(int) int) Renderer {
|
||||
r := LightRenderer{
|
||||
theme: theme,
|
||||
forceBlack: forceBlack,
|
||||
mouse: mouse,
|
||||
ttyin: openTtyIn(),
|
||||
yoffset: 0,
|
||||
tabstop: tabstop,
|
||||
fullscreen: false,
|
||||
upOneLine: false,
|
||||
maxHeightFunc: maxHeightFunc}
|
||||
return &r
|
||||
}
|
||||
|
||||
func (r *LightRenderer) fd() int {
|
||||
return int(r.ttyin.Fd())
|
||||
}
|
||||
|
||||
func (r *LightRenderer) defaultTheme() *ColorTheme {
|
||||
if strings.Contains(os.Getenv("TERM"), "256") {
|
||||
return Dark256
|
||||
}
|
||||
colors, err := exec.Command("tput", "colors").Output()
|
||||
if err == nil && atoi(strings.TrimSpace(string(colors)), 16) > 16 {
|
||||
return Dark256
|
||||
}
|
||||
return Default16
|
||||
}
|
||||
|
||||
func (r *LightRenderer) findOffset() (row int, col int) {
|
||||
r.csi("6n")
|
||||
r.flush()
|
||||
bytes := []byte{}
|
||||
for tries := 0; tries < offsetPollTries; tries++ {
|
||||
bytes = r.getBytesInternal(bytes, tries > 0)
|
||||
offsets := offsetRegexp.FindSubmatch(bytes)
|
||||
if len(offsets) > 2 {
|
||||
return atoi(string(offsets[1]), 0) - 1, atoi(string(offsets[2]), 0) - 1
|
||||
}
|
||||
}
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
func repeat(s string, times int) string {
|
||||
if times > 0 {
|
||||
return strings.Repeat(s, times)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func atoi(s string, defaultValue int) int {
|
||||
value, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Init() {
|
||||
r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
|
||||
|
||||
fd := r.fd()
|
||||
origState, err := terminal.GetState(fd)
|
||||
if err != nil {
|
||||
errorExit(err.Error())
|
||||
}
|
||||
r.origState = origState
|
||||
terminal.MakeRaw(fd)
|
||||
terminalHeight, capHeight := r.updateTerminalSize()
|
||||
if capHeight == terminalHeight {
|
||||
r.fullscreen = true
|
||||
r.height = terminalHeight
|
||||
}
|
||||
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
|
||||
|
||||
if r.fullscreen {
|
||||
r.smcup()
|
||||
} else {
|
||||
r.csi("J")
|
||||
y, x := r.findOffset()
|
||||
r.mouse = r.mouse && y >= 0
|
||||
if x > 0 {
|
||||
r.upOneLine = true
|
||||
r.makeSpace()
|
||||
}
|
||||
for i := 1; i < r.MaxY(); i++ {
|
||||
r.makeSpace()
|
||||
}
|
||||
}
|
||||
|
||||
if r.mouse {
|
||||
r.csi("?1000h")
|
||||
}
|
||||
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
|
||||
r.csi("G")
|
||||
r.csi("K")
|
||||
// r.csi("s")
|
||||
if !r.fullscreen && r.mouse {
|
||||
r.yoffset, _ = r.findOffset()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) makeSpace() {
|
||||
r.stderr("\n")
|
||||
r.csi("G")
|
||||
}
|
||||
|
||||
func (r *LightRenderer) move(y int, x int) {
|
||||
// w.csi("u")
|
||||
if r.y < y {
|
||||
r.csi(fmt.Sprintf("%dB", y-r.y))
|
||||
} else if r.y > y {
|
||||
r.csi(fmt.Sprintf("%dA", r.y-y))
|
||||
}
|
||||
r.stderr("\r")
|
||||
if x > 0 {
|
||||
r.csi(fmt.Sprintf("%dC", x))
|
||||
}
|
||||
r.y = y
|
||||
r.x = x
|
||||
}
|
||||
|
||||
func (r *LightRenderer) origin() {
|
||||
r.move(0, 0)
|
||||
}
|
||||
|
||||
func getEnv(name string, defaultValue int) int {
|
||||
env := os.Getenv(name)
|
||||
if len(env) == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
return atoi(env, defaultValue)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) updateTerminalSize() (int, int) {
|
||||
width, height, err := terminal.GetSize(r.fd())
|
||||
if err == nil {
|
||||
r.width = width
|
||||
if r.fullscreen {
|
||||
r.height = height
|
||||
} else {
|
||||
r.height = r.maxHeightFunc(height)
|
||||
}
|
||||
} else {
|
||||
r.width = getEnv("COLUMNS", defaultWidth)
|
||||
r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight))
|
||||
}
|
||||
return height, r.height
|
||||
}
|
||||
|
||||
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
||||
b := make([]byte, 1)
|
||||
util.SetNonblock(r.ttyin, nonblock)
|
||||
_, err := r.ttyin.Read(b)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return int(b[0]), true
|
||||
}
|
||||
|
||||
func (r *LightRenderer) getBytes() []byte {
|
||||
return r.getBytesInternal(r.buffer, false)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
|
||||
c, ok := r.getch(nonblock)
|
||||
if !nonblock && !ok {
|
||||
r.Close()
|
||||
errorExit("Failed to read " + consoleDevice)
|
||||
}
|
||||
|
||||
retries := 0
|
||||
if c == ESC || nonblock {
|
||||
retries = r.escDelay / escPollInterval
|
||||
}
|
||||
buffer = append(buffer, byte(c))
|
||||
|
||||
for {
|
||||
c, ok = r.getch(true)
|
||||
if !ok {
|
||||
if retries > 0 {
|
||||
retries--
|
||||
time.Sleep(escPollInterval * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
retries = 0
|
||||
buffer = append(buffer, byte(c))
|
||||
}
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
func (r *LightRenderer) GetChar() Event {
|
||||
if len(r.buffer) == 0 {
|
||||
r.buffer = r.getBytes()
|
||||
}
|
||||
if len(r.buffer) == 0 {
|
||||
panic("Empty buffer")
|
||||
}
|
||||
|
||||
sz := 1
|
||||
defer func() {
|
||||
r.buffer = r.buffer[sz:]
|
||||
}()
|
||||
|
||||
switch r.buffer[0] {
|
||||
case CtrlC:
|
||||
return Event{CtrlC, 0, nil}
|
||||
case CtrlG:
|
||||
return Event{CtrlG, 0, nil}
|
||||
case CtrlQ:
|
||||
return Event{CtrlQ, 0, nil}
|
||||
case 127:
|
||||
return Event{BSpace, 0, nil}
|
||||
case 0:
|
||||
return Event{CtrlSpace, 0, nil}
|
||||
case ESC:
|
||||
ev := r.escSequence(&sz)
|
||||
// Second chance
|
||||
if ev.Type == Invalid {
|
||||
r.buffer = r.getBytes()
|
||||
ev = r.escSequence(&sz)
|
||||
}
|
||||
return ev
|
||||
}
|
||||
|
||||
// CTRL-A ~ CTRL-Z
|
||||
if r.buffer[0] <= CtrlZ {
|
||||
return Event{int(r.buffer[0]), 0, nil}
|
||||
}
|
||||
char, rsz := utf8.DecodeRune(r.buffer)
|
||||
if char == utf8.RuneError {
|
||||
return Event{ESC, 0, nil}
|
||||
}
|
||||
sz = rsz
|
||||
return Event{Rune, char, nil}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
if len(r.buffer) < 2 {
|
||||
return Event{ESC, 0, nil}
|
||||
}
|
||||
*sz = 2
|
||||
switch r.buffer[1] {
|
||||
case 13:
|
||||
return Event{AltEnter, 0, nil}
|
||||
case 32:
|
||||
return Event{AltSpace, 0, nil}
|
||||
case 47:
|
||||
return Event{AltSlash, 0, nil}
|
||||
case 98:
|
||||
return Event{AltB, 0, nil}
|
||||
case 100:
|
||||
return Event{AltD, 0, nil}
|
||||
case 102:
|
||||
return Event{AltF, 0, nil}
|
||||
case 127:
|
||||
return Event{AltBS, 0, nil}
|
||||
case 91, 79:
|
||||
if len(r.buffer) < 3 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 3
|
||||
switch r.buffer[2] {
|
||||
case 68:
|
||||
return Event{Left, 0, nil}
|
||||
case 67:
|
||||
return Event{Right, 0, nil}
|
||||
case 66:
|
||||
return Event{Down, 0, nil}
|
||||
case 65:
|
||||
return Event{Up, 0, nil}
|
||||
case 90:
|
||||
return Event{BTab, 0, nil}
|
||||
case 72:
|
||||
return Event{Home, 0, nil}
|
||||
case 70:
|
||||
return Event{End, 0, nil}
|
||||
case 77:
|
||||
return r.mouseSequence(sz)
|
||||
case 80:
|
||||
return Event{F1, 0, nil}
|
||||
case 81:
|
||||
return Event{F2, 0, nil}
|
||||
case 82:
|
||||
return Event{F3, 0, nil}
|
||||
case 83:
|
||||
return Event{F4, 0, nil}
|
||||
case 49, 50, 51, 52, 53, 54:
|
||||
if len(r.buffer) < 4 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 4
|
||||
switch r.buffer[2] {
|
||||
case 50:
|
||||
if len(r.buffer) == 5 && r.buffer[4] == 126 {
|
||||
*sz = 5
|
||||
switch r.buffer[3] {
|
||||
case 48:
|
||||
return Event{F9, 0, nil}
|
||||
case 49:
|
||||
return Event{F10, 0, nil}
|
||||
case 51:
|
||||
return Event{F11, 0, nil}
|
||||
case 52:
|
||||
return Event{F12, 0, nil}
|
||||
}
|
||||
}
|
||||
// Bracketed paste mode \e[200~ / \e[201
|
||||
if r.buffer[3] == 48 && (r.buffer[4] == 48 || r.buffer[4] == 49) && r.buffer[5] == 126 {
|
||||
*sz = 6
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
return Event{Invalid, 0, nil} // INS
|
||||
case 51:
|
||||
return Event{Del, 0, nil}
|
||||
case 52:
|
||||
return Event{End, 0, nil}
|
||||
case 53:
|
||||
return Event{PgUp, 0, nil}
|
||||
case 54:
|
||||
return Event{PgDn, 0, nil}
|
||||
case 49:
|
||||
switch r.buffer[3] {
|
||||
case 126:
|
||||
return Event{Home, 0, nil}
|
||||
case 53, 55, 56, 57:
|
||||
if len(r.buffer) == 5 && r.buffer[4] == 126 {
|
||||
*sz = 5
|
||||
switch r.buffer[3] {
|
||||
case 53:
|
||||
return Event{F5, 0, nil}
|
||||
case 55:
|
||||
return Event{F6, 0, nil}
|
||||
case 56:
|
||||
return Event{F7, 0, nil}
|
||||
case 57:
|
||||
return Event{F8, 0, nil}
|
||||
}
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
case 59:
|
||||
if len(r.buffer) != 6 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 6
|
||||
switch r.buffer[4] {
|
||||
case 50:
|
||||
switch r.buffer[5] {
|
||||
case 68:
|
||||
return Event{Home, 0, nil}
|
||||
case 67:
|
||||
return Event{End, 0, nil}
|
||||
}
|
||||
case 53:
|
||||
switch r.buffer[5] {
|
||||
case 68:
|
||||
return Event{SLeft, 0, nil}
|
||||
case 67:
|
||||
return Event{SRight, 0, nil}
|
||||
}
|
||||
} // r.buffer[4]
|
||||
} // r.buffer[3]
|
||||
} // r.buffer[2]
|
||||
} // r.buffer[2]
|
||||
} // r.buffer[1]
|
||||
if r.buffer[1] >= 'a' && r.buffer[1] <= 'z' {
|
||||
return Event{AltA + int(r.buffer[1]) - 'a', 0, nil}
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) mouseSequence(sz *int) Event {
|
||||
if len(r.buffer) < 6 || !r.mouse {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 6
|
||||
switch r.buffer[3] {
|
||||
case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl
|
||||
35, 39, 43, 51: // mouse-up / shift / cmd / ctrl
|
||||
mod := r.buffer[3] >= 36
|
||||
down := r.buffer[3]%2 == 0
|
||||
x := int(r.buffer[4] - 33)
|
||||
y := int(r.buffer[5]-33) - r.yoffset
|
||||
double := false
|
||||
if down {
|
||||
now := time.Now()
|
||||
if now.Sub(r.prevDownTime) < doubleClickDuration {
|
||||
r.clickY = append(r.clickY, y)
|
||||
} else {
|
||||
r.clickY = []int{y}
|
||||
}
|
||||
r.prevDownTime = now
|
||||
} else {
|
||||
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
|
||||
time.Now().Sub(r.prevDownTime) < doubleClickDuration {
|
||||
double = true
|
||||
}
|
||||
}
|
||||
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}}
|
||||
case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl
|
||||
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
|
||||
mod := r.buffer[3] >= 100
|
||||
s := 1 - int(r.buffer[3]%2)*2
|
||||
x := int(r.buffer[4] - 33)
|
||||
y := int(r.buffer[5]-33) - r.yoffset
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}}
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) smcup() {
|
||||
r.csi("?1049h")
|
||||
}
|
||||
|
||||
func (r *LightRenderer) rmcup() {
|
||||
r.csi("?1049l")
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Pause() {
|
||||
terminal.Restore(r.fd(), r.origState)
|
||||
if r.fullscreen {
|
||||
r.rmcup()
|
||||
} else {
|
||||
r.smcup()
|
||||
r.csi("H")
|
||||
}
|
||||
r.flush()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Resume() bool {
|
||||
terminal.MakeRaw(r.fd())
|
||||
if r.fullscreen {
|
||||
r.smcup()
|
||||
} else {
|
||||
r.rmcup()
|
||||
}
|
||||
r.flush()
|
||||
// Should redraw
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Clear() {
|
||||
// r.csi("u")
|
||||
r.origin()
|
||||
r.csi("J")
|
||||
r.flush()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) RefreshWindows(windows []Window) {
|
||||
r.flush()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Refresh() {
|
||||
r.updateTerminalSize()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Close() {
|
||||
// r.csi("u")
|
||||
if r.fullscreen {
|
||||
r.rmcup()
|
||||
} else {
|
||||
r.origin()
|
||||
if r.upOneLine {
|
||||
r.csi("A")
|
||||
}
|
||||
r.csi("J")
|
||||
}
|
||||
if r.mouse {
|
||||
r.csi("?1000l")
|
||||
}
|
||||
r.flush()
|
||||
terminal.Restore(r.fd(), r.origState)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) MaxX() int {
|
||||
return r.width
|
||||
}
|
||||
|
||||
func (r *LightRenderer) MaxY() int {
|
||||
return r.height
|
||||
}
|
||||
|
||||
func (r *LightRenderer) DoesAutoWrap() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *LightRenderer) IsOptimized() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
|
||||
w := &LightWindow{
|
||||
renderer: r,
|
||||
colored: r.theme != nil,
|
||||
border: borderStyle,
|
||||
top: top,
|
||||
left: left,
|
||||
width: width,
|
||||
height: height,
|
||||
tabstop: r.tabstop,
|
||||
bg: colDefault}
|
||||
if r.theme != nil {
|
||||
w.bg = r.theme.Bg
|
||||
}
|
||||
w.drawBorder()
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorder() {
|
||||
switch w.border {
|
||||
case BorderAround:
|
||||
w.drawBorderAround()
|
||||
case BorderHorizontal:
|
||||
w.drawBorderHorizontal()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorderHorizontal() {
|
||||
w.Move(0, 0)
|
||||
w.CPrint(ColBorder, AttrRegular, repeat("─", w.width))
|
||||
w.Move(w.height-1, 0)
|
||||
w.CPrint(ColBorder, AttrRegular, repeat("─", w.width))
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorderAround() {
|
||||
w.Move(0, 0)
|
||||
w.CPrint(ColBorder, AttrRegular, "┌"+repeat("─", w.width-2)+"┐")
|
||||
for y := 1; y < w.height-1; y++ {
|
||||
w.Move(y, 0)
|
||||
w.CPrint(ColBorder, AttrRegular, "│")
|
||||
w.cprint2(colDefault, w.bg, AttrRegular, repeat(" ", w.width-2))
|
||||
w.CPrint(ColBorder, AttrRegular, "│")
|
||||
}
|
||||
w.Move(w.height-1, 0)
|
||||
w.CPrint(ColBorder, AttrRegular, "└"+repeat("─", w.width-2)+"┘")
|
||||
}
|
||||
|
||||
func (w *LightWindow) csi(code string) {
|
||||
w.renderer.csi(code)
|
||||
}
|
||||
|
||||
func (w *LightWindow) stderr(str string) {
|
||||
w.renderer.stderr(str)
|
||||
}
|
||||
|
||||
func (w *LightWindow) stderrInternal(str string, allowNLCR bool) {
|
||||
w.renderer.stderrInternal(str, allowNLCR)
|
||||
}
|
||||
|
||||
func (w *LightWindow) Top() int {
|
||||
return w.top
|
||||
}
|
||||
|
||||
func (w *LightWindow) Left() int {
|
||||
return w.left
|
||||
}
|
||||
|
||||
func (w *LightWindow) Width() int {
|
||||
return w.width
|
||||
}
|
||||
|
||||
func (w *LightWindow) Height() int {
|
||||
return w.height
|
||||
}
|
||||
|
||||
func (w *LightWindow) Refresh() {
|
||||
}
|
||||
|
||||
func (w *LightWindow) Close() {
|
||||
}
|
||||
|
||||
func (w *LightWindow) X() int {
|
||||
return w.posx
|
||||
}
|
||||
|
||||
func (w *LightWindow) Enclose(y int, x int) bool {
|
||||
return x >= w.left && x < (w.left+w.width) &&
|
||||
y >= w.top && y < (w.top+w.height)
|
||||
}
|
||||
|
||||
func (w *LightWindow) Move(y int, x int) {
|
||||
w.posx = x
|
||||
w.posy = y
|
||||
|
||||
w.renderer.move(w.Top()+y, w.Left()+x)
|
||||
}
|
||||
|
||||
func (w *LightWindow) MoveAndClear(y int, x int) {
|
||||
w.Move(y, x)
|
||||
// We should not delete preview window on the right
|
||||
// csi("K")
|
||||
w.Print(repeat(" ", w.width-x))
|
||||
w.Move(y, x)
|
||||
}
|
||||
|
||||
func attrCodes(attr Attr) []string {
|
||||
codes := []string{}
|
||||
if (attr & Bold) > 0 {
|
||||
codes = append(codes, "1")
|
||||
}
|
||||
if (attr & Dim) > 0 {
|
||||
codes = append(codes, "2")
|
||||
}
|
||||
if (attr & Italic) > 0 {
|
||||
codes = append(codes, "3")
|
||||
}
|
||||
if (attr & Underline) > 0 {
|
||||
codes = append(codes, "4")
|
||||
}
|
||||
if (attr & Blink) > 0 {
|
||||
codes = append(codes, "5")
|
||||
}
|
||||
if (attr & Reverse) > 0 {
|
||||
codes = append(codes, "7")
|
||||
}
|
||||
return codes
|
||||
}
|
||||
|
||||
func colorCodes(fg Color, bg Color) []string {
|
||||
codes := []string{}
|
||||
appendCode := func(c Color, offset int) {
|
||||
if c == colDefault {
|
||||
return
|
||||
}
|
||||
if c.is24() {
|
||||
r := (c >> 16) & 0xff
|
||||
g := (c >> 8) & 0xff
|
||||
b := (c) & 0xff
|
||||
codes = append(codes, fmt.Sprintf("%d;2;%d;%d;%d", 38+offset, r, g, b))
|
||||
} else if c >= colBlack && c <= colWhite {
|
||||
codes = append(codes, fmt.Sprintf("%d", int(c)+30+offset))
|
||||
} else if c > colWhite && c < 16 {
|
||||
codes = append(codes, fmt.Sprintf("%d", int(c)+90+offset-8))
|
||||
} else if c >= 16 && c < 256 {
|
||||
codes = append(codes, fmt.Sprintf("%d;5;%d", 38+offset, c))
|
||||
}
|
||||
}
|
||||
appendCode(fg, 0)
|
||||
appendCode(bg, 10)
|
||||
return codes
|
||||
}
|
||||
|
||||
func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) bool {
|
||||
codes := append(attrCodes(attr), colorCodes(fg, bg)...)
|
||||
w.csi(";" + strings.Join(codes, ";") + "m")
|
||||
return len(codes) > 0
|
||||
}
|
||||
|
||||
func (w *LightWindow) Print(text string) {
|
||||
w.cprint2(colDefault, w.bg, AttrRegular, text)
|
||||
}
|
||||
|
||||
func cleanse(str string) string {
|
||||
return strings.Replace(str, "\x1b", "?", -1)
|
||||
}
|
||||
|
||||
func (w *LightWindow) CPrint(pair ColorPair, attr Attr, text string) {
|
||||
if !w.colored {
|
||||
w.csiColor(colDefault, colDefault, attrFor(pair, attr))
|
||||
} else {
|
||||
w.csiColor(pair.Fg(), pair.Bg(), attr)
|
||||
}
|
||||
w.stderrInternal(cleanse(text), false)
|
||||
w.csi("m")
|
||||
}
|
||||
|
||||
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
|
||||
if w.csiColor(fg, bg, attr) {
|
||||
defer w.csi("m")
|
||||
}
|
||||
w.stderrInternal(cleanse(text), false)
|
||||
}
|
||||
|
||||
type wrappedLine struct {
|
||||
text string
|
||||
displayWidth int
|
||||
}
|
||||
|
||||
func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLine {
|
||||
lines := []wrappedLine{}
|
||||
width := 0
|
||||
line := ""
|
||||
for _, r := range input {
|
||||
w := util.Max(util.RuneWidth(r, prefixLength+width, 8), 1)
|
||||
width += w
|
||||
str := string(r)
|
||||
if r == '\t' {
|
||||
str = repeat(" ", w)
|
||||
}
|
||||
if prefixLength+width <= max {
|
||||
line += str
|
||||
} else {
|
||||
lines = append(lines, wrappedLine{string(line), width - w})
|
||||
line = str
|
||||
prefixLength = 0
|
||||
width = util.RuneWidth(r, prefixLength, 8)
|
||||
}
|
||||
}
|
||||
lines = append(lines, wrappedLine{string(line), width})
|
||||
return lines
|
||||
}
|
||||
|
||||
func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
||||
allLines := strings.Split(str, "\n")
|
||||
for i, line := range allLines {
|
||||
lines := wrapLine(line, w.posx, w.width, w.tabstop)
|
||||
for j, wl := range lines {
|
||||
if w.posx >= w.Width()-1 && wl.displayWidth == 0 {
|
||||
if w.posy < w.height-1 {
|
||||
w.MoveAndClear(w.posy+1, 0)
|
||||
}
|
||||
return FillNextLine
|
||||
}
|
||||
w.stderrInternal(wl.text, false)
|
||||
w.posx += wl.displayWidth
|
||||
if j < len(lines)-1 || i < len(allLines)-1 {
|
||||
if w.posy+1 >= w.height {
|
||||
return FillSuspend
|
||||
}
|
||||
w.MoveAndClear(w.posy+1, 0)
|
||||
onMove()
|
||||
}
|
||||
}
|
||||
}
|
||||
return FillContinue
|
||||
}
|
||||
|
||||
func (w *LightWindow) setBg() {
|
||||
if w.bg != colDefault {
|
||||
w.csiColor(colDefault, w.bg, AttrRegular)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) Fill(text string) FillReturn {
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
w.setBg()
|
||||
return w.fill(text, w.setBg)
|
||||
}
|
||||
|
||||
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
if bg == colDefault {
|
||||
bg = w.bg
|
||||
}
|
||||
if w.csiColor(fg, bg, attr) {
|
||||
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
|
||||
defer w.csi("m")
|
||||
}
|
||||
return w.fill(text, w.setBg)
|
||||
}
|
||||
|
||||
func (w *LightWindow) FinishFill() {
|
||||
for y := w.posy + 1; y < w.height; y++ {
|
||||
w.MoveAndClear(y, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) Erase() {
|
||||
w.drawBorder()
|
||||
// We don't erase the window here to avoid flickering during scroll
|
||||
w.Move(0, 0)
|
||||
}
|
506
src/tui/ncurses.go
Normal file
506
src/tui/ncurses.go
Normal file
@@ -0,0 +1,506 @@
|
||||
// +build ncurses
|
||||
// +build !windows
|
||||
// +build !tcell
|
||||
|
||||
package tui
|
||||
|
||||
/*
|
||||
#include <ncurses.h>
|
||||
#include <locale.h>
|
||||
#cgo !static LDFLAGS: -lncurses
|
||||
#cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl
|
||||
#cgo android static LDFLAGS: -l:libncurses.a -fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch
|
||||
|
||||
FILE* c_tty() {
|
||||
return fopen("/dev/tty", "r");
|
||||
}
|
||||
|
||||
SCREEN* c_newterm(FILE* tty) {
|
||||
return newterm(NULL, stderr, tty);
|
||||
}
|
||||
|
||||
int c_getcurx(WINDOW* win) {
|
||||
return getcurx(win);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func HasFullscreenRenderer() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type Attr C.uint
|
||||
|
||||
type CursesWindow struct {
|
||||
impl *C.WINDOW
|
||||
top int
|
||||
left int
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
func (w *CursesWindow) Top() int {
|
||||
return w.top
|
||||
}
|
||||
|
||||
func (w *CursesWindow) Left() int {
|
||||
return w.left
|
||||
}
|
||||
|
||||
func (w *CursesWindow) Width() int {
|
||||
return w.width
|
||||
}
|
||||
|
||||
func (w *CursesWindow) Height() int {
|
||||
return w.height
|
||||
}
|
||||
|
||||
func (w *CursesWindow) Refresh() {
|
||||
C.wnoutrefresh(w.impl)
|
||||
}
|
||||
|
||||
func (w *CursesWindow) FinishFill() {
|
||||
// NO-OP
|
||||
}
|
||||
|
||||
const (
|
||||
Bold Attr = C.A_BOLD
|
||||
Dim = C.A_DIM
|
||||
Blink = C.A_BLINK
|
||||
Reverse = C.A_REVERSE
|
||||
Underline = C.A_UNDERLINE
|
||||
)
|
||||
|
||||
var Italic Attr = C.A_VERTICAL << 1 // FIXME
|
||||
|
||||
const (
|
||||
AttrRegular Attr = 0
|
||||
)
|
||||
|
||||
var (
|
||||
_screen *C.SCREEN
|
||||
_colorMap map[int]int16
|
||||
_colorFn func(ColorPair, Attr) (C.short, C.int)
|
||||
)
|
||||
|
||||
func init() {
|
||||
_colorMap = make(map[int]int16)
|
||||
if strings.HasPrefix(C.GoString(C.curses_version()), "ncurses 5") {
|
||||
Italic = C.A_NORMAL
|
||||
}
|
||||
}
|
||||
|
||||
func (a Attr) Merge(b Attr) Attr {
|
||||
return a | b
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
|
||||
if C.tigetnum(C.CString("colors")) >= 256 {
|
||||
return Dark256
|
||||
}
|
||||
return Default16
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Init() {
|
||||
C.setlocale(C.LC_ALL, C.CString(""))
|
||||
tty := C.c_tty()
|
||||
if tty == nil {
|
||||
errorExit("Failed to open /dev/tty")
|
||||
}
|
||||
_screen = C.c_newterm(tty)
|
||||
if _screen == nil {
|
||||
errorExit("Invalid $TERM: " + os.Getenv("TERM"))
|
||||
}
|
||||
C.set_term(_screen)
|
||||
if r.mouse {
|
||||
C.mousemask(C.ALL_MOUSE_EVENTS, nil)
|
||||
C.mouseinterval(0)
|
||||
}
|
||||
C.noecho()
|
||||
C.raw() // stty dsusp undef
|
||||
C.nonl()
|
||||
C.keypad(C.stdscr, true)
|
||||
|
||||
delay := 50
|
||||
delayEnv := os.Getenv("ESCDELAY")
|
||||
if len(delayEnv) > 0 {
|
||||
num, err := strconv.Atoi(delayEnv)
|
||||
if err == nil && num >= 0 {
|
||||
delay = num
|
||||
}
|
||||
}
|
||||
C.set_escdelay(C.int(delay))
|
||||
|
||||
if r.theme != nil {
|
||||
C.start_color()
|
||||
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
|
||||
initPairs(r.theme)
|
||||
C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal.index()))))
|
||||
_colorFn = attrColored
|
||||
} else {
|
||||
initTheme(r.theme, nil, r.forceBlack)
|
||||
_colorFn = attrMono
|
||||
}
|
||||
|
||||
C.nodelay(C.stdscr, true)
|
||||
ch := C.getch()
|
||||
if ch != C.ERR {
|
||||
C.ungetch(ch)
|
||||
}
|
||||
C.nodelay(C.stdscr, false)
|
||||
}
|
||||
|
||||
func initPairs(theme *ColorTheme) {
|
||||
C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg))
|
||||
for _, pair := range []ColorPair{
|
||||
ColNormal,
|
||||
ColPrompt,
|
||||
ColMatch,
|
||||
ColCurrent,
|
||||
ColCurrentMatch,
|
||||
ColSpinner,
|
||||
ColInfo,
|
||||
ColCursor,
|
||||
ColSelected,
|
||||
ColHeader,
|
||||
ColBorder} {
|
||||
C.init_pair(C.short(pair.index()), C.short(pair.Fg()), C.short(pair.Bg()))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Pause() {
|
||||
C.endwin()
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Resume() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Close() {
|
||||
C.endwin()
|
||||
C.delscreen(_screen)
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
|
||||
win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left))
|
||||
if r.theme != nil {
|
||||
C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal.index()))))
|
||||
}
|
||||
// FIXME Does not implement BorderHorizontal
|
||||
if borderStyle != BorderNone {
|
||||
pair, attr := _colorFn(ColBorder, 0)
|
||||
C.wcolor_set(win, pair, nil)
|
||||
C.wattron(win, attr)
|
||||
C.box(win, 0, 0)
|
||||
C.wattroff(win, attr)
|
||||
C.wcolor_set(win, 0, nil)
|
||||
}
|
||||
|
||||
return &CursesWindow{
|
||||
impl: win,
|
||||
top: top,
|
||||
left: left,
|
||||
width: width,
|
||||
height: height,
|
||||
}
|
||||
}
|
||||
|
||||
func attrColored(color ColorPair, a Attr) (C.short, C.int) {
|
||||
return C.short(color.index()), C.int(a)
|
||||
}
|
||||
|
||||
func attrMono(color ColorPair, a Attr) (C.short, C.int) {
|
||||
return 0, C.int(attrFor(color, a))
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) MaxX() int {
|
||||
return int(C.COLS)
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) MaxY() int {
|
||||
return int(C.LINES)
|
||||
}
|
||||
|
||||
func (w *CursesWindow) Close() {
|
||||
C.delwin(w.impl)
|
||||
}
|
||||
|
||||
func (w *CursesWindow) Enclose(y int, x int) bool {
|
||||
return bool(C.wenclose(w.impl, C.int(y), C.int(x)))
|
||||
}
|
||||
|
||||
func (w *CursesWindow) Move(y int, x int) {
|
||||
C.wmove(w.impl, C.int(y), C.int(x))
|
||||
}
|
||||
|
||||
func (w *CursesWindow) MoveAndClear(y int, x int) {
|
||||
w.Move(y, x)
|
||||
C.wclrtoeol(w.impl)
|
||||
}
|
||||
|
||||
func (w *CursesWindow) Print(text string) {
|
||||
C.waddstr(w.impl, C.CString(strings.Map(func(r rune) rune {
|
||||
if r < 32 {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, text)))
|
||||
}
|
||||
|
||||
func (w *CursesWindow) CPrint(color ColorPair, attr Attr, text string) {
|
||||
p, a := _colorFn(color, attr)
|
||||
C.wcolor_set(w.impl, p, nil)
|
||||
C.wattron(w.impl, a)
|
||||
w.Print(text)
|
||||
C.wattroff(w.impl, a)
|
||||
C.wcolor_set(w.impl, 0, nil)
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Clear() {
|
||||
C.clear()
|
||||
C.endwin()
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Refresh() {
|
||||
C.refresh()
|
||||
}
|
||||
|
||||
func (w *CursesWindow) Erase() {
|
||||
C.werase(w.impl)
|
||||
}
|
||||
|
||||
func (w *CursesWindow) X() int {
|
||||
return int(C.c_getcurx(w.impl))
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) DoesAutoWrap() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) IsOptimized() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *CursesWindow) Fill(str string) FillReturn {
|
||||
if C.waddstr(w.impl, C.CString(str)) == C.OK {
|
||||
return FillContinue
|
||||
}
|
||||
return FillSuspend
|
||||
}
|
||||
|
||||
func (w *CursesWindow) CFill(fg Color, bg Color, attr Attr, str string) FillReturn {
|
||||
index := ColorPair{fg, bg, -1}.index()
|
||||
C.wcolor_set(w.impl, C.short(index), nil)
|
||||
C.wattron(w.impl, C.int(attr))
|
||||
ret := w.Fill(str)
|
||||
C.wattroff(w.impl, C.int(attr))
|
||||
C.wcolor_set(w.impl, 0, nil)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
|
||||
for _, w := range windows {
|
||||
w.Refresh()
|
||||
}
|
||||
C.doupdate()
|
||||
}
|
||||
|
||||
func (p ColorPair) index() int16 {
|
||||
if p.id >= 0 {
|
||||
return p.id
|
||||
}
|
||||
|
||||
// ncurses does not support 24-bit colors
|
||||
if p.is24() {
|
||||
return ColDefault.index()
|
||||
}
|
||||
|
||||
key := p.key()
|
||||
if found, prs := _colorMap[key]; prs {
|
||||
return found
|
||||
}
|
||||
|
||||
id := int16(len(_colorMap)) + ColUser.id
|
||||
C.init_pair(C.short(id), C.short(p.Fg()), C.short(p.Bg()))
|
||||
_colorMap[key] = id
|
||||
return id
|
||||
}
|
||||
|
||||
func consume(expects ...rune) bool {
|
||||
for _, r := range expects {
|
||||
if int(C.getch()) != int(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func escSequence() Event {
|
||||
C.nodelay(C.stdscr, true)
|
||||
defer func() {
|
||||
C.nodelay(C.stdscr, false)
|
||||
}()
|
||||
c := C.getch()
|
||||
switch c {
|
||||
case C.ERR:
|
||||
return Event{ESC, 0, nil}
|
||||
case CtrlM:
|
||||
return Event{AltEnter, 0, nil}
|
||||
case '/':
|
||||
return Event{AltSlash, 0, nil}
|
||||
case ' ':
|
||||
return Event{AltSpace, 0, nil}
|
||||
case 127, C.KEY_BACKSPACE:
|
||||
return Event{AltBS, 0, nil}
|
||||
case '[':
|
||||
// Bracketed paste mode (printf "\e[?2004h")
|
||||
// \e[200~ TEXT \e[201~
|
||||
if consume('2', '0', '0', '~') {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
}
|
||||
if c >= 'a' && c <= 'z' {
|
||||
return Event{AltA + int(c) - 'a', 0, nil}
|
||||
}
|
||||
|
||||
if c >= '0' && c <= '9' {
|
||||
return Event{Alt0 + int(c) - '0', 0, nil}
|
||||
}
|
||||
|
||||
// Don't care. Ignore the rest.
|
||||
for ; c != C.ERR; c = C.getch() {
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) GetChar() Event {
|
||||
c := C.getch()
|
||||
switch c {
|
||||
case C.ERR:
|
||||
// Unexpected error from blocking read
|
||||
r.Close()
|
||||
errorExit("Failed to read /dev/tty")
|
||||
case C.KEY_UP:
|
||||
return Event{Up, 0, nil}
|
||||
case C.KEY_DOWN:
|
||||
return Event{Down, 0, nil}
|
||||
case C.KEY_LEFT:
|
||||
return Event{Left, 0, nil}
|
||||
case C.KEY_RIGHT:
|
||||
return Event{Right, 0, nil}
|
||||
case C.KEY_HOME:
|
||||
return Event{Home, 0, nil}
|
||||
case C.KEY_END:
|
||||
return Event{End, 0, nil}
|
||||
case C.KEY_BACKSPACE:
|
||||
return Event{BSpace, 0, nil}
|
||||
case C.KEY_F0 + 1:
|
||||
return Event{F1, 0, nil}
|
||||
case C.KEY_F0 + 2:
|
||||
return Event{F2, 0, nil}
|
||||
case C.KEY_F0 + 3:
|
||||
return Event{F3, 0, nil}
|
||||
case C.KEY_F0 + 4:
|
||||
return Event{F4, 0, nil}
|
||||
case C.KEY_F0 + 5:
|
||||
return Event{F5, 0, nil}
|
||||
case C.KEY_F0 + 6:
|
||||
return Event{F6, 0, nil}
|
||||
case C.KEY_F0 + 7:
|
||||
return Event{F7, 0, nil}
|
||||
case C.KEY_F0 + 8:
|
||||
return Event{F8, 0, nil}
|
||||
case C.KEY_F0 + 9:
|
||||
return Event{F9, 0, nil}
|
||||
case C.KEY_F0 + 10:
|
||||
return Event{F10, 0, nil}
|
||||
case C.KEY_F0 + 11:
|
||||
return Event{F11, 0, nil}
|
||||
case C.KEY_F0 + 12:
|
||||
return Event{F12, 0, nil}
|
||||
case C.KEY_DC:
|
||||
return Event{Del, 0, nil}
|
||||
case C.KEY_PPAGE:
|
||||
return Event{PgUp, 0, nil}
|
||||
case C.KEY_NPAGE:
|
||||
return Event{PgDn, 0, nil}
|
||||
case C.KEY_BTAB:
|
||||
return Event{BTab, 0, nil}
|
||||
case C.KEY_ENTER:
|
||||
return Event{CtrlM, 0, nil}
|
||||
case C.KEY_SLEFT:
|
||||
return Event{SLeft, 0, nil}
|
||||
case C.KEY_SRIGHT:
|
||||
return Event{SRight, 0, nil}
|
||||
case C.KEY_MOUSE:
|
||||
var me C.MEVENT
|
||||
if C.getmouse(&me) != C.ERR {
|
||||
mod := ((me.bstate & C.BUTTON_SHIFT) | (me.bstate & C.BUTTON_CTRL) | (me.bstate & C.BUTTON_ALT)) > 0
|
||||
x := int(me.x)
|
||||
y := int(me.y)
|
||||
/* Cannot use BUTTON1_DOUBLE_CLICKED due to mouseinterval(0) */
|
||||
if (me.bstate & C.BUTTON1_PRESSED) > 0 {
|
||||
now := time.Now()
|
||||
if now.Sub(r.prevDownTime) < doubleClickDuration {
|
||||
r.clickY = append(r.clickY, y)
|
||||
} else {
|
||||
r.clickY = []int{y}
|
||||
r.prevDownTime = now
|
||||
}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, false, mod}}
|
||||
} else if (me.bstate & C.BUTTON1_RELEASED) > 0 {
|
||||
double := false
|
||||
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
|
||||
time.Now().Sub(r.prevDownTime) < doubleClickDuration {
|
||||
double = true
|
||||
}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, double, mod}}
|
||||
} else if (me.bstate&0x8000000) > 0 || (me.bstate&0x80) > 0 {
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, mod}}
|
||||
} else if (me.bstate & C.BUTTON4_PRESSED) > 0 {
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 1, false, false, mod}}
|
||||
}
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
case C.KEY_RESIZE:
|
||||
return Event{Resize, 0, nil}
|
||||
case ESC:
|
||||
return escSequence()
|
||||
case 127:
|
||||
return Event{BSpace, 0, nil}
|
||||
case 0:
|
||||
return Event{CtrlSpace, 0, nil}
|
||||
}
|
||||
// CTRL-A ~ CTRL-Z
|
||||
if c >= CtrlA && c <= CtrlZ {
|
||||
return Event{int(c), 0, nil}
|
||||
}
|
||||
|
||||
// Multi-byte character
|
||||
buffer := []byte{byte(c)}
|
||||
for {
|
||||
r, _ := utf8.DecodeRune(buffer)
|
||||
if r != utf8.RuneError {
|
||||
return Event{Rune, r, nil}
|
||||
}
|
||||
|
||||
c := C.getch()
|
||||
if c == C.ERR {
|
||||
break
|
||||
}
|
||||
if c >= C.KEY_CODE_YES {
|
||||
C.ungetch(c)
|
||||
break
|
||||
}
|
||||
buffer = append(buffer, byte(c))
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
571
src/tui/tcell.go
Normal file
571
src/tui/tcell.go
Normal file
@@ -0,0 +1,571 @@
|
||||
// +build tcell windows
|
||||
|
||||
package tui
|
||||
|
||||
import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"runtime"
|
||||
|
||||
// https://github.com/gdamore/tcell/pull/135
|
||||
"github.com/junegunn/tcell"
|
||||
"github.com/junegunn/tcell/encoding"
|
||||
|
||||
"github.com/junegunn/go-runewidth"
|
||||
)
|
||||
|
||||
func HasFullscreenRenderer() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p ColorPair) style() tcell.Style {
|
||||
style := tcell.StyleDefault
|
||||
return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg()))
|
||||
}
|
||||
|
||||
type Attr tcell.Style
|
||||
|
||||
type TcellWindow struct {
|
||||
color bool
|
||||
top int
|
||||
left int
|
||||
width int
|
||||
height int
|
||||
lastX int
|
||||
lastY int
|
||||
moveCursor bool
|
||||
borderStyle BorderStyle
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Top() int {
|
||||
return w.top
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Left() int {
|
||||
return w.left
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Width() int {
|
||||
return w.width
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Height() int {
|
||||
return w.height
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Refresh() {
|
||||
if w.moveCursor {
|
||||
_screen.ShowCursor(w.left+w.lastX, w.top+w.lastY)
|
||||
w.moveCursor = false
|
||||
}
|
||||
w.lastX = 0
|
||||
w.lastY = 0
|
||||
switch w.borderStyle {
|
||||
case BorderAround:
|
||||
w.drawBorder(true)
|
||||
case BorderHorizontal:
|
||||
w.drawBorder(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *TcellWindow) FinishFill() {
|
||||
// NO-OP
|
||||
}
|
||||
|
||||
const (
|
||||
Bold Attr = Attr(tcell.AttrBold)
|
||||
Dim = Attr(tcell.AttrDim)
|
||||
Blink = Attr(tcell.AttrBlink)
|
||||
Reverse = Attr(tcell.AttrReverse)
|
||||
Underline = Attr(tcell.AttrUnderline)
|
||||
Italic = Attr(tcell.AttrNone) // Not supported
|
||||
)
|
||||
|
||||
const (
|
||||
AttrRegular Attr = 0
|
||||
)
|
||||
|
||||
func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
|
||||
if _screen.Colors() >= 256 {
|
||||
return Dark256
|
||||
}
|
||||
return Default16
|
||||
}
|
||||
|
||||
var (
|
||||
_colorToAttribute = []tcell.Color{
|
||||
tcell.ColorBlack,
|
||||
tcell.ColorRed,
|
||||
tcell.ColorGreen,
|
||||
tcell.ColorYellow,
|
||||
tcell.ColorBlue,
|
||||
tcell.ColorDarkMagenta,
|
||||
tcell.ColorLightCyan,
|
||||
tcell.ColorWhite,
|
||||
}
|
||||
)
|
||||
|
||||
func (c Color) Style() tcell.Color {
|
||||
if c <= colDefault {
|
||||
return tcell.ColorDefault
|
||||
} else if c >= colBlack && c <= colWhite {
|
||||
return _colorToAttribute[int(c)]
|
||||
} else {
|
||||
return tcell.Color(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (a Attr) Merge(b Attr) Attr {
|
||||
return a | b
|
||||
}
|
||||
|
||||
var (
|
||||
_screen tcell.Screen
|
||||
)
|
||||
|
||||
func (r *FullscreenRenderer) initScreen() {
|
||||
s, e := tcell.NewScreen()
|
||||
if e != nil {
|
||||
errorExit(e.Error())
|
||||
}
|
||||
if e = s.Init(); e != nil {
|
||||
errorExit(e.Error())
|
||||
}
|
||||
if r.mouse {
|
||||
s.EnableMouse()
|
||||
} else {
|
||||
s.DisableMouse()
|
||||
}
|
||||
_screen = s
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Init() {
|
||||
encoding.Register()
|
||||
|
||||
r.initScreen()
|
||||
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) MaxX() int {
|
||||
ncols, _ := _screen.Size()
|
||||
return int(ncols)
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) MaxY() int {
|
||||
_, nlines := _screen.Size()
|
||||
return int(nlines)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) X() int {
|
||||
return w.lastX
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) DoesAutoWrap() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) IsOptimized() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Clear() {
|
||||
_screen.Sync()
|
||||
_screen.Clear()
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Refresh() {
|
||||
// noop
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) GetChar() Event {
|
||||
ev := _screen.PollEvent()
|
||||
switch ev := ev.(type) {
|
||||
case *tcell.EventResize:
|
||||
return Event{Resize, 0, nil}
|
||||
|
||||
// process mouse events:
|
||||
case *tcell.EventMouse:
|
||||
x, y := ev.Position()
|
||||
button := ev.Buttons()
|
||||
mod := ev.Modifiers() != 0
|
||||
if button&tcell.WheelDown != 0 {
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, 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(r.prevDownTime) < doubleClickDuration {
|
||||
r.clickY = append(r.clickY, x)
|
||||
} else {
|
||||
r.clickY = []int{x}
|
||||
r.prevDownTime = now
|
||||
}
|
||||
} else {
|
||||
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
|
||||
time.Now().Sub(r.prevDownTime) < doubleClickDuration {
|
||||
double = true
|
||||
}
|
||||
}
|
||||
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, 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.KeyCtrlSpace:
|
||||
return Event{CtrlSpace, 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{F11, 0, nil}
|
||||
case tcell.KeyF12:
|
||||
return Event{F12, 0, nil}
|
||||
|
||||
// ev.Ch doesn't work for some reason for space:
|
||||
case tcell.KeyRune:
|
||||
r := ev.Rune()
|
||||
if alt {
|
||||
switch r {
|
||||
case ' ':
|
||||
return Event{AltSpace, 0, nil}
|
||||
case '/':
|
||||
return Event{AltSlash, 0, nil}
|
||||
}
|
||||
if r >= 'a' && r <= 'z' {
|
||||
return Event{AltA + int(r) - 'a', 0, nil}
|
||||
}
|
||||
if r >= '0' && r <= '9' {
|
||||
return Event{Alt0 + int(r) - '0', 0, nil}
|
||||
}
|
||||
}
|
||||
return Event{Rune, r, nil}
|
||||
|
||||
case tcell.KeyEsc:
|
||||
return Event{ESC, 0, nil}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Pause() {
|
||||
_screen.Fini()
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Resume() bool {
|
||||
r.initScreen()
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Close() {
|
||||
_screen.Fini()
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
|
||||
// TODO
|
||||
for _, w := range windows {
|
||||
w.Refresh()
|
||||
}
|
||||
_screen.Show()
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
|
||||
// TODO
|
||||
return &TcellWindow{
|
||||
color: r.theme != nil,
|
||||
top: top,
|
||||
left: left,
|
||||
width: width,
|
||||
height: height,
|
||||
borderStyle: borderStyle}
|
||||
}
|
||||
|
||||
func (w *TcellWindow) 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 *TcellWindow) Erase() {
|
||||
// TODO
|
||||
fill(w.left, w.top, w.width, w.height, ' ')
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Enclose(y int, x int) bool {
|
||||
return x >= w.left && x < (w.left+w.width) &&
|
||||
y >= w.top && y < (w.top+w.height)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Move(y int, x int) {
|
||||
w.lastX = x
|
||||
w.lastY = y
|
||||
w.moveCursor = true
|
||||
}
|
||||
|
||||
func (w *TcellWindow) MoveAndClear(y int, x int) {
|
||||
w.Move(y, x)
|
||||
for i := w.lastX; i < w.width; i++ {
|
||||
_screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, ColDefault.style())
|
||||
}
|
||||
w.lastX = x
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Print(text string) {
|
||||
w.printString(text, ColDefault, 0)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) {
|
||||
t := text
|
||||
lx := 0
|
||||
|
||||
var style tcell.Style
|
||||
if w.color {
|
||||
style = pair.style().
|
||||
Reverse(a&Attr(tcell.AttrReverse) != 0).
|
||||
Underline(a&Attr(tcell.AttrUnderline) != 0)
|
||||
} else {
|
||||
style = 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.lastY++
|
||||
lx = 0
|
||||
} else {
|
||||
|
||||
if r == '\u000D' { // skip carriage return
|
||||
continue
|
||||
}
|
||||
|
||||
var xPos = w.left + w.lastX + lx
|
||||
var yPos = w.top + w.lastY
|
||||
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
|
||||
_screen.SetContent(xPos, yPos, r, nil, style)
|
||||
}
|
||||
lx += runewidth.RuneWidth(r)
|
||||
}
|
||||
}
|
||||
w.lastX += lx
|
||||
}
|
||||
|
||||
func (w *TcellWindow) CPrint(pair ColorPair, attr Attr, text string) {
|
||||
w.printString(text, pair, attr)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) FillReturn {
|
||||
lx := 0
|
||||
|
||||
var style tcell.Style
|
||||
if w.color {
|
||||
style = pair.style()
|
||||
} else {
|
||||
style = 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.lastY++
|
||||
w.lastX = 0
|
||||
lx = 0
|
||||
} else {
|
||||
var xPos = w.left + w.lastX + lx
|
||||
|
||||
// word wrap:
|
||||
if xPos >= (w.left + w.width) {
|
||||
w.lastY++
|
||||
w.lastX = 0
|
||||
lx = 0
|
||||
xPos = w.left
|
||||
}
|
||||
var yPos = w.top + w.lastY
|
||||
|
||||
if yPos >= (w.top + w.height) {
|
||||
return FillSuspend
|
||||
}
|
||||
|
||||
_screen.SetContent(xPos, yPos, r, nil, style)
|
||||
lx += runewidth.RuneWidth(r)
|
||||
}
|
||||
}
|
||||
w.lastX += lx
|
||||
|
||||
return FillContinue
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Fill(str string) FillReturn {
|
||||
return w.fillString(str, ColDefault, 0)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
|
||||
return w.fillString(str, ColorPair{fg, bg, -1}, a)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) drawBorder(around bool) {
|
||||
left := w.left
|
||||
right := left + w.width
|
||||
top := w.top
|
||||
bot := top + w.height
|
||||
|
||||
var style tcell.Style
|
||||
if w.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)
|
||||
}
|
||||
|
||||
if around {
|
||||
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)
|
||||
}
|
||||
}
|
425
src/tui/tui.go
Normal file
425
src/tui/tui.go
Normal file
@@ -0,0 +1,425 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Types of user action
|
||||
const (
|
||||
Rune = iota
|
||||
|
||||
CtrlA
|
||||
CtrlB
|
||||
CtrlC
|
||||
CtrlD
|
||||
CtrlE
|
||||
CtrlF
|
||||
CtrlG
|
||||
CtrlH
|
||||
Tab
|
||||
CtrlJ
|
||||
CtrlK
|
||||
CtrlL
|
||||
CtrlM
|
||||
CtrlN
|
||||
CtrlO
|
||||
CtrlP
|
||||
CtrlQ
|
||||
CtrlR
|
||||
CtrlS
|
||||
CtrlT
|
||||
CtrlU
|
||||
CtrlV
|
||||
CtrlW
|
||||
CtrlX
|
||||
CtrlY
|
||||
CtrlZ
|
||||
ESC
|
||||
CtrlSpace
|
||||
|
||||
Invalid
|
||||
Resize
|
||||
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
|
||||
F11
|
||||
F12
|
||||
|
||||
AltEnter
|
||||
AltSpace
|
||||
AltSlash
|
||||
AltBS
|
||||
|
||||
Alt0
|
||||
)
|
||||
|
||||
const ( // Reset iota
|
||||
AltA = Alt0 + 'a' - '0' + iota
|
||||
AltB
|
||||
AltC
|
||||
AltD
|
||||
AltE
|
||||
AltF
|
||||
AltZ = AltA + 'z' - 'a'
|
||||
)
|
||||
|
||||
const (
|
||||
doubleClickDuration = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
type Color int32
|
||||
|
||||
func (c Color) is24() bool {
|
||||
return c > 0 && (c&(1<<24)) > 0
|
||||
}
|
||||
|
||||
const (
|
||||
colUndefined Color = -2
|
||||
colDefault = -1
|
||||
)
|
||||
|
||||
const (
|
||||
colBlack Color = iota
|
||||
colRed
|
||||
colGreen
|
||||
colYellow
|
||||
colBlue
|
||||
colMagenta
|
||||
colCyan
|
||||
colWhite
|
||||
)
|
||||
|
||||
type FillReturn int
|
||||
|
||||
const (
|
||||
FillContinue FillReturn = iota
|
||||
FillNextLine
|
||||
FillSuspend
|
||||
)
|
||||
|
||||
type ColorPair struct {
|
||||
fg Color
|
||||
bg Color
|
||||
id int16
|
||||
}
|
||||
|
||||
func HexToColor(rrggbb string) Color {
|
||||
r, _ := strconv.ParseInt(rrggbb[1:3], 16, 0)
|
||||
g, _ := strconv.ParseInt(rrggbb[3:5], 16, 0)
|
||||
b, _ := strconv.ParseInt(rrggbb[5:7], 16, 0)
|
||||
return Color((1 << 24) + (r << 16) + (g << 8) + b)
|
||||
}
|
||||
|
||||
func NewColorPair(fg Color, bg Color) ColorPair {
|
||||
return ColorPair{fg, bg, -1}
|
||||
}
|
||||
|
||||
func (p ColorPair) Fg() Color {
|
||||
return p.fg
|
||||
}
|
||||
|
||||
func (p ColorPair) Bg() Color {
|
||||
return p.bg
|
||||
}
|
||||
|
||||
func (p ColorPair) key() int {
|
||||
return (int(p.Fg()) << 8) + int(p.Bg())
|
||||
}
|
||||
|
||||
func (p ColorPair) is24() bool {
|
||||
return p.Fg().is24() || p.Bg().is24()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (t *ColorTheme) HasBg() bool {
|
||||
return t.Bg != colDefault
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
Type int
|
||||
Char rune
|
||||
MouseEvent *MouseEvent
|
||||
}
|
||||
|
||||
type MouseEvent struct {
|
||||
Y int
|
||||
X int
|
||||
S int
|
||||
Down bool
|
||||
Double bool
|
||||
Mod bool
|
||||
}
|
||||
|
||||
type BorderStyle int
|
||||
|
||||
const (
|
||||
BorderNone BorderStyle = iota
|
||||
BorderAround
|
||||
BorderHorizontal
|
||||
)
|
||||
|
||||
type Renderer interface {
|
||||
Init()
|
||||
Pause()
|
||||
Resume() bool
|
||||
Clear()
|
||||
RefreshWindows(windows []Window)
|
||||
Refresh()
|
||||
Close()
|
||||
|
||||
GetChar() Event
|
||||
|
||||
MaxX() int
|
||||
MaxY() int
|
||||
DoesAutoWrap() bool
|
||||
IsOptimized() bool
|
||||
|
||||
NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window
|
||||
}
|
||||
|
||||
type Window interface {
|
||||
Top() int
|
||||
Left() int
|
||||
Width() int
|
||||
Height() int
|
||||
|
||||
Refresh()
|
||||
FinishFill()
|
||||
Close()
|
||||
|
||||
X() int
|
||||
Enclose(y int, x int) bool
|
||||
|
||||
Move(y int, x int)
|
||||
MoveAndClear(y int, x int)
|
||||
Print(text string)
|
||||
CPrint(color ColorPair, attr Attr, text string)
|
||||
Fill(text string) FillReturn
|
||||
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
|
||||
Erase()
|
||||
}
|
||||
|
||||
type FullscreenRenderer struct {
|
||||
theme *ColorTheme
|
||||
mouse bool
|
||||
forceBlack bool
|
||||
prevDownTime time.Time
|
||||
clickY []int
|
||||
}
|
||||
|
||||
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
|
||||
r := &FullscreenRenderer{
|
||||
theme: theme,
|
||||
mouse: mouse,
|
||||
forceBlack: forceBlack,
|
||||
prevDownTime: time.Unix(0, 0),
|
||||
clickY: []int{}}
|
||||
return r
|
||||
}
|
||||
|
||||
var (
|
||||
Default16 *ColorTheme
|
||||
Dark256 *ColorTheme
|
||||
Light256 *ColorTheme
|
||||
|
||||
ColDefault ColorPair
|
||||
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 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 errorExit(message string) {
|
||||
fmt.Fprintln(os.Stderr, message)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func init() {
|
||||
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, baseTheme *ColorTheme, forceBlack bool) {
|
||||
if theme == nil {
|
||||
initPalette(theme)
|
||||
return
|
||||
}
|
||||
|
||||
if forceBlack {
|
||||
theme.Bg = colBlack
|
||||
}
|
||||
|
||||
o := func(a Color, b Color) Color {
|
||||
if b == colUndefined {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
theme.Fg = o(baseTheme.Fg, theme.Fg)
|
||||
theme.Bg = o(baseTheme.Bg, theme.Bg)
|
||||
theme.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)
|
||||
|
||||
initPalette(theme)
|
||||
}
|
||||
|
||||
func initPalette(theme *ColorTheme) {
|
||||
ColDefault = ColorPair{colDefault, colDefault, 0}
|
||||
if theme != nil {
|
||||
ColNormal = ColorPair{theme.Fg, theme.Bg, 1}
|
||||
ColPrompt = ColorPair{theme.Prompt, theme.Bg, 2}
|
||||
ColMatch = ColorPair{theme.Match, theme.Bg, 3}
|
||||
ColCurrent = ColorPair{theme.Current, theme.DarkBg, 4}
|
||||
ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg, 5}
|
||||
ColSpinner = ColorPair{theme.Spinner, theme.Bg, 6}
|
||||
ColInfo = ColorPair{theme.Info, theme.Bg, 7}
|
||||
ColCursor = ColorPair{theme.Cursor, theme.DarkBg, 8}
|
||||
ColSelected = ColorPair{theme.Selected, theme.DarkBg, 9}
|
||||
ColHeader = ColorPair{theme.Header, theme.Bg, 10}
|
||||
ColBorder = ColorPair{theme.Border, theme.Bg, 11}
|
||||
} else {
|
||||
ColNormal = ColorPair{colDefault, colDefault, 1}
|
||||
ColPrompt = ColorPair{colDefault, colDefault, 2}
|
||||
ColMatch = ColorPair{colDefault, colDefault, 3}
|
||||
ColCurrent = ColorPair{colDefault, colDefault, 4}
|
||||
ColCurrentMatch = ColorPair{colDefault, colDefault, 5}
|
||||
ColSpinner = ColorPair{colDefault, colDefault, 6}
|
||||
ColInfo = ColorPair{colDefault, colDefault, 7}
|
||||
ColCursor = ColorPair{colDefault, colDefault, 8}
|
||||
ColSelected = ColorPair{colDefault, colDefault, 9}
|
||||
ColHeader = ColorPair{colDefault, colDefault, 10}
|
||||
ColBorder = ColorPair{colDefault, colDefault, 11}
|
||||
}
|
||||
ColUser = ColorPair{colDefault, colDefault, 12}
|
||||
}
|
||||
|
||||
func attrFor(color ColorPair, attr Attr) Attr {
|
||||
switch color {
|
||||
case ColCurrent:
|
||||
return attr | Reverse
|
||||
case ColMatch:
|
||||
return attr | Underline
|
||||
case ColCurrentMatch:
|
||||
return attr | Underline | Reverse
|
||||
}
|
||||
return attr
|
||||
}
|
20
src/tui/tui_test.go
Normal file
20
src/tui/tui_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package tui
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestHexToColor(t *testing.T) {
|
||||
assert := func(expr string, r, g, b int) {
|
||||
color := HexToColor(expr)
|
||||
if !color.is24() ||
|
||||
int((color>>16)&0xff) != r ||
|
||||
int((color>>8)&0xff) != g ||
|
||||
int((color)&0xff) != b {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
assert("#ff0000", 255, 0, 0)
|
||||
assert("#010203", 1, 2, 3)
|
||||
assert("#102030", 16, 32, 48)
|
||||
assert("#ffffff", 255, 255, 255)
|
||||
}
|
@@ -23,21 +23,23 @@ end
|
||||
# List assets
|
||||
assets = Hash[rel['assets'].map { |a| a.values_at *%w[name id] }]
|
||||
|
||||
files.select { |f| File.exists? f }.each do |file|
|
||||
name = File.basename file
|
||||
files.select { |f| File.exists? f }.map do |file|
|
||||
Thread.new do
|
||||
name = File.basename file
|
||||
|
||||
if asset_id = assets[name]
|
||||
puts "#{name} found. Deleting asset id #{asset_id}."
|
||||
RestClient.delete "#{base}/assets/#{asset_id}",
|
||||
:authorization => "token #{token}"
|
||||
else
|
||||
puts "#{name} not found"
|
||||
if asset_id = assets[name]
|
||||
puts "#{name} found. Deleting asset id #{asset_id}."
|
||||
RestClient.delete "#{base}/assets/#{asset_id}",
|
||||
:authorization => "token #{token}"
|
||||
else
|
||||
puts "#{name} not found"
|
||||
end
|
||||
|
||||
puts "Uploading #{name}"
|
||||
RestClient.post(
|
||||
"#{base.sub 'api', 'uploads'}/#{rel['id']}/assets?name=#{name}",
|
||||
File.read(file),
|
||||
:authorization => "token #{token}",
|
||||
:content_type => "application/octet-stream")
|
||||
end
|
||||
|
||||
puts "Uploading #{name}"
|
||||
RestClient.post(
|
||||
"#{base.sub 'api', 'uploads'}/#{rel['id']}/assets?name=#{name}",
|
||||
File.read(file),
|
||||
:authorization => "token #{token}",
|
||||
:content_type => "application/octet-stream")
|
||||
end
|
||||
end.each(&:join)
|
||||
|
@@ -1,15 +1,29 @@
|
||||
package util
|
||||
|
||||
// #include <unistd.h>
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/go-isatty"
|
||||
"github.com/junegunn/go-runewidth"
|
||||
)
|
||||
|
||||
var _runeWidths = make(map[rune]int)
|
||||
|
||||
// RuneWidth returns rune width
|
||||
func RuneWidth(r rune, prefixWidth int, tabstop int) int {
|
||||
if r == '\t' {
|
||||
return tabstop - prefixWidth%tabstop
|
||||
} else if w, found := _runeWidths[r]; found {
|
||||
return w
|
||||
} else {
|
||||
w := Max(runewidth.RuneWidth(r), 1)
|
||||
_runeWidths[r] = w
|
||||
return w
|
||||
}
|
||||
}
|
||||
|
||||
// Max returns the largest integer
|
||||
func Max(first int, second int) int {
|
||||
if first >= second {
|
||||
@@ -95,14 +109,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())
|
||||
}
|
||||
|
28
src/util/util_unix.go
Normal file
28
src/util/util_unix.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// +build !windows
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// SetNonBlock executes syscall.SetNonblock on file descriptor
|
||||
func SetNonblock(file *os.File, nonblock bool) {
|
||||
syscall.SetNonblock(int(file.Fd()), nonblock)
|
||||
}
|
34
src/util/util_windows.go
Normal file
34
src/util/util_windows.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// +build windows
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
// SetNonBlock executes syscall.SetNonblock on file descriptor
|
||||
func SetNonblock(file *os.File, nonblock bool) {
|
||||
syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock)
|
||||
}
|
420
test/test_go.rb
420
test/test_go.rb
@@ -111,17 +111,39 @@ class Tmux
|
||||
File.read(TEMPNAME).split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
|
||||
end
|
||||
|
||||
def until pane = 0
|
||||
def until refresh = false, pane = 0
|
||||
lines = nil
|
||||
begin
|
||||
wait do
|
||||
lines = capture(pane)
|
||||
class << lines
|
||||
def counts
|
||||
self.lazy
|
||||
.map { |l| l.scan /^. ([0-9]+)\/([0-9]+)( \(([0-9]+)\))?/ }
|
||||
.reject(&:empty?)
|
||||
.first&.first&.map(&:to_i)&.values_at(0, 1, 3) || [0, 0, 0]
|
||||
end
|
||||
|
||||
def match_count
|
||||
counts[0]
|
||||
end
|
||||
|
||||
def item_count
|
||||
self[-2] ? self[-2].strip.split('/').last.to_i : 0
|
||||
counts[1]
|
||||
end
|
||||
|
||||
def select_count
|
||||
counts[2]
|
||||
end
|
||||
|
||||
def any_include? val
|
||||
method = val.is_a?(Regexp) ? :match : :include?
|
||||
self.select { |line| line.send method, val }.first
|
||||
end
|
||||
end
|
||||
yield lines
|
||||
yield(lines).tap do |ok|
|
||||
send_keys 'C-l' if refresh && !ok
|
||||
end
|
||||
end
|
||||
rescue Exception
|
||||
puts $!.backtrace
|
||||
@@ -163,6 +185,11 @@ class TestBase < Minitest::Test
|
||||
@temp_suffix].join '-'
|
||||
end
|
||||
|
||||
def writelines path, lines
|
||||
File.unlink path while File.exists? path
|
||||
File.open(path, 'w') { |f| f << lines.join($/) + $/ }
|
||||
end
|
||||
|
||||
def readonce
|
||||
wait { File.exists?(tempname) }
|
||||
File.read(tempname)
|
||||
@@ -297,6 +324,19 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| lines.last !~ /^>/ }
|
||||
end
|
||||
|
||||
def test_file_word
|
||||
tmux.send_keys "#{FZF} -q '--/foo bar/foo-bar/baz' --filepath-word", :Enter
|
||||
tmux.until { |lines| lines.last =~ /^>/ }
|
||||
|
||||
tmux.send_keys :Escape, :b
|
||||
tmux.send_keys :Escape, :b
|
||||
tmux.send_keys :Escape, :b
|
||||
tmux.send_keys :Escape, :d
|
||||
tmux.send_keys :Escape, :f
|
||||
tmux.send_keys :Escape, :BSpace
|
||||
tmux.until { |lines| lines.last == '> --///baz' }
|
||||
end
|
||||
|
||||
def test_multi_order
|
||||
tmux.send_keys "seq 1 10 | #{fzf :multi}", :Enter
|
||||
tmux.until { |lines| lines.last =~ /^>/ }
|
||||
@@ -577,6 +617,17 @@ class TestGoFZF < TestBase
|
||||
], `#{FZF} -foo --tiebreak=begin,length < #{tempname}`.split($/)
|
||||
end
|
||||
|
||||
def test_tiebreak_begin_algo_v2
|
||||
writelines tempname, [
|
||||
'baz foo bar',
|
||||
'foo bar baz',
|
||||
]
|
||||
assert_equal [
|
||||
'foo bar baz',
|
||||
'baz foo bar',
|
||||
], `#{FZF} -fbar --tiebreak=begin --algo=v2 < #{tempname}`.split($/)
|
||||
end
|
||||
|
||||
def test_tiebreak_end
|
||||
writelines tempname, [
|
||||
'xoxxxxxxxx',
|
||||
@@ -839,7 +890,7 @@ class TestGoFZF < TestBase
|
||||
|
||||
def test_execute_multi
|
||||
output = '/tmp/fzf-test-execute-multi'
|
||||
opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{} >> #{output}; sync)\\"]
|
||||
opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{+} >> #{output}; sync)\\"]
|
||||
writelines tempname, %w[foo'bar foo"bar foo$bar foobar]
|
||||
tmux.send_keys "cat #{tempname} | #{fzf opts}", :Enter
|
||||
tmux.until { |lines| lines[-2].include? '4/4' }
|
||||
@@ -862,6 +913,43 @@ class TestGoFZF < TestBase
|
||||
File.unlink output rescue nil
|
||||
end
|
||||
|
||||
def test_execute_plus_flag
|
||||
output = tempname + ".tmp"
|
||||
File.unlink output rescue nil
|
||||
writelines tempname, ["foo bar", "123 456"]
|
||||
|
||||
tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
|
||||
|
||||
execute = lambda do
|
||||
tmux.send_keys 'x', 'y'
|
||||
tmux.until { |lines| lines[-2].include? '0/2' }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| lines[-2].include? '2/2' }
|
||||
end
|
||||
|
||||
tmux.until { |lines| lines[-2].include? '2/2' }
|
||||
execute.call
|
||||
|
||||
tmux.send_keys :Up
|
||||
tmux.send_keys :Tab
|
||||
execute.call
|
||||
|
||||
tmux.send_keys :Tab
|
||||
execute.call
|
||||
|
||||
tmux.send_keys :Enter
|
||||
tmux.prepare
|
||||
readonce
|
||||
|
||||
assert_equal [
|
||||
%[foo bar/foo bar/bar/bar],
|
||||
%[123 456/foo bar/456/bar],
|
||||
%[123 456 foo bar/foo bar/456 bar/bar]
|
||||
], File.readlines(output).map(&:chomp)
|
||||
rescue
|
||||
File.unlink output rescue nil
|
||||
end
|
||||
|
||||
def test_execute_shell
|
||||
# Custom script to use as $SHELL
|
||||
output = tempname + '.out'
|
||||
@@ -1029,7 +1117,7 @@ class TestGoFZF < TestBase
|
||||
}.each do |ts, exp|
|
||||
tmux.prepare
|
||||
tmux.send_keys %[cat #{tempname} | fzf --tabstop=#{ts}], :Enter
|
||||
tmux.until { |lines| exp.start_with? lines[-3].to_s.strip.sub(/\.\.$/, '') }
|
||||
tmux.until(true) { |lines| exp.start_with? lines[-3].to_s.strip.sub(/\.\.$/, '') }
|
||||
tmux.send_keys :Enter
|
||||
end
|
||||
end
|
||||
@@ -1055,12 +1143,6 @@ class TestGoFZF < TestBase
|
||||
assert_equal 1, $?.exitstatus
|
||||
end
|
||||
|
||||
def test_invalid_term
|
||||
lines = `TERM=xxx #{FZF}`
|
||||
assert_equal 2, $?.exitstatus
|
||||
assert lines.include?('Invalid $TERM: xxx')
|
||||
end
|
||||
|
||||
def test_invalid_option
|
||||
lines = `#{FZF} --foobar 2>&1`
|
||||
assert_equal 2, $?.exitstatus
|
||||
@@ -1164,7 +1246,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_preview
|
||||
tmux.send_keys %[seq 1000 | sed s/^2$// | #{FZF} --preview 'sleep 0.2; echo {{}-{}}' --bind ?:toggle-preview], :Enter
|
||||
tmux.send_keys %[seq 1000 | sed s/^2$// | #{FZF} -m --preview 'sleep 0.2; echo {{}-{+}}' --bind ?:toggle-preview], :Enter
|
||||
tmux.until { |lines| lines[1].include?(' {1-1}') }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| lines[1].include?(' {-}') }
|
||||
@@ -1178,6 +1260,17 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| lines[-2].start_with? ' 28/1000' }
|
||||
tmux.send_keys 'foobar'
|
||||
tmux.until { |lines| !lines[1].include?('{') }
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.until { |lines| lines.match_count == 1000 }
|
||||
tmux.until { |lines| lines[1].include?(' {1-1}') }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| lines[1].include?(' {-1}') }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| lines[1].include?(' {3-1 }') }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| lines[1].include?(' {4-1 3}') }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| lines[1].include?(' {5-1 3 4}') }
|
||||
end
|
||||
|
||||
def test_preview_hidden
|
||||
@@ -1191,10 +1284,17 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| lines[-1] == '> 555' }
|
||||
end
|
||||
|
||||
private
|
||||
def writelines path, lines
|
||||
File.unlink path while File.exists? path
|
||||
File.open(path, 'w') { |f| f << lines.join($/) + $/ }
|
||||
def test_preview_size_0
|
||||
File.unlink tempname rescue nil
|
||||
tmux.send_keys %[seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0], :Enter
|
||||
tmux.until { |lines| lines.item_count == 100 && lines[1] == ' 100/100' && lines[2] == '> 1' }
|
||||
tmux.until { |_| %w[1] == File.readlines(tempname).map(&:chomp) }
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| lines[3] == '> 2' }
|
||||
tmux.until { |_| %w[1 2] == File.readlines(tempname).map(&:chomp) }
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| lines[4] == '> 3' }
|
||||
tmux.until { |_| %w[1 2 3] == File.readlines(tempname).map(&:chomp) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1213,74 +1313,66 @@ module TestShell
|
||||
tmux.prepare
|
||||
end
|
||||
|
||||
def test_ctrl_t
|
||||
def unset_var name
|
||||
tmux.prepare
|
||||
tmux.send_keys "unset #{name}", :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'C-t', pane: 0
|
||||
lines = tmux.until(1) { |lines| lines.item_count > 1 }
|
||||
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
|
||||
tmux.send_keys :BTab, :BTab, pane: 1
|
||||
tmux.until(1) { |lines| lines[-2].include?('(2)') }
|
||||
tmux.send_keys :Enter, pane: 1
|
||||
tmux.until(0) { |lines| lines[-1].include? expected }
|
||||
tmux.send_keys 'C-c'
|
||||
|
||||
# FZF_TMUX=0
|
||||
new_shell
|
||||
tmux.send_keys 'C-t', pane: 0
|
||||
lines = tmux.until(0) { |lines| lines.item_count > 1 }
|
||||
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
|
||||
tmux.send_keys :BTab, :BTab, pane: 0
|
||||
tmux.until(0) { |lines| lines[-2].include?('(2)') }
|
||||
tmux.send_keys :Enter, pane: 0
|
||||
tmux.until(0) { |lines| lines[-1].include? expected }
|
||||
tmux.send_keys 'C-c', 'C-d'
|
||||
end
|
||||
|
||||
def test_ctrl_t_command
|
||||
def test_ctrl_t
|
||||
set_var "FZF_CTRL_T_COMMAND", "seq 100"
|
||||
tmux.send_keys 'C-t', pane: 0
|
||||
lines = tmux.until(1) { |lines| lines.item_count == 100 }
|
||||
tmux.send_keys :BTab, :BTab, :BTab, pane: 1
|
||||
tmux.until(1) { |lines| lines[-2].include?('(3)') }
|
||||
tmux.send_keys :Enter, pane: 1
|
||||
tmux.until(0) { |lines| lines[-1].include? '1 2 3' }
|
||||
|
||||
lines = retries do
|
||||
tmux.prepare
|
||||
tmux.send_keys 'C-t'
|
||||
tmux.until { |lines| lines.item_count == 100 }
|
||||
end
|
||||
tmux.send_keys :Tab, :Tab, :Tab
|
||||
tmux.until { |lines| lines.any_include? ' (3)' }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| lines.any_include? '1 2 3' }
|
||||
tmux.send_keys 'C-c'
|
||||
end
|
||||
|
||||
def test_ctrl_t_unicode
|
||||
FileUtils.mkdir_p '/tmp/fzf-test'
|
||||
tmux.paste 'cd /tmp/fzf-test; echo -n test1 > "fzf-unicode 테스트1"; echo -n test2 > "fzf-unicode 테스트2"'
|
||||
tmux.prepare
|
||||
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/' }
|
||||
writelines tempname, ['fzf-unicode 테스트1', 'fzf-unicode 테스트2']
|
||||
set_var "FZF_CTRL_T_COMMAND", "cat #{tempname}"
|
||||
|
||||
tmux.send_keys '1', pane: 1
|
||||
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
|
||||
tmux.send_keys :BTab, pane: 1
|
||||
tmux.until(1) { |lines| lines[-2].include? '(1)' }
|
||||
retries do
|
||||
tmux.prepare
|
||||
tmux.send_keys 'echo ', 'C-t'
|
||||
tmux.until { |lines| lines.item_count == 2 }
|
||||
end
|
||||
tmux.send_keys 'fzf-unicode'
|
||||
tmux.until { |lines| lines.match_count == 2 }
|
||||
|
||||
tmux.send_keys :BSpace, pane: 1
|
||||
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' }
|
||||
tmux.send_keys '1'
|
||||
tmux.until { |lines| lines.match_count == 1 }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| lines.select_count == 1 }
|
||||
|
||||
tmux.send_keys '2', pane: 1
|
||||
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
|
||||
tmux.send_keys :BTab, pane: 1
|
||||
tmux.until(1) { |lines| lines[-2].include? '(2)' }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| lines.match_count == 2 }
|
||||
|
||||
tmux.send_keys '2'
|
||||
tmux.until { |lines| lines.match_count == 1 }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| lines.select_count == 2 }
|
||||
|
||||
tmux.send_keys :Enter, 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.send_keys :Enter
|
||||
tmux.until { |lines| lines[-1].include? 'test1test2' }
|
||||
tmux.until { |lines| lines.any_include? /echo.*fzf-unicode.*1.*fzf-unicode.*2/ }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| lines.any_include? /^fzf-unicode.*1.*fzf-unicode.*2/ }
|
||||
end
|
||||
|
||||
def test_alt_c
|
||||
tmux.prepare
|
||||
tmux.send_keys :Escape, :c, pane: 0
|
||||
lines = tmux.until(1) { |lines| lines.item_count > 0 && lines[-3][2..-1] }
|
||||
expected = lines[-3][2..-1]
|
||||
tmux.send_keys :Enter, pane: 1
|
||||
lines = retries do
|
||||
tmux.prepare
|
||||
tmux.send_keys :Escape, :c
|
||||
tmux.until { |lines| lines.item_count > 0 }
|
||||
end
|
||||
expected = lines.reverse.select { |l| l.start_with? '>' }.first[2..-1]
|
||||
tmux.send_keys :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys :pwd, :Enter
|
||||
tmux.until { |lines| lines[-1].end_with?(expected) }
|
||||
@@ -1292,10 +1384,12 @@ module TestShell
|
||||
tmux.prepare
|
||||
tmux.send_keys 'cd /', :Enter
|
||||
|
||||
tmux.prepare
|
||||
tmux.send_keys :Escape, :c, pane: 0
|
||||
lines = tmux.until(1) { |lines| lines.item_count == 1 }
|
||||
tmux.send_keys :Enter, pane: 1
|
||||
retries do
|
||||
tmux.prepare
|
||||
tmux.send_keys :Escape, :c
|
||||
lines = tmux.until { |lines| lines.item_count == 1 }
|
||||
end
|
||||
tmux.send_keys :Enter
|
||||
|
||||
tmux.prepare
|
||||
tmux.send_keys :pwd, :Enter
|
||||
@@ -1308,16 +1402,29 @@ module TestShell
|
||||
tmux.send_keys 'echo 2nd', :Enter; tmux.prepare
|
||||
tmux.send_keys 'echo 3d', :Enter; tmux.prepare
|
||||
tmux.send_keys 'echo 3rd', :Enter; tmux.prepare
|
||||
tmux.send_keys 'echo 4th', :Enter; tmux.prepare
|
||||
tmux.send_keys 'C-r', pane: 0
|
||||
tmux.until(1) { |lines| lines.item_count > 0 }
|
||||
tmux.send_keys '3d', pane: 1
|
||||
tmux.until(1) { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort
|
||||
tmux.send_keys :Enter, pane: 1
|
||||
tmux.send_keys 'echo 4th', :Enter
|
||||
retries do
|
||||
tmux.prepare
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| lines.item_count > 0 }
|
||||
end
|
||||
tmux.send_keys '3d'
|
||||
tmux.until { |lines| lines[-3].end_with? 'echo 3rd' }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| lines[-1] == 'echo 3rd' }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| lines[-1] == '3rd' }
|
||||
end
|
||||
|
||||
def retries times = 3, &block
|
||||
(times - 1).times do |t|
|
||||
begin
|
||||
return block.call
|
||||
rescue RuntimeError
|
||||
end
|
||||
end
|
||||
block.call
|
||||
end
|
||||
end
|
||||
|
||||
module CompletionTest
|
||||
@@ -1329,60 +1436,50 @@ module CompletionTest
|
||||
FileUtils.touch File.expand_path(f)
|
||||
end
|
||||
tmux.prepare
|
||||
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0
|
||||
tmux.until(1) { |lines| lines.item_count > 0 }
|
||||
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab
|
||||
tmux.until { |lines| lines.item_count > 0 }
|
||||
tmux.send_keys ' !d'
|
||||
tmux.until(1) { |lines| lines[-2].include?(' 2/') }
|
||||
tmux.send_keys :BTab, :BTab
|
||||
tmux.until(1) { |lines| lines[-2].include?('(2)') }
|
||||
tmux.until { |lines| lines.match_count == 2 }
|
||||
tmux.send_keys :Tab, :Tab
|
||||
tmux.until { |lines| lines.select_count == 2 }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until do |lines|
|
||||
tmux.send_keys 'C-L'
|
||||
tmux.until(true) do |lines|
|
||||
lines[-1].include?('/tmp/fzf-test/10') &&
|
||||
lines[-1].include?('/tmp/fzf-test/100')
|
||||
end
|
||||
|
||||
# ~USERNAME**<TAB>
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.send_keys "cat ~#{ENV['USER']}**", :Tab, pane: 0
|
||||
tmux.until(1) { |lines| lines.item_count > 0 }
|
||||
tmux.send_keys '.fzf-home'
|
||||
tmux.until(1) { |lines| lines[-3].end_with? '.fzf-home' }
|
||||
tmux.send_keys "cat ~#{ENV['USER']}**", :Tab
|
||||
tmux.until { |lines| lines.item_count > 0 }
|
||||
tmux.send_keys "'.fzf-home"
|
||||
tmux.until { |lines| lines.select { |l| l.include? '.fzf-home' }.count > 1 }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until do |lines|
|
||||
tmux.send_keys 'C-L'
|
||||
tmux.until(true) do |lines|
|
||||
lines[-1].end_with?('.fzf-home')
|
||||
end
|
||||
|
||||
# ~INVALID_USERNAME**<TAB>
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.send_keys "cat ~such**", :Tab, pane: 0
|
||||
tmux.until(1) { |lines| lines[-3].end_with? 'no~such~user' }
|
||||
tmux.send_keys "cat ~such**", :Tab
|
||||
tmux.until(true) { |lines| lines.any_include? 'no~such~user' }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until do |lines|
|
||||
tmux.send_keys 'C-L'
|
||||
lines[-1].end_with?('no~such~user')
|
||||
end
|
||||
tmux.until(true) { |lines| lines[-1].end_with?('no~such~user') }
|
||||
|
||||
# /tmp/fzf\ test**<TAB>
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab, pane: 0
|
||||
tmux.until(1) { |lines| lines.item_count > 0 }
|
||||
tmux.send_keys 'C-K', :Enter
|
||||
tmux.until do |lines|
|
||||
tmux.send_keys 'C-L'
|
||||
lines[-1].end_with?('/tmp/fzf\ test/foobar')
|
||||
end
|
||||
tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab
|
||||
tmux.until { |lines| lines.item_count > 0 }
|
||||
tmux.send_keys 'foobar$'
|
||||
tmux.until { |lines| lines.match_count == 1 }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) { |lines| lines[-1].end_with?('/tmp/fzf\ test/foobar') }
|
||||
|
||||
# Should include hidden files
|
||||
(1..100).each { |i| FileUtils.touch "/tmp/fzf-test/.hidden-#{i}" }
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab, pane: 0
|
||||
tmux.until(1) do |lines|
|
||||
tmux.send_keys 'C-L'
|
||||
lines[-2].include?('100/') &&
|
||||
lines[-3].include?('/tmp/fzf-test/.hidden-')
|
||||
end
|
||||
tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab
|
||||
tmux.until(true) { |lines| lines.match_count == 100 && lines.any_include?('/tmp/fzf-test/.hidden-') }
|
||||
tmux.send_keys :Enter
|
||||
ensure
|
||||
['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f|
|
||||
@@ -1391,24 +1488,24 @@ module CompletionTest
|
||||
end
|
||||
|
||||
def test_file_completion_root
|
||||
tmux.send_keys 'ls /**', :Tab, pane: 0
|
||||
tmux.until(1) { |lines| lines.item_count > 0 }
|
||||
tmux.send_keys 'ls /**', :Tab
|
||||
tmux.until { |lines| lines.item_count > 0 }
|
||||
tmux.send_keys :Enter
|
||||
end
|
||||
|
||||
def test_dir_completion
|
||||
tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab, pane: 0
|
||||
tmux.until(1) { |lines| lines.item_count > 0 }
|
||||
tmux.send_keys :BTab, :BTab # BTab does not work here
|
||||
tmux.send_keys 55
|
||||
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until do |lines|
|
||||
tmux.send_keys 'C-L'
|
||||
lines[-1] == 'cd /tmp/fzf-test/d55/'
|
||||
(1..100).each do |idx|
|
||||
FileUtils.mkdir_p "/tmp/fzf-test/d#{idx}"
|
||||
end
|
||||
FileUtils.touch '/tmp/fzf-test/d55/xxx'
|
||||
tmux.prepare
|
||||
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab
|
||||
tmux.until { |lines| lines.item_count > 0 }
|
||||
tmux.send_keys :Tab, :Tab # Tab does not work here
|
||||
tmux.send_keys 55
|
||||
tmux.until { |lines| lines.match_count == 1 }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/' }
|
||||
tmux.send_keys :xx
|
||||
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' }
|
||||
|
||||
@@ -1430,15 +1527,13 @@ module CompletionTest
|
||||
lines = tmux.until { |lines| lines[-1].start_with? '[1]' }
|
||||
pid = lines[-1].split.last
|
||||
tmux.prepare
|
||||
tmux.send_keys 'kill ', :Tab, pane: 0
|
||||
tmux.until(1) { |lines| lines.item_count > 0 }
|
||||
tmux.send_keys 'C-L'
|
||||
tmux.send_keys 'kill ', :Tab
|
||||
tmux.until { |lines| lines.item_count > 0 }
|
||||
tmux.send_keys 'sleep12345'
|
||||
tmux.until(1) { |lines| lines[-3].include? 'sleep 12345' }
|
||||
tmux.until { |lines| lines.any_include? 'sleep 12345' }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until do |lines|
|
||||
tmux.send_keys 'C-L'
|
||||
lines[-1] == "kill #{pid}"
|
||||
end
|
||||
tmux.until(true) { |lines| lines[-1].include? "kill #{pid}" }
|
||||
ensure
|
||||
Process.kill 'KILL', pid.to_i rescue nil if pid
|
||||
end
|
||||
@@ -1446,58 +1541,55 @@ module CompletionTest
|
||||
def test_custom_completion
|
||||
tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'ls /tmp/**', :Tab, pane: 0
|
||||
tmux.until(1) { |lines| lines.item_count == 11 }
|
||||
tmux.send_keys :BTab, :BTab, :BTab
|
||||
tmux.until(1) { |lines| lines[-2].include? '(3)' }
|
||||
tmux.send_keys 'ls /tmp/**', :Tab
|
||||
tmux.until { |lines| lines.item_count == 11 }
|
||||
tmux.send_keys :Tab, :Tab, :Tab
|
||||
tmux.until { |lines| lines.select_count == 3 }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until do |lines|
|
||||
tmux.send_keys 'C-L'
|
||||
lines[-1] == "ls /tmp 1 2"
|
||||
end
|
||||
tmux.until(true) { |lines| lines[-1] == "ls /tmp 1 2" }
|
||||
end
|
||||
|
||||
def test_unset_completion
|
||||
tmux.send_keys 'export FOO=BAR', :Enter
|
||||
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
|
||||
tmux.prepare
|
||||
|
||||
# Using tmux
|
||||
tmux.send_keys 'unset FOO**', :Tab, pane: 0
|
||||
tmux.until(1) { |lines| lines[-2].include? ' 1/' }
|
||||
tmux.send_keys 'unset FZFFOOBR**', :Tab
|
||||
tmux.until { |lines| lines.match_count == 1 }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| lines[-1] == 'unset FOO' }
|
||||
tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' }
|
||||
tmux.send_keys 'C-c'
|
||||
|
||||
# FZF_TMUX=0
|
||||
# FZF_TMUX=1
|
||||
new_shell
|
||||
tmux.send_keys 'unset FOO**', :Tab
|
||||
tmux.until { |lines| lines[-2].include? ' 1/' }
|
||||
tmux.send_keys 'unset FZFFOOBR**', :Tab, pane: 0
|
||||
tmux.until(false, 1) { |lines| lines.match_count == 1 }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| lines[-1] == 'unset FOO' }
|
||||
tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' }
|
||||
end
|
||||
|
||||
def test_file_completion_unicode
|
||||
FileUtils.mkdir_p '/tmp/fzf-test'
|
||||
tmux.paste 'cd /tmp/fzf-test; echo -n test3 > "fzf-unicode 테스트1"; echo -n test4 > "fzf-unicode 테스트2"'
|
||||
tmux.prepare
|
||||
tmux.send_keys 'cat fzf-unicode**', :Tab, pane: 0
|
||||
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' }
|
||||
tmux.send_keys 'cat fzf-unicode**', :Tab
|
||||
tmux.until { |lines| lines.match_count == 2 }
|
||||
|
||||
tmux.send_keys '1', pane: 1
|
||||
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
|
||||
tmux.send_keys :BTab, pane: 1
|
||||
tmux.until(1) { |lines| lines[-2].include? '(1)' }
|
||||
tmux.send_keys '1'
|
||||
tmux.until { |lines| lines.match_count == 1 }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| lines.select_count == 1 }
|
||||
|
||||
tmux.send_keys :BSpace, pane: 1
|
||||
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| lines.match_count == 2 }
|
||||
|
||||
tmux.send_keys '2', pane: 1
|
||||
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
|
||||
tmux.send_keys :BTab, pane: 1
|
||||
tmux.until(1) { |lines| lines[-2].include? '(2)' }
|
||||
tmux.send_keys '2'
|
||||
tmux.until { |lines| lines.select_count == 1 }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| lines.select_count == 2 }
|
||||
|
||||
tmux.send_keys :Enter, pane: 1
|
||||
tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) { |lines| lines.any_include? 'cat' }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| lines[-1].include? 'test3test4' }
|
||||
end
|
||||
@@ -1509,7 +1601,7 @@ class TestBash < TestBase
|
||||
|
||||
def new_shell
|
||||
tmux.prepare
|
||||
tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter
|
||||
tmux.send_keys "FZF_TMUX=1 #{Shell.bash}", :Enter
|
||||
tmux.prepare
|
||||
end
|
||||
|
||||
@@ -1524,7 +1616,7 @@ class TestZsh < TestBase
|
||||
include CompletionTest
|
||||
|
||||
def new_shell
|
||||
tmux.send_keys "FZF_TMUX=0 #{Shell.zsh}", :Enter
|
||||
tmux.send_keys "FZF_TMUX=1 #{Shell.zsh}", :Enter
|
||||
tmux.prepare
|
||||
end
|
||||
|
||||
@@ -1538,7 +1630,7 @@ class TestFish < TestBase
|
||||
include TestShell
|
||||
|
||||
def new_shell
|
||||
tmux.send_keys 'env FZF_TMUX=0 fish', :Enter
|
||||
tmux.send_keys 'env FZF_TMUX=1 fish', :Enter
|
||||
tmux.send_keys 'function fish_prompt; end; clear', :Enter
|
||||
tmux.until { |lines| lines.empty? }
|
||||
end
|
||||
|
Reference in New Issue
Block a user