mirror of
https://github.com/junegunn/fzf.git
synced 2025-08-15 04:05:48 -07:00
Compare commits
88 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2023012408 | ||
|
95a7661bb1 | ||
|
618d317803 | ||
|
ae897c8cdb | ||
|
d0a0f3c052 | ||
|
91b9591b10 | ||
|
aa7361337d | ||
|
284d77fe2e | ||
|
826178f1e2 | ||
|
acccf8a9b8 | ||
|
57c066f0be | ||
|
e44f64ae92 | ||
|
d51980a3f5 | ||
|
c3d73e7ecb | ||
|
b077f6821d | ||
|
a79de11af7 | ||
|
2023011763 | ||
|
b46e40e86b | ||
|
a6d6cdd165 | ||
|
dc8da605f9 | ||
|
8b299a29c7 | ||
|
3109b865d2 | ||
|
0c5956c43c | ||
|
1c83b39691 | ||
|
77874b473c | ||
|
b7cce7be15 | ||
|
3cd3362417 | ||
|
e97e925efb | ||
|
0f032235cf | ||
|
e0f0984da7 | ||
|
4d22b5aaef | ||
|
80b8846318 | ||
|
bf641faafa | ||
|
23d8b78ce1 | ||
|
3b2244077d | ||
|
ee5cdb9713 | ||
|
03d02d67f7 | ||
|
5798145581 | ||
|
51ef0b7f66 | ||
|
97b4542c73 | ||
|
c1cd0c09a2 | ||
|
1fc1f47d80 | ||
|
ec471a5bc2 | ||
|
a893fc0ca2 | ||
|
3761dc0433 | ||
|
aa71a07fbe | ||
|
088293f5e7 | ||
|
7c660aa86e | ||
|
435d8fa0a2 | ||
|
5cd6f1d064 | ||
|
ec20dfe312 | ||
|
924ffb5a35 | ||
|
62c7f59b94 | ||
|
e97176b1d7 | ||
|
d649f5d826 | ||
|
6c37177cf5 | ||
|
14775aa975 | ||
|
44b6336372 | ||
|
36d2bb332b | ||
|
4dbe45640a | ||
|
4b3f0b9f08 | ||
|
12af069dca | ||
|
d42e708d31 | ||
|
b7bb973118 | ||
|
750b2a6313 | ||
|
de0da86bd7 | ||
|
8e283f512a | ||
|
73162a4bc3 | ||
|
1a9761736e | ||
|
fd1f7665a7 | ||
|
6d14573fd0 | ||
|
cf69b836ac | ||
|
a7a771b92b | ||
|
def011c029 | ||
|
4b055bf260 | ||
|
1ba7484d60 | ||
|
51c518da1e | ||
|
a3b6b03dfb | ||
|
18e3b38c69 | ||
|
0ad30063ff | ||
|
7812c64a31 | ||
|
3d2376ab52 | ||
|
6b207bbf2b | ||
|
3f079ba7c6 | ||
|
8f4c89f50e | ||
|
6b7a543c82 | ||
|
2ba68d24f2 | ||
|
46877e0a92 |
4
.github/workflows/linux.yml
vendored
4
.github/workflows/linux.yml
vendored
@@ -27,13 +27,13 @@ jobs:
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.0.0
|
||||
ruby-version: 3.1.0
|
||||
|
||||
- name: Install packages
|
||||
run: sudo apt-get install --yes zsh fish tmux
|
||||
|
||||
- name: Install Ruby gems
|
||||
run: sudo gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
|
||||
run: sudo gem install --no-document minitest:5.17.0 rubocop:1.43.0 rubocop-minitest:0.25.1 rubocop-performance:1.15.2
|
||||
|
||||
- name: Rubocop
|
||||
run: rubocop --require rubocop-minitest --require rubocop-performance
|
||||
|
@@ -73,6 +73,7 @@ builds:
|
||||
- arm
|
||||
- arm64
|
||||
- loong64
|
||||
- ppc64le
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
|
@@ -28,3 +28,5 @@ Style/WordArray:
|
||||
MinSize: 1
|
||||
Minitest/AssertEqual:
|
||||
Enabled: false
|
||||
Naming/VariableNumber:
|
||||
Enabled: false
|
||||
|
@@ -15,7 +15,7 @@ Advanced fzf examples
|
||||
* [Toggling between data sources](#toggling-between-data-sources)
|
||||
* [Ripgrep integration](#ripgrep-integration)
|
||||
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
|
||||
* [Using fzf as interative Ripgrep launcher](#using-fzf-as-interative-ripgrep-launcher)
|
||||
* [Using fzf as interactive Ripgrep launcher](#using-fzf-as-interactive-ripgrep-launcher)
|
||||
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
|
||||
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
|
||||
* [Log tailing](#log-tailing)
|
||||
@@ -310,7 +310,7 @@ I know it's a lot to digest, let's try to break down the code.
|
||||
- Once we selected a line, we open the file with `vim` (`vim
|
||||
"${selected[0]}"`) and move the cursor to the line (`+${selected[1]}`).
|
||||
|
||||
### Using fzf as interative Ripgrep launcher
|
||||
### Using fzf as interactive Ripgrep launcher
|
||||
|
||||
We have learned that we can bind `reload` action to a key (e.g.
|
||||
`--bind=ctrl-r:execute(ps -ef)`). In the next example, we are going to **bind
|
||||
|
150
CHANGELOG.md
150
CHANGELOG.md
@@ -1,6 +1,156 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.37.0
|
||||
------
|
||||
- Added a way to customize the separator of inline info
|
||||
```sh
|
||||
fzf --info 'inline: ╱ ' --prompt '╱ ' --color prompt:bright-yellow
|
||||
```
|
||||
- New event
|
||||
- `focus` - Triggered when the focus changes due to a vertical cursor
|
||||
movement or a search result update
|
||||
```sh
|
||||
fzf --bind 'focus:transform-preview-label:echo [ {} ]' --preview 'cat {}'
|
||||
|
||||
# Any action bound to the event runs synchronously and thus can make the interface sluggish
|
||||
# e.g. lolcat isn't one of the fastest programs, and every cursor movement in
|
||||
# fzf will be noticeably affected by its execution time
|
||||
fzf --bind 'focus:transform-preview-label:echo [ {} ] | lolcat -f' --preview 'cat {}'
|
||||
|
||||
# Beware not to introduce an infinite loop
|
||||
seq 10 | fzf --bind 'focus:up' --cycle
|
||||
```
|
||||
- New actions
|
||||
- `change-border-label`
|
||||
- `change-preview-label`
|
||||
- `transform-border-label`
|
||||
- `transform-preview-label`
|
||||
- Bug fixes and improvements
|
||||
|
||||
0.36.0
|
||||
------
|
||||
- Added `--listen=HTTP_PORT` option to start HTTP server. It allows external
|
||||
processes to send actions to perform via POST method.
|
||||
```sh
|
||||
# Start HTTP server on port 6266
|
||||
fzf --listen 6266
|
||||
|
||||
# Send actions to the server
|
||||
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
|
||||
```
|
||||
- Added draggable scrollbar to the main search window and the preview window
|
||||
```sh
|
||||
# Hide scrollbar
|
||||
fzf --no-scrollbar
|
||||
|
||||
# Customize scrollbar
|
||||
fzf --scrollbar ┆ --color scrollbar:blue
|
||||
```
|
||||
- New event
|
||||
- Added `load` event that is triggered when the input stream is complete
|
||||
and the initial processing of the list is complete.
|
||||
```sh
|
||||
# Change the prompt to "loaded" when the input stream is complete
|
||||
(seq 10; sleep 1; seq 11 20) | fzf --prompt 'Loading> ' --bind 'load:change-prompt:Loaded> '
|
||||
|
||||
# You can use it instead of 'start' event without `--sync` if asynchronous
|
||||
# trigger is not an issue.
|
||||
(seq 10; sleep 1; seq 11 20) | fzf --bind 'load:last'
|
||||
```
|
||||
- New actions
|
||||
- Added `pos(...)` action to move the cursor to the numeric position
|
||||
- `first` and `last` are equivalent to `pos(1)` and `pos(-1)` respectively
|
||||
```sh
|
||||
# Put the cursor on the 10th item
|
||||
seq 100 | fzf --sync --bind 'start:pos(10)'
|
||||
|
||||
# Put the cursor on the 10th to last item
|
||||
seq 100 | fzf --sync --bind 'start:pos(-10)'
|
||||
```
|
||||
- Added `reload-sync(...)` action which replaces the current list only after
|
||||
the reload process is complete. This is useful when the command takes
|
||||
a while to produce the initial output and you don't want fzf to run against
|
||||
an empty list while the command is running.
|
||||
```sh
|
||||
# You can still filter and select entries from the initial list for 3 seconds
|
||||
seq 100 | fzf --bind 'load:reload-sync(sleep 3; seq 1000)+unbind(load)'
|
||||
```
|
||||
- Added `next-selected` and `prev-selected` actions to move between selected
|
||||
items
|
||||
```sh
|
||||
# `next-selected` will move the pointer to the next selected item below the current line
|
||||
# `prev-selected` will move the pointer to the previous selected item above the current line
|
||||
seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected
|
||||
|
||||
# Both actions respect --layout option
|
||||
seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected --layout reverse
|
||||
```
|
||||
- Added `change-query(...)` action that simply changes the query string to the
|
||||
given static string. This can be useful when used with `--listen`.
|
||||
```sh
|
||||
curl localhost:6266 -d "change-query:$(date)"
|
||||
```
|
||||
- Added `transform-prompt(...)` action for transforming the prompt string
|
||||
using an external command
|
||||
```sh
|
||||
# Press space to change the prompt string using an external command
|
||||
# (only the first line of the output is taken)
|
||||
fzf --bind 'space:reload(ls),load:transform-prompt(printf "%s> " "$(date)")'
|
||||
```
|
||||
- Added `transform-query(...)` action for transforming the query string using
|
||||
an external command
|
||||
```sh
|
||||
# Press space to convert the query to uppercase letters
|
||||
fzf --bind 'space:transform-query(tr "[:lower:]" "[:upper:]" <<< {q})'
|
||||
|
||||
# Bind it to 'change' event for automatic conversion
|
||||
fzf --bind 'change:transform-query(tr "[:lower:]" "[:upper:]" <<< {q})'
|
||||
|
||||
# Can only type numbers
|
||||
fzf --bind 'change:transform-query(sed "s/[^0-9]//g" <<< {q})'
|
||||
```
|
||||
- `put` action can optionally take an argument string
|
||||
```sh
|
||||
# a will put 'alpha' on the prompt, ctrl-b will put 'bravo'
|
||||
fzf --bind 'a:put+put(lpha),ctrl-b:put(bravo)'
|
||||
```
|
||||
- Added color name `preview-label` for `--preview-label` (defaults to `label`
|
||||
for `--border-label`)
|
||||
- Better support for (Windows) terminals where each box-drawing character
|
||||
takes 2 columns. Set `RUNEWIDTH_EASTASIAN` environment variable to `1`.
|
||||
- On Vim, the variable will be automatically set if `&ambiwidth` is `double`
|
||||
- Behavior changes
|
||||
- fzf will always execute the preview command if the command template
|
||||
contains `{q}` even when it's empty. If you prefer the old behavior,
|
||||
you'll have to check if `{q}` is empty in your command.
|
||||
```sh
|
||||
# This will show // even when the query is empty
|
||||
: | fzf --preview 'echo /{q}/'
|
||||
|
||||
# But if you don't want it,
|
||||
: | fzf --preview '[ -n {q} ] || exit; echo /{q}/'
|
||||
```
|
||||
- `double-click` will behave the same as `enter` unless otherwise specified,
|
||||
so you don't have to repeat the same action twice in `--bind` in most cases.
|
||||
```sh
|
||||
# No need to bind 'double-click' to the same action
|
||||
fzf --bind 'enter:execute:less {}' # --bind 'double-click:execute:less {}'
|
||||
```
|
||||
- If the color for `separator` is not specified, it will default to the
|
||||
color for `border`. Same holds true for `scrollbar`. This is to reduce
|
||||
the number of configuration items required to achieve a consistent color
|
||||
scheme.
|
||||
- If `follow` flag is specified in `--preview-window` option, fzf will
|
||||
automatically scroll to the bottom of the streaming preview output. But
|
||||
when the user manually scrolls the window, the following stops. With
|
||||
this version, fzf will resume following if the user scrolls the window
|
||||
to the bottom.
|
||||
- Default border style on Windows is changed to `sharp` because some
|
||||
Windows terminals are not capable of displaying `rounded` border
|
||||
characters correctly.
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
0.35.1
|
||||
------
|
||||
- Fixed a bug where fzf with `--tiebreak=chunk` crashes on inverse match query
|
||||
|
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2022 Junegunn Choi
|
||||
Copyright (c) 2013-2023 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
|
||||
|
@@ -12,6 +12,9 @@ differ depending on the package manager.
|
||||
" If installed using Homebrew
|
||||
set rtp+=/usr/local/opt/fzf
|
||||
|
||||
" If installed using Homebrew on Apple Silicon
|
||||
set rtp+=/opt/homebrew/opt/fzf
|
||||
|
||||
" If installed using git
|
||||
set rtp+=~/.fzf
|
||||
```
|
||||
@@ -309,7 +312,7 @@ following options are allowed:
|
||||
- `yoffset` [float default 0.5 range [0 ~ 1]]
|
||||
- `xoffset` [float default 0.5 range [0 ~ 1]]
|
||||
- `relative` [boolean default v:false]
|
||||
- `border` [string default `rounded`]: Border style
|
||||
- `border` [string default `rounded` (`sharp` on Windows)]: Border style
|
||||
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
|
||||
|
||||
`fzf#wrap`
|
||||
@@ -483,4 +486,4 @@ autocmd FileType fzf set laststatus=0 noshowmode noruler
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2021 Junegunn Choi
|
||||
Copyright (c) 2013-2023 Junegunn Choi
|
||||
|
114
README.md
114
README.md
@@ -25,11 +25,11 @@ Table of Contents
|
||||
<!-- vim-markdown-toc GFM -->
|
||||
|
||||
* [Installation](#installation)
|
||||
* [Using Homebrew](#using-homebrew)
|
||||
* [Using git](#using-git)
|
||||
* [Using Linux package managers](#using-linux-package-managers)
|
||||
* [Windows](#windows)
|
||||
* [As Vim plugin](#as-vim-plugin)
|
||||
* [Using Homebrew](#using-homebrew)
|
||||
* [Using git](#using-git)
|
||||
* [Using Linux package managers](#using-linux-package-managers)
|
||||
* [Windows](#windows)
|
||||
* [As Vim plugin](#as-vim-plugin)
|
||||
* [Upgrading fzf](#upgrading-fzf)
|
||||
* [Building fzf](#building-fzf)
|
||||
* [Usage](#usage)
|
||||
@@ -52,16 +52,16 @@ Table of Contents
|
||||
* [Custom fuzzy completion](#custom-fuzzy-completion)
|
||||
* [Vim plugin](#vim-plugin)
|
||||
* [Advanced topics](#advanced-topics)
|
||||
* [Performance](#performance)
|
||||
* [Executing external programs](#executing-external-programs)
|
||||
* [Reloading the candidate list](#reloading-the-candidate-list)
|
||||
* [1. Update the list of processes by pressing CTRL-R](#1-update-the-list-of-processes-by-pressing-ctrl-r)
|
||||
* [2. Switch between sources by pressing CTRL-D or CTRL-F](#2-switch-between-sources-by-pressing-ctrl-d-or-ctrl-f)
|
||||
* [3. Interactive ripgrep integration](#3-interactive-ripgrep-integration)
|
||||
* [Preview window](#preview-window)
|
||||
* [Performance](#performance)
|
||||
* [Executing external programs](#executing-external-programs)
|
||||
* [Reloading the candidate list](#reloading-the-candidate-list)
|
||||
* [1. Update the list of processes by pressing CTRL-R](#1-update-the-list-of-processes-by-pressing-ctrl-r)
|
||||
* [2. Switch between sources by pressing CTRL-D or CTRL-F](#2-switch-between-sources-by-pressing-ctrl-d-or-ctrl-f)
|
||||
* [3. Interactive ripgrep integration](#3-interactive-ripgrep-integration)
|
||||
* [Preview window](#preview-window)
|
||||
* [Tips](#tips)
|
||||
* [Respecting `.gitignore`](#respecting-gitignore)
|
||||
* [Fish shell](#fish-shell)
|
||||
* [Respecting `.gitignore`](#respecting-gitignore)
|
||||
* [Fish shell](#fish-shell)
|
||||
* [Related projects](#related-projects)
|
||||
* [License](#license)
|
||||
|
||||
@@ -115,7 +115,7 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
|
||||
| Package Manager | Linux Distribution | Command |
|
||||
| --- | --- | --- |
|
||||
| APK | Alpine Linux | `sudo apk add fzf` |
|
||||
| APT | Debian 9+/Ubuntu 19.10+ | `sudo apt-get install fzf` |
|
||||
| APT | Debian 9+/Ubuntu 19.10+ | `sudo apt install fzf` |
|
||||
| Conda | | `conda install -c conda-forge fzf` |
|
||||
| DNF | Fedora | `sudo dnf install fzf` |
|
||||
| Nix | NixOS, etc. | `nix-env -iA nixpkgs.fzf` |
|
||||
@@ -129,7 +129,7 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
|
||||
> :warning: **Key bindings (CTRL-T / CTRL-R / ALT-C) and fuzzy auto-completion
|
||||
> may not be enabled by default.**
|
||||
>
|
||||
> Refer to the package documentation for more information. (e.g. `apt-cache show fzf`)
|
||||
> Refer to the package documentation for more information. (e.g. `apt show fzf`)
|
||||
|
||||
[](https://repology.org/project/fzf/versions)
|
||||
|
||||
@@ -202,7 +202,7 @@ files excluding hidden ones. (You can override the default command with
|
||||
vim $(fzf)
|
||||
```
|
||||
|
||||
#### Using the finder
|
||||
### Using the finder
|
||||
|
||||
- `CTRL-K` / `CTRL-J` (or `CTRL-P` / `CTRL-N`) to move cursor up and down
|
||||
- `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit
|
||||
@@ -211,7 +211,7 @@ vim $(fzf)
|
||||
- Mouse: scroll, click, double-click; shift-click and shift-scroll on
|
||||
multi-select mode
|
||||
|
||||
#### Layout
|
||||
### Layout
|
||||
|
||||
fzf by default starts in fullscreen mode, but you can make it start below the
|
||||
cursor with `--height` option.
|
||||
@@ -234,7 +234,7 @@ default. For example,
|
||||
export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border'
|
||||
```
|
||||
|
||||
#### Search syntax
|
||||
### Search syntax
|
||||
|
||||
Unless otherwise specified, fzf starts in "extended-search mode" where you can
|
||||
type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt
|
||||
@@ -262,7 +262,7 @@ or `py`.
|
||||
^core go$ | rb$ | py$
|
||||
```
|
||||
|
||||
#### Environment variables
|
||||
### Environment variables
|
||||
|
||||
- `FZF_DEFAULT_COMMAND`
|
||||
- Default command to use when input is tty
|
||||
@@ -278,11 +278,11 @@ or `py`.
|
||||
- Default options
|
||||
- e.g. `export FZF_DEFAULT_OPTS="--layout=reverse --inline-info"`
|
||||
|
||||
#### Options
|
||||
### Options
|
||||
|
||||
See the man page (`man fzf`) for the full list of options.
|
||||
|
||||
#### Demo
|
||||
### Demo
|
||||
If you learn by watching videos, check out this screencast by [@samoshkin](https://github.com/samoshkin) to explore `fzf` features.
|
||||
|
||||
<a title="fzf - command-line fuzzy finder" href="https://www.youtube.com/watch?v=qgG5Jhi_Els">
|
||||
@@ -333,17 +333,37 @@ fish.
|
||||
|
||||
- `CTRL-T` - Paste the selected files and directories onto the command-line
|
||||
- Set `FZF_CTRL_T_COMMAND` to override the default command
|
||||
- Set `FZF_CTRL_T_OPTS` to pass additional options
|
||||
- Set `FZF_CTRL_T_OPTS` to pass additional options to fzf
|
||||
```sh
|
||||
# Preview file content using bat (https://github.com/sharkdp/bat)
|
||||
export FZF_CTRL_T_OPTS="
|
||||
--preview 'bat -n --color=always {}'
|
||||
--bind 'ctrl-/:change-preview-window(down|hidden|)'"
|
||||
```
|
||||
- `CTRL-R` - Paste the selected command from history onto the command-line
|
||||
- If you want to see the commands in chronological order, press `CTRL-R`
|
||||
again which toggles sorting by relevance
|
||||
- Set `FZF_CTRL_R_OPTS` to pass additional options
|
||||
- Set `FZF_CTRL_R_OPTS` to pass additional options to fzf
|
||||
```sh
|
||||
# CTRL-/ to toggle small preview window to see the full command
|
||||
# CTRL-Y to copy the command into clipboard using pbcopy
|
||||
export FZF_CTRL_R_OPTS="
|
||||
--preview 'echo {}' --preview-window up:3:hidden:wrap
|
||||
--bind 'ctrl-/:toggle-preview'
|
||||
--bind 'ctrl-y:execute-silent(echo -n {2..} | pbcopy)+abort'
|
||||
--color header:italic
|
||||
--header 'Press CTRL-Y to copy command into clipboard'"
|
||||
```
|
||||
- `ALT-C` - cd into the selected directory
|
||||
- Set `FZF_ALT_C_COMMAND` to override the default command
|
||||
- Set `FZF_ALT_C_OPTS` to pass additional options
|
||||
- Set `FZF_ALT_C_OPTS` to pass additional options to fzf
|
||||
```sh
|
||||
# Print tree structure in the preview window
|
||||
export FZF_ALT_C_OPTS="--preview 'tree -C {}'"
|
||||
```
|
||||
|
||||
If you're on a tmux session, you can start fzf in a tmux split-pane or in
|
||||
a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `-d 40%`).
|
||||
a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `export FZF_TMUX_OPTS='-p80%,60%'`).
|
||||
See `fzf-tmux --help` for available options.
|
||||
|
||||
More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Configuring-shell-key-bindings).
|
||||
@@ -351,7 +371,7 @@ More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/C
|
||||
Fuzzy completion for bash and zsh
|
||||
---------------------------------
|
||||
|
||||
#### Files and directories
|
||||
### Files and directories
|
||||
|
||||
Fuzzy completion for files and directories can be triggered if the word before
|
||||
the cursor ends with the trigger sequence, which is by default `**`.
|
||||
@@ -380,7 +400,7 @@ cd **<TAB>
|
||||
cd ~/github/fzf**<TAB>
|
||||
```
|
||||
|
||||
#### Process IDs
|
||||
### Process IDs
|
||||
|
||||
Fuzzy completion for PIDs is provided for kill command.
|
||||
|
||||
@@ -389,7 +409,7 @@ Fuzzy completion for PIDs is provided for kill command.
|
||||
kill -9 **<TAB>
|
||||
```
|
||||
|
||||
#### Host names
|
||||
### Host names
|
||||
|
||||
For ssh and telnet commands, fuzzy completion for hostnames is provided. The
|
||||
names are extracted from /etc/hosts and ~/.ssh/config.
|
||||
@@ -399,7 +419,7 @@ ssh **<TAB>
|
||||
telnet **<TAB>
|
||||
```
|
||||
|
||||
#### Environment variables / Aliases
|
||||
### Environment variables / Aliases
|
||||
|
||||
```sh
|
||||
unset **<TAB>
|
||||
@@ -407,7 +427,7 @@ export **<TAB>
|
||||
unalias **<TAB>
|
||||
```
|
||||
|
||||
#### Settings
|
||||
### Settings
|
||||
|
||||
```sh
|
||||
# Use ~~ as the trigger sequence instead of the default **
|
||||
@@ -429,7 +449,7 @@ _fzf_compgen_dir() {
|
||||
fd --type d --hidden --follow --exclude ".git" . "$1"
|
||||
}
|
||||
|
||||
# (EXPERIMENTAL) Advanced customization of fzf options via _fzf_comprun function
|
||||
# Advanced customization of fzf options via _fzf_comprun function
|
||||
# - The first argument to the function is the name of the command.
|
||||
# - You should make sure to pass the rest of the arguments to fzf.
|
||||
_fzf_comprun() {
|
||||
@@ -437,15 +457,15 @@ _fzf_comprun() {
|
||||
shift
|
||||
|
||||
case "$command" in
|
||||
cd) fzf "$@" --preview 'tree -C {} | head -200' ;;
|
||||
export|unset) fzf "$@" --preview "eval 'echo \$'{}" ;;
|
||||
ssh) fzf "$@" --preview 'dig {}' ;;
|
||||
*) fzf "$@" ;;
|
||||
cd) fzf --preview 'tree -C {} | head -200' "$@" ;;
|
||||
export|unset) fzf --preview "eval 'echo \$'{}" "$@" ;;
|
||||
ssh) fzf --preview 'dig {}' "$@" ;;
|
||||
*) fzf --preview 'bat -n --color=always {}' "$@" ;;
|
||||
esac
|
||||
}
|
||||
```
|
||||
|
||||
#### Supported commands
|
||||
### Supported commands
|
||||
|
||||
On bash, fuzzy completion is enabled only for a predefined set of commands
|
||||
(`complete | grep _fzf` to see the list). But you can enable it for other
|
||||
@@ -457,7 +477,7 @@ _fzf_setup_completion path ag git kubectl
|
||||
_fzf_setup_completion dir tree
|
||||
```
|
||||
|
||||
#### Custom fuzzy completion
|
||||
### Custom fuzzy completion
|
||||
|
||||
_**(Custom completion API is experimental and subject to change)**_
|
||||
|
||||
@@ -517,9 +537,8 @@ Advanced topics
|
||||
|
||||
### Performance
|
||||
|
||||
fzf is fast and is [getting even faster][perf]. Performance should not be
|
||||
a problem in most use cases. However, you might want to be aware of the
|
||||
options that affect performance.
|
||||
fzf is fast. Performance should not be a problem in most use cases. However,
|
||||
you might want to be aware of the options that can affect performance.
|
||||
|
||||
- `--ansi` tells fzf to extract and parse ANSI color codes in the input, and it
|
||||
makes the initial scanning slower. So it's not recommended that you add it
|
||||
@@ -527,12 +546,6 @@ options that affect performance.
|
||||
- `--nth` makes fzf slower because it has to tokenize each line.
|
||||
- `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each
|
||||
line.
|
||||
- If you absolutely need better performance, you can consider using
|
||||
`--algo=v1` (the default being `v2`) to make fzf use a faster greedy
|
||||
algorithm. However, this algorithm is not guaranteed to find the optimal
|
||||
ordering of the matches and is not recommended.
|
||||
|
||||
[perf]: https://junegunn.kr/images/fzf-0.17.0.png
|
||||
|
||||
### Executing external programs
|
||||
|
||||
@@ -591,7 +604,7 @@ If ripgrep doesn't find any matches, it will exit with a non-zero exit status,
|
||||
and fzf will warn you about it. To suppress the warning message, we added
|
||||
`|| true` to the command, so that it always exits with 0.
|
||||
|
||||
See ["Using fzf as interative Ripgrep launcher"](https://github.com/junegunn/fzf/blob/master/ADVANCED.md#using-fzf-as-interative-ripgrep-launcher)
|
||||
See ["Using fzf as interactive Ripgrep launcher"](https://github.com/junegunn/fzf/blob/master/ADVANCED.md#using-fzf-as-interactive-ripgrep-launcher)
|
||||
for a fuller example with preview window options.
|
||||
|
||||
### Preview window
|
||||
@@ -612,7 +625,7 @@ syntax-highlights the content of a file, such as
|
||||
[Highlight](http://www.andre-simon.de/doku/highlight/en/highlight.php):
|
||||
|
||||
```bash
|
||||
fzf --preview 'bat --style=numbers --color=always --line-range :500 {}'
|
||||
fzf --preview 'bat --color=always {}' --preview-window '~3'
|
||||
```
|
||||
|
||||
You can customize the size, position, and border of the preview window using
|
||||
@@ -622,6 +635,7 @@ You can customize the size, position, and border of the preview window using
|
||||
```bash
|
||||
fzf --height 40% --layout reverse --info inline --border \
|
||||
--preview 'file {}' --preview-window up,1,border-horizontal \
|
||||
--bind 'ctrl-/:change-preview-window(50%|hidden|)' \
|
||||
--color 'fg:#bbccdd,fg+:#ddeeff,bg:#334455,preview-bg:#223344,border:#778899'
|
||||
```
|
||||
|
||||
@@ -708,4 +722,4 @@ https://github.com/junegunn/fzf/wiki/Related-projects
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2021 Junegunn Choi
|
||||
Copyright (c) 2013-2023 Junegunn Choi
|
||||
|
@@ -325,7 +325,7 @@ following options are allowed:
|
||||
- `yoffset` [float default 0.5 range [0 ~ 1]]
|
||||
- `xoffset` [float default 0.5 range [0 ~ 1]]
|
||||
- `relative` [boolean default v:false]
|
||||
- `border` [string default `rounded`]: Border style
|
||||
- `border` [string default `rounded` (`sharp` on Windows)]: Border style
|
||||
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
|
||||
|
||||
|
||||
@@ -506,7 +506,7 @@ LICENSE *fzf-license*
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2021 Junegunn Choi
|
||||
Copyright (c) 2013-2023 Junegunn Choi
|
||||
|
||||
==============================================================================
|
||||
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:
|
||||
|
8
go.mod
8
go.mod
@@ -1,8 +1,8 @@
|
||||
module github.com/junegunn/fzf
|
||||
|
||||
require (
|
||||
github.com/gdamore/tcell/v2 v2.5.3
|
||||
github.com/mattn/go-isatty v0.0.16
|
||||
github.com/gdamore/tcell/v2 v2.5.4
|
||||
github.com/mattn/go-isatty v0.0.17
|
||||
github.com/mattn/go-runewidth v0.0.14
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/rivo/uniseg v0.4.2
|
||||
@@ -14,8 +14,8 @@ require (
|
||||
require (
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
)
|
||||
|
||||
go 1.17
|
||||
|
33
go.sum
33
go.sum
@@ -1,12 +1,11 @@
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.5.3 h1:b9XQrT6QGbgI7JvZOJXFNczOQeIYbo8BfeSMzt2sAV0=
|
||||
github.com/gdamore/tcell/v2 v2.5.3/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
|
||||
github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
|
||||
github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||
@@ -16,17 +15,33 @@ github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
|
||||
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/saracen/walker v0.1.3 h1:YtcKKmpRPy6XJTHJ75J2QYXXZYWnZNQxPCVqZSHVV/g=
|
||||
github.com/saracen/walker v0.1.3/go.mod h1:FU+7qU8DeQQgSZDmmThMJi93kPkLFgy0oVAcLxurjIk=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
3
install
3
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.35.1
|
||||
version=0.37.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
@@ -176,6 +176,7 @@ case "$archi" in
|
||||
Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||
Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||
Linux\ loongarch64) download fzf-$version-linux_loong64.tar.gz ;;
|
||||
Linux\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;;
|
||||
Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;;
|
||||
FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;;
|
||||
OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
$version="0.35.1"
|
||||
$version="0.37.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
2
main.go
2
main.go
@@ -5,7 +5,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version string = "0.35"
|
||||
var version string = "0.37"
|
||||
var revision string = "devel"
|
||||
|
||||
func main() {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
.ig
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2021 Junegunn Choi
|
||||
Copyright (c) 2013-2023 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 "Nov 2022" "fzf 0.35.1" "fzf-tmux - open fzf in tmux split pane"
|
||||
.TH fzf-tmux 1 "Jan 2023" "fzf 0.37.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf-tmux - open fzf in tmux split pane
|
||||
|
255
man/man1/fzf.1
255
man/man1/fzf.1
@@ -1,7 +1,7 @@
|
||||
.ig
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2021 Junegunn Choi
|
||||
Copyright (c) 2013-2023 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 "Nov 2022" "fzf 0.35.1" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "Jan 2023" "fzf 0.37.0" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -230,6 +230,10 @@ Draw border around the finder
|
||||
.BR none
|
||||
.br
|
||||
|
||||
If you use a terminal emulator where each box-drawing character takes
|
||||
2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR to \fB1\fR. If the border is
|
||||
still not properly rendered, set \fB--no-unicode\fR.
|
||||
|
||||
.TP
|
||||
.BI "--border-label" [=LABEL]
|
||||
Label to print on the horizontal border line. Should be used with one of the
|
||||
@@ -333,11 +337,13 @@ e.g.
|
||||
Determines the display style of finder info (match counters).
|
||||
|
||||
.br
|
||||
.BR default " Display on the next line to the prompt"
|
||||
.BR default " Display on the next line to the prompt"
|
||||
.br
|
||||
.BR inline " Display on the same line"
|
||||
.BR inline " Display on the same line with the default separator ' < '"
|
||||
.br
|
||||
.BR hidden " Do not display finder info"
|
||||
.BR inline:SEPARATOR " Display on the same line with a non-default separator"
|
||||
.br
|
||||
.BR hidden " Do not display finder info"
|
||||
.br
|
||||
|
||||
.TP
|
||||
@@ -356,6 +362,15 @@ ANSI color codes are supported.
|
||||
Do not display horizontal separator on the info line. A synonym for
|
||||
\fB--separator=''\fB
|
||||
|
||||
.TP
|
||||
.BI "--scrollbar=" "CHAR"
|
||||
Use the given character to render scrollbar. (default: '│' or ':' depending on
|
||||
\fB--no-unicode\fR).
|
||||
|
||||
.TP
|
||||
.B "--no-scrollbar"
|
||||
Do not display scrollbar. A synonym for \fB--scrollbar=''\fB
|
||||
|
||||
.TP
|
||||
.BI "--prompt=" "STR"
|
||||
Input prompt (default: '> ')
|
||||
@@ -404,26 +419,28 @@ color mappings.
|
||||
\fBbw \fRNo colors (equivalent to \fB--no-color\fR)
|
||||
|
||||
.B COLOR NAMES:
|
||||
\fBfg \fRText
|
||||
\fBbg \fRBackground
|
||||
\fBpreview-fg \fRPreview window text
|
||||
\fBpreview-bg \fRPreview window background
|
||||
\fBhl \fRHighlighted substrings
|
||||
\fBfg+ \fRText (current line)
|
||||
\fBbg+ \fRBackground (current line)
|
||||
\fBgutter \fRGutter on the left (defaults to \fBbg+\fR)
|
||||
\fBhl+ \fRHighlighted substrings (current line)
|
||||
\fBquery \fRQuery string
|
||||
\fBdisabled \fRQuery string when search is disabled
|
||||
\fBinfo \fRInfo line (match counters)
|
||||
\fBseparator \fRHorizontal separator on info line (match counters)
|
||||
\fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR)
|
||||
\fBlabel \fRBorder label (\fB--border-label\fR and \fB--preview-label\fR)
|
||||
\fBprompt \fRPrompt
|
||||
\fBpointer \fRPointer to the current line
|
||||
\fBmarker \fRMulti-select marker
|
||||
\fBspinner \fRStreaming input indicator
|
||||
\fBheader \fRHeader
|
||||
\fBfg \fRText
|
||||
\fBpreview-fg \fRPreview window text
|
||||
\fBbg \fRBackground
|
||||
\fBpreview-bg \fRPreview window background
|
||||
\fBhl \fRHighlighted substrings
|
||||
\fBfg+ \fRText (current line)
|
||||
\fBbg+ \fRBackground (current line)
|
||||
\fBgutter \fRGutter on the left
|
||||
\fBhl+ \fRHighlighted substrings (current line)
|
||||
\fBquery \fRQuery string
|
||||
\fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR)
|
||||
\fBinfo \fRInfo line (match counters)
|
||||
\fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR)
|
||||
\fBseparator \fRHorizontal separator on info line
|
||||
\fBscrollbar \fRScrollbar
|
||||
\fBlabel \fRBorder label (\fB--border-label\fR and \fB--preview-label\fR)
|
||||
\fBpreview-label \fRBorder label of the preview window (\fB--preview-label\fR)
|
||||
\fBprompt \fRPrompt
|
||||
\fBpointer \fRPointer to the current line
|
||||
\fBmarker \fRMulti-select marker
|
||||
\fBspinner \fRStreaming input indicator
|
||||
\fBheader \fRHeader
|
||||
|
||||
.B ANSI COLORS:
|
||||
\fB-1 \fRDefault terminal foreground/background color
|
||||
@@ -481,7 +498,7 @@ Use black background
|
||||
.BI "--history=" "HISTORY_FILE"
|
||||
Load search history from the specified file and update the file on completion.
|
||||
When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to
|
||||
\fBnext-history\fR and \fBprevious-history\fR.
|
||||
\fBnext-history\fR and \fBprev-history\fR.
|
||||
.TP
|
||||
.BI "--history-size=" "N"
|
||||
Maximum number of entries in the history file (default: 1000). The file is
|
||||
@@ -535,7 +552,8 @@ e.g.
|
||||
Note that you can escape a placeholder pattern by prepending a backslash.
|
||||
|
||||
Preview window will be updated even when there is no match for the current
|
||||
query if any of the placeholder expressions evaluates to a non-empty string.
|
||||
query if any of the placeholder expressions evaluates to a non-empty string
|
||||
or \fB{q}\fR is in the command template.
|
||||
|
||||
Since 0.24.0, fzf can render partial preview content before the preview command
|
||||
completes. ANSI escape sequence for clearing the display (\fBCSI 2 J\fR) is
|
||||
@@ -556,9 +574,9 @@ Label to print on the horizontal border line of the preview window.
|
||||
Should be used with one of the following \fB--preview-window\fR options.
|
||||
|
||||
.br
|
||||
.B * border-rounded (default)
|
||||
.B * border-rounded (default on non-Windows platforms)
|
||||
.br
|
||||
.B * border-sharp
|
||||
.B * border-sharp (default on Windows)
|
||||
.br
|
||||
.B * border-bold
|
||||
.br
|
||||
@@ -720,6 +738,18 @@ ncurses finder only after the input stream is complete.
|
||||
e.g. \fBfzf --multi | fzf --sync\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "--listen=HTTP_PORT"
|
||||
Start HTTP server on the given port. It allows external processes to send
|
||||
actions to perform via POST method.
|
||||
|
||||
e.g.
|
||||
\fB# Start HTTP server on port 6266
|
||||
fzf --listen 6266
|
||||
|
||||
# Send action to the server
|
||||
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
|
||||
\fR
|
||||
.TP
|
||||
.B "--version"
|
||||
Display version information and exit
|
||||
|
||||
@@ -914,6 +944,15 @@ e.g.
|
||||
\fB# Move cursor to the last item and select all items
|
||||
seq 1000 | fzf --multi --sync --bind start:last+select-all\fR
|
||||
.RE
|
||||
\fIload\fR
|
||||
.RS
|
||||
Triggered when the input stream is complete and the initial processing of the
|
||||
list is complete.
|
||||
|
||||
e.g.
|
||||
\fB# Change the prompt to "loaded" when the input stream is complete
|
||||
(seq 10; sleep 1; seq 11 20) | fzf --prompt 'Loading> ' --bind 'load:change-prompt:Loaded> '\fR
|
||||
.RE
|
||||
\fIchange\fR
|
||||
.RS
|
||||
Triggered whenever the query string is changed
|
||||
@@ -922,6 +961,22 @@ e.g.
|
||||
\fB# Move cursor to the first entry whenever the query is changed
|
||||
fzf --bind change:first\fR
|
||||
.RE
|
||||
\fIfocus\fR
|
||||
.RS
|
||||
Triggered when the focus changes due to a vertical cursor movement or a search
|
||||
result update.
|
||||
|
||||
e.g.
|
||||
\fBfzf --bind 'focus:transform-preview-label:echo [ {} ]' --preview 'cat {}'
|
||||
|
||||
# Any action bound to the event runs synchronously and thus can make the interface sluggish
|
||||
# e.g. lolcat isn't one of the fastest programs, and every cursor movement in
|
||||
# fzf will be noticeably affected by its execution time
|
||||
fzf --bind 'focus:transform-preview-label:echo [ {} ] | lolcat -f' --preview 'cat {}'
|
||||
|
||||
# Beware not to introduce an infinite loop
|
||||
seq 10 | fzf --bind 'focus:up' --cycle\fR
|
||||
.RE
|
||||
|
||||
\fIbackward-eof\fR
|
||||
.RS
|
||||
@@ -935,81 +990,93 @@ e.g.
|
||||
.SS AVAILABLE ACTIONS:
|
||||
A key or an event can be bound to one or more of the following actions.
|
||||
|
||||
\fBACTION: DEFAULT BINDINGS (NOTES):
|
||||
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
|
||||
\fBaccept\fR \fIenter double-click\fR
|
||||
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
|
||||
\fBbackward-char\fR \fIctrl-b left\fR
|
||||
\fBbackward-delete-char\fR \fIctrl-h bspace\fR
|
||||
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
|
||||
\fBbackward-kill-word\fR \fIalt-bs\fR
|
||||
\fBbackward-word\fR \fIalt-b shift-left\fR
|
||||
\fBbeginning-of-line\fR \fIctrl-a home\fR
|
||||
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
|
||||
\fBchange-preview(...)\fR (change \fB--preview\fR option)
|
||||
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
|
||||
\fBchange-prompt(...)\fR (change prompt to the given string)
|
||||
\fBclear-screen\fR \fIctrl-l\fR
|
||||
\fBclear-selection\fR (clear multi-selection)
|
||||
\fBclose\fR (close preview window if open, abort fzf otherwise)
|
||||
\fBclear-query\fR (clear query string)
|
||||
\fBdelete-char\fR \fIdel\fR
|
||||
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
|
||||
\fBACTION: DEFAULT BINDINGS (NOTES):
|
||||
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
|
||||
\fBaccept\fR \fIenter double-click\fR
|
||||
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
|
||||
\fBbackward-char\fR \fIctrl-b left\fR
|
||||
\fBbackward-delete-char\fR \fIctrl-h bspace\fR
|
||||
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
|
||||
\fBbackward-kill-word\fR \fIalt-bs\fR
|
||||
\fBbackward-word\fR \fIalt-b shift-left\fR
|
||||
\fBbeginning-of-line\fR \fIctrl-a home\fR
|
||||
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
|
||||
\fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string)
|
||||
\fBchange-preview(...)\fR (change \fB--preview\fR option)
|
||||
\fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string)
|
||||
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
|
||||
\fBchange-prompt(...)\fR (change prompt to the given string)
|
||||
\fBchange-query(...)\fR (change query string to the given string)
|
||||
\fBclear-screen\fR \fIctrl-l\fR
|
||||
\fBclear-selection\fR (clear multi-selection)
|
||||
\fBclose\fR (close preview window if open, abort fzf otherwise)
|
||||
\fBclear-query\fR (clear query string)
|
||||
\fBdelete-char\fR \fIdel\fR
|
||||
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
|
||||
\fBdeselect\fR
|
||||
\fBdeselect-all\fR (deselect all matches)
|
||||
\fBdisable-search\fR (disable search functionality)
|
||||
\fBdown\fR \fIctrl-j ctrl-n down\fR
|
||||
\fBenable-search\fR (enable search functionality)
|
||||
\fBend-of-line\fR \fIctrl-e end\fR
|
||||
\fBexecute(...)\fR (see below for the details)
|
||||
\fBexecute-silent(...)\fR (see below for the details)
|
||||
\fBfirst\fR (move to the first match)
|
||||
\fBforward-char\fR \fIctrl-f right\fR
|
||||
\fBforward-word\fR \fIalt-f shift-right\fR
|
||||
\fBdeselect-all\fR (deselect all matches)
|
||||
\fBdisable-search\fR (disable search functionality)
|
||||
\fBdown\fR \fIctrl-j ctrl-n down\fR
|
||||
\fBenable-search\fR (enable search functionality)
|
||||
\fBend-of-line\fR \fIctrl-e end\fR
|
||||
\fBexecute(...)\fR (see below for the details)
|
||||
\fBexecute-silent(...)\fR (see below for the details)
|
||||
\fBfirst\fR (move to the first match; same as \fBpos(1)\fR)
|
||||
\fBforward-char\fR \fIctrl-f right\fR
|
||||
\fBforward-word\fR \fIalt-f shift-right\fR
|
||||
\fBignore\fR
|
||||
\fBjump\fR (EasyMotion-like 2-keystroke movement)
|
||||
\fBjump-accept\fR (jump and accept)
|
||||
\fBjump\fR (EasyMotion-like 2-keystroke movement)
|
||||
\fBjump-accept\fR (jump and accept)
|
||||
\fBkill-line\fR
|
||||
\fBkill-word\fR \fIalt-d\fR
|
||||
\fBlast\fR (move to the last match)
|
||||
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
|
||||
\fBpage-down\fR \fIpgdn\fR
|
||||
\fBpage-up\fR \fIpgup\fR
|
||||
\fBkill-word\fR \fIalt-d\fR
|
||||
\fBlast\fR (move to the last match; same as \fBpos(-1)\fR)
|
||||
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
|
||||
\fBnext-selected\fR (move to the next selected item)
|
||||
\fBpage-down\fR \fIpgdn\fR
|
||||
\fBpage-up\fR \fIpgup\fR
|
||||
\fBhalf-page-down\fR
|
||||
\fBhalf-page-up\fR
|
||||
\fBpreview(...)\fR (see below for the details)
|
||||
\fBpreview-down\fR \fIshift-down\fR
|
||||
\fBpreview-up\fR \fIshift-up\fR
|
||||
\fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end)
|
||||
\fBprev-history\fR (\fIctrl-p\fR on \fB--history\fR)
|
||||
\fBprev-selected\fR (move to the previous selected item)
|
||||
\fBpreview(...)\fR (see below for the details)
|
||||
\fBpreview-down\fR \fIshift-down\fR
|
||||
\fBpreview-up\fR \fIshift-up\fR
|
||||
\fBpreview-page-down\fR
|
||||
\fBpreview-page-up\fR
|
||||
\fBpreview-half-page-down\fR
|
||||
\fBpreview-half-page-up\fR
|
||||
\fBpreview-bottom\fR
|
||||
\fBpreview-top\fR
|
||||
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
|
||||
\fBprint-query\fR (print query and exit)
|
||||
\fBput\fR (put the character to the prompt)
|
||||
\fBprint-query\fR (print query and exit)
|
||||
\fBput\fR (put the character to the prompt)
|
||||
\fBput(...)\fR (put the given string to the prompt)
|
||||
\fBrefresh-preview\fR
|
||||
\fBrebind(...)\fR (rebind bindings after \fBunbind\fR)
|
||||
\fBreload(...)\fR (see below for the details)
|
||||
\fBreplace-query\fR (replace query string with the current selection)
|
||||
\fBrebind(...)\fR (rebind bindings after \fBunbind\fR)
|
||||
\fBreload(...)\fR (see below for the details)
|
||||
\fBreload-sync(...)\fR (see below for the details)
|
||||
\fBreplace-query\fR (replace query string with the current selection)
|
||||
\fBselect\fR
|
||||
\fBselect-all\fR (select all matches)
|
||||
\fBtoggle\fR (\fIright-click\fR)
|
||||
\fBtoggle-all\fR (toggle all matches)
|
||||
\fBtoggle+down\fR \fIctrl-i (tab)\fR
|
||||
\fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
|
||||
\fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
|
||||
\fBselect-all\fR (select all matches)
|
||||
\fBtoggle\fR (\fIright-click\fR)
|
||||
\fBtoggle-all\fR (toggle all matches)
|
||||
\fBtoggle+down\fR \fIctrl-i (tab)\fR
|
||||
\fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
|
||||
\fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
|
||||
\fBtoggle-preview\fR
|
||||
\fBtoggle-preview-wrap\fR
|
||||
\fBtoggle-search\fR (toggle search functionality)
|
||||
\fBtoggle-search\fR (toggle search functionality)
|
||||
\fBtoggle-sort\fR
|
||||
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
|
||||
\fBunbind(...)\fR (unbind bindings)
|
||||
\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
|
||||
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
|
||||
\fBtransform-border-label(...)\fR (transform border label using an external command)
|
||||
\fBtransform-preview-label(...)\fR (transform preview label using an external command)
|
||||
\fBtransform-prompt(...)\fR (transform prompt string using an external command)
|
||||
\fBtransform-query(...)\fR (transform query string using an external command)
|
||||
\fBunbind(...)\fR (unbind bindings)
|
||||
\fBunix-line-discard\fR \fIctrl-u\fR
|
||||
\fBunix-word-rubout\fR \fIctrl-w\fR
|
||||
\fBup\fR \fIctrl-k ctrl-p up\fR
|
||||
\fByank\fR \fIctrl-y\fR
|
||||
|
||||
.SS ACTION COMPOSITION
|
||||
|
||||
@@ -1032,6 +1099,8 @@ that case, you can use any of the following alternative notations to avoid
|
||||
parse errors.
|
||||
|
||||
\fBaction-name[...]\fR
|
||||
\fBaction-name{...}\fR
|
||||
\fBaction-name<...>\fR
|
||||
\fBaction-name~...~\fR
|
||||
\fBaction-name!...!\fR
|
||||
\fBaction-name@...@\fR
|
||||
@@ -1092,6 +1161,16 @@ e.g.
|
||||
fzf --bind "change:reload:$RG_PREFIX {q} || true" \\
|
||||
--ansi --disabled --query "$INITIAL_QUERY"\fR
|
||||
|
||||
\fBreload-sync(...)\fR is a synchronous version of \fBreload\fR that replaces
|
||||
the list only when the command is complete. This is useful when the command
|
||||
takes a while to produce the initial output and you don't want fzf to run
|
||||
against an empty list while the command is running.
|
||||
|
||||
|
||||
e.g.
|
||||
\fB# You can still filter and select entries from the initial list for 3 seconds
|
||||
seq 100 | fzf --bind 'load:reload-sync(sleep 3; seq 1000)+unbind(load)'\fR
|
||||
|
||||
.SS PREVIEW BINDING
|
||||
|
||||
With \fBpreview(...)\fR action, you can specify multiple different preview
|
||||
|
@@ -1,4 +1,4 @@
|
||||
" Copyright (c) 2017 Junegunn Choi
|
||||
" Copyright (c) 2013-2023 Junegunn Choi
|
||||
"
|
||||
" MIT License
|
||||
"
|
||||
@@ -511,7 +511,10 @@ try
|
||||
let height = s:calc_size(&lines, dict.down, dict)
|
||||
let optstr .= ' --height='.height
|
||||
endif
|
||||
let optstr .= s:border_opt(get(dict, 'window', 0))
|
||||
" Respect --border option given in 'options'
|
||||
if stridx(optstr, '--border') < 0 && stridx(optstr, '--no-border') < 0
|
||||
let optstr .= s:border_opt(get(dict, 'window', 0))
|
||||
endif
|
||||
let prev_default_command = $FZF_DEFAULT_COMMAND
|
||||
if len(source_command)
|
||||
let $FZF_DEFAULT_COMMAND = source_command
|
||||
@@ -755,9 +758,9 @@ function! s:border_opt(window)
|
||||
endif
|
||||
|
||||
" Border style
|
||||
let style = tolower(get(a:window, 'border', 'rounded'))
|
||||
if !has_key(a:window, 'border') && !get(a:window, 'rounded', 1)
|
||||
let style = 'sharp'
|
||||
let style = tolower(get(a:window, 'border', ''))
|
||||
if !has_key(a:window, 'border') && has_key(a:window, 'rounded')
|
||||
let style = a:window.rounded ? 'rounded' : 'sharp'
|
||||
endif
|
||||
if style == 'none' || style == 'no'
|
||||
return ''
|
||||
@@ -765,7 +768,7 @@ function! s:border_opt(window)
|
||||
|
||||
" For --border styles, we need fzf 0.24.0 or above
|
||||
call fzf#exec('0.24.0')
|
||||
let opt = ' --border=' . style
|
||||
let opt = ' --border ' . style
|
||||
if has_key(a:window, 'highlight')
|
||||
let color = s:get_color('fg', a:window.highlight)
|
||||
if len(color)
|
||||
@@ -827,6 +830,17 @@ if exists(':tnoremap')
|
||||
tnoremap <silent> <Plug>(fzf-normal) <C-\><C-n>
|
||||
endif
|
||||
|
||||
let s:warned = 0
|
||||
function! s:handle_ambidouble(dict)
|
||||
if &ambiwidth == 'double'
|
||||
let a:dict.env = { 'RUNEWIDTH_EASTASIAN': '1' }
|
||||
elseif !s:warned && $RUNEWIDTH_EASTASIAN == '1' && &ambiwidth !=# 'double'
|
||||
call s:warn("$RUNEWIDTH_EASTASIAN is '1' but &ambiwidth is not 'double'")
|
||||
2sleep
|
||||
let s:warned = 1
|
||||
endif
|
||||
endfunction
|
||||
|
||||
function! s:execute_term(dict, command, temps) abort
|
||||
let winrest = winrestcmd()
|
||||
let pbuf = bufnr('')
|
||||
@@ -896,6 +910,7 @@ function! s:execute_term(dict, command, temps) abort
|
||||
endif
|
||||
let command .= s:term_marker
|
||||
if has('nvim')
|
||||
call s:handle_ambidouble(fzf)
|
||||
call termopen(command, fzf)
|
||||
else
|
||||
let term_opts = {'exit_cb': function(fzf.on_exit)}
|
||||
@@ -907,6 +922,7 @@ function! s:execute_term(dict, command, temps) abort
|
||||
else
|
||||
let term_opts.curwin = 1
|
||||
endif
|
||||
call s:handle_ambidouble(term_opts)
|
||||
let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts)
|
||||
if is_popup && exists('#TerminalWinOpen')
|
||||
doautocmd <nomodeline> TerminalWinOpen
|
||||
|
@@ -310,7 +310,7 @@ complete -o default -F _fzf_opts_completion fzf-tmux
|
||||
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
|
||||
a_cmds="
|
||||
awk cat diff diff3
|
||||
emacs emacsclient ex file ftp g++ gcc gvim head hg java
|
||||
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
|
||||
javac ld less more mvim nvim patch perl python ruby
|
||||
sed sftp sort source tail tee uniq vi view vim wc xdg-open
|
||||
basename bunzip2 bzip2 chmod chown curl cp dirname du
|
||||
|
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2021 Junegunn Choi
|
||||
Copyright (c) 2013-2023 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
|
||||
|
@@ -617,7 +617,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
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 := charWhite
|
||||
prevClass := initialCharClass
|
||||
if sidx > 0 {
|
||||
prevClass = charClassOf(text.Get(sidx - 1))
|
||||
}
|
||||
|
47
src/core.go
47
src/core.go
@@ -213,15 +213,6 @@ func Run(opts *Options, version string, revision string) {
|
||||
clearSelection := util.Once(false)
|
||||
ticks := 0
|
||||
var nextCommand *string
|
||||
restart := func(command string) {
|
||||
reading = true
|
||||
clearCache = util.Once(true)
|
||||
clearSelection = util.Once(true)
|
||||
chunkList.Clear()
|
||||
itemIndex = 0
|
||||
header = make([]string, 0, opts.HeaderLines)
|
||||
go reader.restart(command)
|
||||
}
|
||||
eventBox.Watch(EvtReadNew)
|
||||
total := 0
|
||||
query := []rune{}
|
||||
@@ -236,6 +227,25 @@ func Run(opts *Options, version string, revision string) {
|
||||
terminal.startChan <- fitpad{-1, -1}
|
||||
}
|
||||
}
|
||||
|
||||
useSnapshot := false
|
||||
var snapshot []*Chunk
|
||||
var prevSnapshot []*Chunk
|
||||
var count int
|
||||
restart := func(command string) {
|
||||
reading = true
|
||||
clearCache = util.Once(true)
|
||||
clearSelection = util.Once(true)
|
||||
// We should not update snapshot if reload is triggered again while
|
||||
// the previous reload is in progress
|
||||
if useSnapshot && prevSnapshot != nil {
|
||||
snapshot, count = chunkList.Snapshot()
|
||||
}
|
||||
chunkList.Clear()
|
||||
itemIndex = 0
|
||||
header = make([]string, 0, opts.HeaderLines)
|
||||
go reader.restart(command)
|
||||
}
|
||||
for {
|
||||
delay := true
|
||||
ticks++
|
||||
@@ -267,7 +277,13 @@ func Run(opts *Options, version string, revision string) {
|
||||
} else {
|
||||
reading = reading && evt == EvtReadNew
|
||||
}
|
||||
snapshot, count := chunkList.Snapshot()
|
||||
if useSnapshot && evt == EvtReadFin {
|
||||
useSnapshot = false
|
||||
prevSnapshot = nil
|
||||
}
|
||||
if !useSnapshot {
|
||||
snapshot, count = chunkList.Snapshot()
|
||||
}
|
||||
total = count
|
||||
terminal.UpdateCount(total, !reading, value.(*string))
|
||||
if opts.Sync {
|
||||
@@ -277,7 +293,7 @@ func Run(opts *Options, version string, revision string) {
|
||||
if heightUnknown && !deferred {
|
||||
determine(!reading)
|
||||
}
|
||||
reset := clearCache()
|
||||
reset := !useSnapshot && clearCache()
|
||||
matcher.Reset(snapshot, input(reset), false, !reading, sort, reset)
|
||||
|
||||
case EvtSearchNew:
|
||||
@@ -286,6 +302,9 @@ func Run(opts *Options, version string, revision string) {
|
||||
case searchRequest:
|
||||
sort = val.sort
|
||||
command = val.command
|
||||
if command != nil {
|
||||
useSnapshot = val.sync
|
||||
}
|
||||
}
|
||||
if command != nil {
|
||||
if reading {
|
||||
@@ -296,8 +315,10 @@ func Run(opts *Options, version string, revision string) {
|
||||
}
|
||||
break
|
||||
}
|
||||
snapshot, _ := chunkList.Snapshot()
|
||||
reset := clearCache()
|
||||
if !useSnapshot {
|
||||
snapshot, _ = chunkList.Snapshot()
|
||||
}
|
||||
reset := !useSnapshot && clearCache()
|
||||
matcher.Reset(snapshot, input(reset), true, !reading, sort, reset)
|
||||
delay = false
|
||||
|
||||
|
661
src/options.go
661
src/options.go
@@ -70,9 +70,11 @@ const usage = `usage: fzf [options]
|
||||
(default: 0 or center)
|
||||
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
|
||||
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
|
||||
--info=STYLE Finder info style [default|inline|hidden]
|
||||
--info=STYLE Finder info style [default|hidden|inline|inline:SEPARATOR]
|
||||
--separator=STR String to form horizontal separator on info line
|
||||
--no-separator Hide info line separator
|
||||
--scrollbar[=CHAR] Scrollbar character
|
||||
--no-scrollbar Hide scrollbar
|
||||
--prompt=STR Input prompt (default: '> ')
|
||||
--pointer=STR Pointer to the current line (default: '>')
|
||||
--marker=STR Multi-select marker (default: '>')
|
||||
@@ -113,6 +115,7 @@ const usage = `usage: fzf [options]
|
||||
--read0 Read input delimited by ASCII NUL characters
|
||||
--print0 Print output delimited by ASCII NUL characters
|
||||
--sync Synchronous search for multi-staged filtering
|
||||
--listen=HTTP_PORT Start HTTP server to receive actions (POST /)
|
||||
--version Display version information and exit
|
||||
|
||||
Environment variables
|
||||
@@ -122,6 +125,8 @@ const usage = `usage: fzf [options]
|
||||
|
||||
`
|
||||
|
||||
const defaultInfoSep = " < "
|
||||
|
||||
// Case denotes case-sensitivity of search
|
||||
type Case int
|
||||
|
||||
@@ -204,6 +209,14 @@ type previewOpts struct {
|
||||
alternative *previewOpts
|
||||
}
|
||||
|
||||
func (o *previewOpts) Visible() bool {
|
||||
return o.size.size > 0 || o.alternative != nil && o.alternative.size.size > 0
|
||||
}
|
||||
|
||||
func (o *previewOpts) Toggle() {
|
||||
o.hidden = !o.hidden
|
||||
}
|
||||
|
||||
func parseLabelPosition(opts *labelOpts, arg string) {
|
||||
opts.column = 0
|
||||
opts.bottom = false
|
||||
@@ -235,6 +248,10 @@ func (a previewOpts) sameContentLayout(b previewOpts) bool {
|
||||
return a.wrap == b.wrap && a.headerLines == b.headerLines
|
||||
}
|
||||
|
||||
func firstLine(s string) string {
|
||||
return strings.SplitN(s, "\n", 2)[0]
|
||||
}
|
||||
|
||||
// Options stores the values of command-line options
|
||||
type Options struct {
|
||||
Fuzzy bool
|
||||
@@ -266,6 +283,7 @@ type Options struct {
|
||||
ScrollOff int
|
||||
FileWord bool
|
||||
InfoStyle infoStyle
|
||||
InfoSep string
|
||||
Separator *string
|
||||
JumpLabels string
|
||||
Prompt string
|
||||
@@ -289,6 +307,7 @@ type Options struct {
|
||||
HeaderLines int
|
||||
HeaderFirst bool
|
||||
Ellipsis string
|
||||
Scrollbar *string
|
||||
Margin [4]sizeSpec
|
||||
Padding [4]sizeSpec
|
||||
BorderShape tui.BorderShape
|
||||
@@ -296,12 +315,13 @@ type Options struct {
|
||||
PreviewLabel labelOpts
|
||||
Unicode bool
|
||||
Tabstop int
|
||||
ListenPort int
|
||||
ClearOnExit bool
|
||||
Version bool
|
||||
}
|
||||
|
||||
func defaultPreviewOpts(command string) previewOpts {
|
||||
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded, 0, 0, nil}
|
||||
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.DefaultBorderShape, 0, 0, nil}
|
||||
}
|
||||
|
||||
func defaultOptions() *Options {
|
||||
@@ -357,6 +377,7 @@ func defaultOptions() *Options {
|
||||
HeaderLines: 0,
|
||||
HeaderFirst: false,
|
||||
Ellipsis: "..",
|
||||
Scrollbar: nil,
|
||||
Margin: defaultMargin(),
|
||||
Padding: defaultMargin(),
|
||||
Unicode: true,
|
||||
@@ -529,7 +550,7 @@ func parseBorder(str string, optional bool) tui.BorderShape {
|
||||
return tui.BorderNone
|
||||
default:
|
||||
if optional && str == "" {
|
||||
return tui.BorderRounded
|
||||
return tui.DefaultBorderShape
|
||||
}
|
||||
errorExit("invalid border style (expected: rounded|sharp|bold|double|horizontal|vertical|top|bottom|left|right|none)")
|
||||
}
|
||||
@@ -537,8 +558,13 @@ func parseBorder(str string, optional bool) tui.BorderShape {
|
||||
}
|
||||
|
||||
func parseKeyChords(str string, message string) map[tui.Event]string {
|
||||
return parseKeyChordsImpl(str, message, errorExit)
|
||||
}
|
||||
|
||||
func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.Event]string {
|
||||
if len(str) == 0 {
|
||||
errorExit(message)
|
||||
exit(message)
|
||||
return nil
|
||||
}
|
||||
|
||||
str = regexp.MustCompile("(?i)(alt-),").ReplaceAllString(str, "$1"+string([]rune{escapedComma}))
|
||||
@@ -588,6 +614,10 @@ func parseKeyChords(str string, message string) map[tui.Event]string {
|
||||
add(tui.BackwardEOF)
|
||||
case "start":
|
||||
add(tui.Start)
|
||||
case "load":
|
||||
add(tui.Load)
|
||||
case "focus":
|
||||
add(tui.Focus)
|
||||
case "alt-enter", "alt-return":
|
||||
chords[tui.CtrlAltKey('m')] = key
|
||||
case "alt-space":
|
||||
@@ -670,7 +700,8 @@ func parseKeyChords(str string, message string) map[tui.Event]string {
|
||||
} else if len(runes) == 1 {
|
||||
chords[tui.Key(runes[0])] = key
|
||||
} else {
|
||||
errorExit("unsupported key: " + key)
|
||||
exit("unsupported key: " + key)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -843,8 +874,12 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
|
||||
mergeAttr(&theme.Border)
|
||||
case "separator":
|
||||
mergeAttr(&theme.Separator)
|
||||
case "scrollbar":
|
||||
mergeAttr(&theme.Scrollbar)
|
||||
case "label":
|
||||
mergeAttr(&theme.BorderLabel)
|
||||
case "preview-label":
|
||||
mergeAttr(&theme.PreviewLabel)
|
||||
case "prompt":
|
||||
mergeAttr(&theme.Prompt)
|
||||
case "spinner":
|
||||
@@ -866,8 +901,9 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
|
||||
}
|
||||
|
||||
var (
|
||||
executeRegexp *regexp.Regexp
|
||||
splitRegexp *regexp.Regexp
|
||||
executeRegexp *regexp.Regexp
|
||||
splitRegexp *regexp.Regexp
|
||||
actionNameRegexp *regexp.Regexp
|
||||
)
|
||||
|
||||
func firstKey(keymap map[tui.Event]string) tui.Event {
|
||||
@@ -884,48 +920,271 @@ const (
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Backreferences are not supported.
|
||||
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
|
||||
executeRegexp = regexp.MustCompile(
|
||||
`(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|(?:re|un)bind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|(?:re|un)bind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
|
||||
`(?si)[:+](execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
|
||||
splitRegexp = regexp.MustCompile("[,:]+")
|
||||
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
||||
}
|
||||
|
||||
func parseKeymap(keymap map[tui.Event][]*action, str string) {
|
||||
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
|
||||
symbol := ":"
|
||||
if strings.HasPrefix(src, "+") {
|
||||
symbol = "+"
|
||||
func maskActionContents(action string) string {
|
||||
masked := ""
|
||||
Loop:
|
||||
for len(action) > 0 {
|
||||
loc := executeRegexp.FindStringIndex(action)
|
||||
if loc == nil {
|
||||
masked += action
|
||||
break
|
||||
}
|
||||
prefix := symbol + "execute"
|
||||
if strings.HasPrefix(src[1:], "reload") {
|
||||
prefix = symbol + "reload"
|
||||
} else if strings.HasPrefix(src[1:], "change-preview-window") {
|
||||
prefix = symbol + "change-preview-window"
|
||||
} else if strings.HasPrefix(src[1:], "change-preview") {
|
||||
prefix = symbol + "change-preview"
|
||||
} else if strings.HasPrefix(src[1:], "preview") {
|
||||
prefix = symbol + "preview"
|
||||
} else if strings.HasPrefix(src[1:], "unbind") {
|
||||
prefix = symbol + "unbind"
|
||||
} else if strings.HasPrefix(src[1:], "rebind") {
|
||||
prefix = symbol + "rebind"
|
||||
} else if strings.HasPrefix(src[1:], "change-prompt") {
|
||||
prefix = symbol + "change-prompt"
|
||||
} else if src[len(prefix)] == '-' {
|
||||
c := src[len(prefix)+1]
|
||||
if c == 's' || c == 'S' {
|
||||
prefix += "-silent"
|
||||
} else {
|
||||
prefix += "-multi"
|
||||
}
|
||||
masked += action[:loc[1]]
|
||||
action = action[loc[1]:]
|
||||
if len(action) == 0 {
|
||||
break
|
||||
}
|
||||
return prefix + "(" + strings.Repeat(" ", len(src)-len(prefix)-2) + ")"
|
||||
})
|
||||
cs := string(action[0])
|
||||
ce := ")"
|
||||
switch action[0] {
|
||||
case ':':
|
||||
masked += strings.Repeat(" ", len(action))
|
||||
break Loop
|
||||
case '(':
|
||||
ce = ")"
|
||||
case '{':
|
||||
ce = "}"
|
||||
case '[':
|
||||
ce = "]"
|
||||
case '<':
|
||||
ce = ">"
|
||||
case '~', '!', '@', '#', '$', '%', '^', '&', '*', ';', '/', '|':
|
||||
ce = string(cs)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
cs = regexp.QuoteMeta(cs)
|
||||
ce = regexp.QuoteMeta(ce)
|
||||
|
||||
// @$ or @+
|
||||
loc = regexp.MustCompile(fmt.Sprintf(`(?s)^%s.*?(%s[+,]|%s$)`, cs, ce, ce)).FindStringIndex(action)
|
||||
if loc == nil {
|
||||
masked += action
|
||||
break
|
||||
}
|
||||
// Keep + or , at the end
|
||||
lastChar := action[loc[1]-1]
|
||||
if lastChar == '+' || lastChar == ',' {
|
||||
loc[1]--
|
||||
}
|
||||
masked += strings.Repeat(" ", loc[1])
|
||||
action = action[loc[1]:]
|
||||
}
|
||||
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
|
||||
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
|
||||
masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1)
|
||||
return masked
|
||||
}
|
||||
|
||||
func parseSingleActionList(str string, exit func(string)) []*action {
|
||||
// We prepend a colon to satisfy executeRegexp and remove it later
|
||||
masked := maskActionContents(":" + str)[1:]
|
||||
return parseActionList(masked, str, []*action{}, false, exit)
|
||||
}
|
||||
|
||||
func parseActionList(masked string, original string, prevActions []*action, putAllowed bool, exit func(string)) []*action {
|
||||
maskedStrings := strings.Split(masked, "+")
|
||||
originalStrings := make([]string, len(maskedStrings))
|
||||
idx := 0
|
||||
for i, maskedString := range maskedStrings {
|
||||
originalStrings[i] = original[idx : idx+len(maskedString)]
|
||||
idx += len(maskedString) + 1
|
||||
}
|
||||
actions := make([]*action, 0, len(maskedStrings))
|
||||
appendAction := func(types ...actionType) {
|
||||
actions = append(actions, toActions(types...)...)
|
||||
}
|
||||
prevSpec := ""
|
||||
for specIndex, spec := range originalStrings {
|
||||
spec = prevSpec + spec
|
||||
specLower := strings.ToLower(spec)
|
||||
switch specLower {
|
||||
case "ignore":
|
||||
appendAction(actIgnore)
|
||||
case "beginning-of-line":
|
||||
appendAction(actBeginningOfLine)
|
||||
case "abort":
|
||||
appendAction(actAbort)
|
||||
case "accept":
|
||||
appendAction(actAccept)
|
||||
case "accept-non-empty":
|
||||
appendAction(actAcceptNonEmpty)
|
||||
case "print-query":
|
||||
appendAction(actPrintQuery)
|
||||
case "refresh-preview":
|
||||
appendAction(actRefreshPreview)
|
||||
case "replace-query":
|
||||
appendAction(actReplaceQuery)
|
||||
case "backward-char":
|
||||
appendAction(actBackwardChar)
|
||||
case "backward-delete-char":
|
||||
appendAction(actBackwardDeleteChar)
|
||||
case "backward-delete-char/eof":
|
||||
appendAction(actBackwardDeleteCharEOF)
|
||||
case "backward-word":
|
||||
appendAction(actBackwardWord)
|
||||
case "clear-screen":
|
||||
appendAction(actClearScreen)
|
||||
case "delete-char":
|
||||
appendAction(actDeleteChar)
|
||||
case "delete-char/eof":
|
||||
appendAction(actDeleteCharEOF)
|
||||
case "deselect":
|
||||
appendAction(actDeselect)
|
||||
case "end-of-line":
|
||||
appendAction(actEndOfLine)
|
||||
case "cancel":
|
||||
appendAction(actCancel)
|
||||
case "clear-query":
|
||||
appendAction(actClearQuery)
|
||||
case "clear-selection":
|
||||
appendAction(actClearSelection)
|
||||
case "forward-char":
|
||||
appendAction(actForwardChar)
|
||||
case "forward-word":
|
||||
appendAction(actForwardWord)
|
||||
case "jump":
|
||||
appendAction(actJump)
|
||||
case "jump-accept":
|
||||
appendAction(actJumpAccept)
|
||||
case "kill-line":
|
||||
appendAction(actKillLine)
|
||||
case "kill-word":
|
||||
appendAction(actKillWord)
|
||||
case "unix-line-discard", "line-discard":
|
||||
appendAction(actUnixLineDiscard)
|
||||
case "unix-word-rubout", "word-rubout":
|
||||
appendAction(actUnixWordRubout)
|
||||
case "yank":
|
||||
appendAction(actYank)
|
||||
case "backward-kill-word":
|
||||
appendAction(actBackwardKillWord)
|
||||
case "toggle-down":
|
||||
appendAction(actToggle, actDown)
|
||||
case "toggle-up":
|
||||
appendAction(actToggle, actUp)
|
||||
case "toggle-in":
|
||||
appendAction(actToggleIn)
|
||||
case "toggle-out":
|
||||
appendAction(actToggleOut)
|
||||
case "toggle-all":
|
||||
appendAction(actToggleAll)
|
||||
case "toggle-search":
|
||||
appendAction(actToggleSearch)
|
||||
case "select":
|
||||
appendAction(actSelect)
|
||||
case "select-all":
|
||||
appendAction(actSelectAll)
|
||||
case "deselect-all":
|
||||
appendAction(actDeselectAll)
|
||||
case "close":
|
||||
appendAction(actClose)
|
||||
case "toggle":
|
||||
appendAction(actToggle)
|
||||
case "down":
|
||||
appendAction(actDown)
|
||||
case "up":
|
||||
appendAction(actUp)
|
||||
case "first", "top":
|
||||
appendAction(actFirst)
|
||||
case "last":
|
||||
appendAction(actLast)
|
||||
case "page-up":
|
||||
appendAction(actPageUp)
|
||||
case "page-down":
|
||||
appendAction(actPageDown)
|
||||
case "half-page-up":
|
||||
appendAction(actHalfPageUp)
|
||||
case "half-page-down":
|
||||
appendAction(actHalfPageDown)
|
||||
case "prev-history", "previous-history":
|
||||
appendAction(actPrevHistory)
|
||||
case "next-history":
|
||||
appendAction(actNextHistory)
|
||||
case "prev-selected":
|
||||
appendAction(actPrevSelected)
|
||||
case "next-selected":
|
||||
appendAction(actNextSelected)
|
||||
case "toggle-preview":
|
||||
appendAction(actTogglePreview)
|
||||
case "toggle-preview-wrap":
|
||||
appendAction(actTogglePreviewWrap)
|
||||
case "toggle-sort":
|
||||
appendAction(actToggleSort)
|
||||
case "preview-top":
|
||||
appendAction(actPreviewTop)
|
||||
case "preview-bottom":
|
||||
appendAction(actPreviewBottom)
|
||||
case "preview-up":
|
||||
appendAction(actPreviewUp)
|
||||
case "preview-down":
|
||||
appendAction(actPreviewDown)
|
||||
case "preview-page-up":
|
||||
appendAction(actPreviewPageUp)
|
||||
case "preview-page-down":
|
||||
appendAction(actPreviewPageDown)
|
||||
case "preview-half-page-up":
|
||||
appendAction(actPreviewHalfPageUp)
|
||||
case "preview-half-page-down":
|
||||
appendAction(actPreviewHalfPageDown)
|
||||
case "enable-search":
|
||||
appendAction(actEnableSearch)
|
||||
case "disable-search":
|
||||
appendAction(actDisableSearch)
|
||||
case "put":
|
||||
if putAllowed {
|
||||
appendAction(actRune)
|
||||
} else {
|
||||
exit("unable to put non-printable character")
|
||||
}
|
||||
default:
|
||||
t := isExecuteAction(specLower)
|
||||
if t == actIgnore {
|
||||
if specIndex == 0 && specLower == "" {
|
||||
actions = append(prevActions, actions...)
|
||||
} else {
|
||||
exit("unknown action: " + spec)
|
||||
}
|
||||
} else {
|
||||
offset := len(actionNameRegexp.FindString(spec))
|
||||
var actionArg string
|
||||
if spec[offset] == ':' {
|
||||
if specIndex == len(originalStrings)-1 {
|
||||
actionArg = spec[offset+1:]
|
||||
actions = append(actions, &action{t: t, a: actionArg})
|
||||
} else {
|
||||
prevSpec = spec + "+"
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
actionArg = spec[offset+1 : len(spec)-1]
|
||||
actions = append(actions, &action{t: t, a: actionArg})
|
||||
}
|
||||
switch t {
|
||||
case actUnbind, actRebind:
|
||||
parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit)
|
||||
case actChangePreviewWindow:
|
||||
opts := previewOpts{}
|
||||
for _, arg := range strings.Split(actionArg, "|") {
|
||||
// Make sure that each expression is valid
|
||||
parsePreviewWindowImpl(&opts, arg, exit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
prevSpec = ""
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
func parseKeymap(keymap map[tui.Event][]*action, str string, exit func(string)) {
|
||||
masked := maskActionContents(str)
|
||||
idx := 0
|
||||
for _, pairStr := range strings.Split(masked, ",") {
|
||||
origPairStr := str[idx : idx+len(pairStr)]
|
||||
@@ -933,7 +1192,7 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) {
|
||||
|
||||
pair := strings.SplitN(pairStr, ":", 2)
|
||||
if len(pair) < 2 {
|
||||
errorExit("bind action not specified: " + origPairStr)
|
||||
exit("bind action not specified: " + origPairStr)
|
||||
}
|
||||
var key tui.Event
|
||||
if len(pair[0]) == 1 && pair[0][0] == escapedColon {
|
||||
@@ -943,243 +1202,63 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) {
|
||||
} else if len(pair[0]) == 1 && pair[0][0] == escapedPlus {
|
||||
key = tui.Key('+')
|
||||
} else {
|
||||
keys := parseKeyChords(pair[0], "key name required")
|
||||
keys := parseKeyChordsImpl(pair[0], "key name required", exit)
|
||||
key = firstKey(keys)
|
||||
}
|
||||
|
||||
idx2 := len(pair[0]) + 1
|
||||
specs := strings.Split(pair[1], "+")
|
||||
actions := make([]*action, 0, len(specs))
|
||||
appendAction := func(types ...actionType) {
|
||||
actions = append(actions, toActions(types...)...)
|
||||
}
|
||||
prevSpec := ""
|
||||
for specIndex, maskedSpec := range specs {
|
||||
spec := origPairStr[idx2 : idx2+len(maskedSpec)]
|
||||
idx2 += len(maskedSpec) + 1
|
||||
spec = prevSpec + spec
|
||||
specLower := strings.ToLower(spec)
|
||||
switch specLower {
|
||||
case "ignore":
|
||||
appendAction(actIgnore)
|
||||
case "beginning-of-line":
|
||||
appendAction(actBeginningOfLine)
|
||||
case "abort":
|
||||
appendAction(actAbort)
|
||||
case "accept":
|
||||
appendAction(actAccept)
|
||||
case "accept-non-empty":
|
||||
appendAction(actAcceptNonEmpty)
|
||||
case "print-query":
|
||||
appendAction(actPrintQuery)
|
||||
case "refresh-preview":
|
||||
appendAction(actRefreshPreview)
|
||||
case "replace-query":
|
||||
appendAction(actReplaceQuery)
|
||||
case "backward-char":
|
||||
appendAction(actBackwardChar)
|
||||
case "backward-delete-char":
|
||||
appendAction(actBackwardDeleteChar)
|
||||
case "backward-delete-char/eof":
|
||||
appendAction(actBackwardDeleteCharEOF)
|
||||
case "backward-word":
|
||||
appendAction(actBackwardWord)
|
||||
case "clear-screen":
|
||||
appendAction(actClearScreen)
|
||||
case "delete-char":
|
||||
appendAction(actDeleteChar)
|
||||
case "delete-char/eof":
|
||||
appendAction(actDeleteCharEOF)
|
||||
case "deselect":
|
||||
appendAction(actDeselect)
|
||||
case "end-of-line":
|
||||
appendAction(actEndOfLine)
|
||||
case "cancel":
|
||||
appendAction(actCancel)
|
||||
case "clear-query":
|
||||
appendAction(actClearQuery)
|
||||
case "clear-selection":
|
||||
appendAction(actClearSelection)
|
||||
case "forward-char":
|
||||
appendAction(actForwardChar)
|
||||
case "forward-word":
|
||||
appendAction(actForwardWord)
|
||||
case "jump":
|
||||
appendAction(actJump)
|
||||
case "jump-accept":
|
||||
appendAction(actJumpAccept)
|
||||
case "kill-line":
|
||||
appendAction(actKillLine)
|
||||
case "kill-word":
|
||||
appendAction(actKillWord)
|
||||
case "unix-line-discard", "line-discard":
|
||||
appendAction(actUnixLineDiscard)
|
||||
case "unix-word-rubout", "word-rubout":
|
||||
appendAction(actUnixWordRubout)
|
||||
case "yank":
|
||||
appendAction(actYank)
|
||||
case "backward-kill-word":
|
||||
appendAction(actBackwardKillWord)
|
||||
case "toggle-down":
|
||||
appendAction(actToggle, actDown)
|
||||
case "toggle-up":
|
||||
appendAction(actToggle, actUp)
|
||||
case "toggle-in":
|
||||
appendAction(actToggleIn)
|
||||
case "toggle-out":
|
||||
appendAction(actToggleOut)
|
||||
case "toggle-all":
|
||||
appendAction(actToggleAll)
|
||||
case "toggle-search":
|
||||
appendAction(actToggleSearch)
|
||||
case "select":
|
||||
appendAction(actSelect)
|
||||
case "select-all":
|
||||
appendAction(actSelectAll)
|
||||
case "deselect-all":
|
||||
appendAction(actDeselectAll)
|
||||
case "close":
|
||||
appendAction(actClose)
|
||||
case "toggle":
|
||||
appendAction(actToggle)
|
||||
case "down":
|
||||
appendAction(actDown)
|
||||
case "up":
|
||||
appendAction(actUp)
|
||||
case "first", "top":
|
||||
appendAction(actFirst)
|
||||
case "last":
|
||||
appendAction(actLast)
|
||||
case "page-up":
|
||||
appendAction(actPageUp)
|
||||
case "page-down":
|
||||
appendAction(actPageDown)
|
||||
case "half-page-up":
|
||||
appendAction(actHalfPageUp)
|
||||
case "half-page-down":
|
||||
appendAction(actHalfPageDown)
|
||||
case "previous-history":
|
||||
appendAction(actPreviousHistory)
|
||||
case "next-history":
|
||||
appendAction(actNextHistory)
|
||||
case "toggle-preview":
|
||||
appendAction(actTogglePreview)
|
||||
case "toggle-preview-wrap":
|
||||
appendAction(actTogglePreviewWrap)
|
||||
case "toggle-sort":
|
||||
appendAction(actToggleSort)
|
||||
case "preview-top":
|
||||
appendAction(actPreviewTop)
|
||||
case "preview-bottom":
|
||||
appendAction(actPreviewBottom)
|
||||
case "preview-up":
|
||||
appendAction(actPreviewUp)
|
||||
case "preview-down":
|
||||
appendAction(actPreviewDown)
|
||||
case "preview-page-up":
|
||||
appendAction(actPreviewPageUp)
|
||||
case "preview-page-down":
|
||||
appendAction(actPreviewPageDown)
|
||||
case "preview-half-page-up":
|
||||
appendAction(actPreviewHalfPageUp)
|
||||
case "preview-half-page-down":
|
||||
appendAction(actPreviewHalfPageDown)
|
||||
case "enable-search":
|
||||
appendAction(actEnableSearch)
|
||||
case "disable-search":
|
||||
appendAction(actDisableSearch)
|
||||
case "put":
|
||||
if key.Type == tui.Rune && unicode.IsGraphic(key.Char) {
|
||||
appendAction(actRune)
|
||||
} else {
|
||||
errorExit("unable to put non-printable character: " + pair[0])
|
||||
}
|
||||
default:
|
||||
t := isExecuteAction(specLower)
|
||||
if t == actIgnore {
|
||||
if specIndex == 0 && specLower == "" {
|
||||
actions = append(keymap[key], actions...)
|
||||
} else {
|
||||
errorExit("unknown action: " + spec)
|
||||
}
|
||||
} else {
|
||||
var offset int
|
||||
switch t {
|
||||
case actReload:
|
||||
offset = len("reload")
|
||||
case actPreview:
|
||||
offset = len("preview")
|
||||
case actChangePreviewWindow:
|
||||
offset = len("change-preview-window")
|
||||
case actChangePreview:
|
||||
offset = len("change-preview")
|
||||
case actChangePrompt:
|
||||
offset = len("change-prompt")
|
||||
case actUnbind:
|
||||
offset = len("unbind")
|
||||
case actRebind:
|
||||
offset = len("rebind")
|
||||
case actExecuteSilent:
|
||||
offset = len("execute-silent")
|
||||
case actExecuteMulti:
|
||||
offset = len("execute-multi")
|
||||
default:
|
||||
offset = len("execute")
|
||||
}
|
||||
var actionArg string
|
||||
if spec[offset] == ':' {
|
||||
if specIndex == len(specs)-1 {
|
||||
actionArg = spec[offset+1:]
|
||||
actions = append(actions, &action{t: t, a: actionArg})
|
||||
} else {
|
||||
prevSpec = spec + "+"
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
actionArg = spec[offset+1 : len(spec)-1]
|
||||
actions = append(actions, &action{t: t, a: actionArg})
|
||||
}
|
||||
if t == actUnbind || t == actRebind {
|
||||
parseKeyChords(actionArg, spec[0:offset]+" target required")
|
||||
}
|
||||
}
|
||||
}
|
||||
prevSpec = ""
|
||||
}
|
||||
keymap[key] = actions
|
||||
putAllowed := key.Type == tui.Rune && unicode.IsGraphic(key.Char)
|
||||
keymap[key] = parseActionList(pair[1], origPairStr[len(pair[0])+1:], keymap[key], putAllowed, exit)
|
||||
}
|
||||
}
|
||||
|
||||
func isExecuteAction(str string) actionType {
|
||||
matches := executeRegexp.FindAllStringSubmatch(":"+str, -1)
|
||||
if matches == nil || len(matches) != 1 {
|
||||
masked := maskActionContents(":" + str)[1:]
|
||||
if masked == str {
|
||||
// Not masked
|
||||
return actIgnore
|
||||
}
|
||||
prefix := matches[0][1]
|
||||
if len(prefix) == 0 {
|
||||
prefix = matches[0][2]
|
||||
}
|
||||
|
||||
prefix := actionNameRegexp.FindString(str)
|
||||
switch prefix {
|
||||
case "reload":
|
||||
return actReload
|
||||
case "reload-sync":
|
||||
return actReloadSync
|
||||
case "unbind":
|
||||
return actUnbind
|
||||
case "rebind":
|
||||
return actRebind
|
||||
case "preview":
|
||||
return actPreview
|
||||
case "change-border-label":
|
||||
return actChangeBorderLabel
|
||||
case "change-preview-label":
|
||||
return actChangePreviewLabel
|
||||
case "change-preview-window":
|
||||
return actChangePreviewWindow
|
||||
case "change-preview":
|
||||
return actChangePreview
|
||||
case "change-prompt":
|
||||
return actChangePrompt
|
||||
case "change-query":
|
||||
return actChangeQuery
|
||||
case "pos":
|
||||
return actPosition
|
||||
case "execute":
|
||||
return actExecute
|
||||
case "execute-silent":
|
||||
return actExecuteSilent
|
||||
case "execute-multi":
|
||||
return actExecuteMulti
|
||||
case "put":
|
||||
return actPut
|
||||
case "transform-border-label":
|
||||
return actTransformBorderLabel
|
||||
case "transform-preview-label":
|
||||
return actTransformPreviewLabel
|
||||
case "transform-prompt":
|
||||
return actTransformPrompt
|
||||
case "transform-query":
|
||||
return actTransformQuery
|
||||
}
|
||||
return actIgnore
|
||||
}
|
||||
@@ -1247,21 +1326,29 @@ func parseLayout(str string) layoutType {
|
||||
return layoutDefault
|
||||
}
|
||||
|
||||
func parseInfoStyle(str string) infoStyle {
|
||||
func parseInfoStyle(str string) (infoStyle, string) {
|
||||
switch str {
|
||||
case "default":
|
||||
return infoDefault
|
||||
return infoDefault, ""
|
||||
case "inline":
|
||||
return infoInline
|
||||
return infoInline, defaultInfoSep
|
||||
case "hidden":
|
||||
return infoHidden
|
||||
return infoHidden, ""
|
||||
default:
|
||||
errorExit("invalid info style (expected: default|inline|hidden)")
|
||||
prefix := "inline:"
|
||||
if strings.HasPrefix(str, prefix) {
|
||||
return infoInline, strings.ReplaceAll(str[len(prefix):], "\n", " ")
|
||||
}
|
||||
errorExit("invalid info style (expected: default|hidden|inline|inline:SEPARATOR)")
|
||||
}
|
||||
return infoDefault
|
||||
return infoDefault, ""
|
||||
}
|
||||
|
||||
func parsePreviewWindow(opts *previewOpts, input string) {
|
||||
parsePreviewWindowImpl(opts, input, errorExit)
|
||||
}
|
||||
|
||||
func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string)) {
|
||||
tokenRegex := regexp.MustCompile(`[:,]*(<([1-9][0-9]*)\(([^)<]+)\)|[^,:]+)`)
|
||||
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
|
||||
offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`)
|
||||
@@ -1333,7 +1420,8 @@ func parsePreviewWindow(opts *previewOpts, input string) {
|
||||
} else if offsetRegex.MatchString(token) {
|
||||
opts.scroll = token
|
||||
} else {
|
||||
errorExit("invalid preview window option: " + token)
|
||||
exit("invalid preview window option: " + token)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1342,7 +1430,7 @@ func parsePreviewWindow(opts *previewOpts, input string) {
|
||||
opts.alternative = &alternativeOpts
|
||||
opts.alternative.hidden = false
|
||||
opts.alternative.alternative = nil
|
||||
parsePreviewWindow(opts.alternative, alternative)
|
||||
parsePreviewWindowImpl(opts.alternative, alternative, exit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1443,7 +1531,7 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
case "--tiebreak":
|
||||
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
|
||||
case "--bind":
|
||||
parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"))
|
||||
parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"), errorExit)
|
||||
case "--color":
|
||||
_, spec := optionalNextString(allArgs, &i)
|
||||
if len(spec) == 0 {
|
||||
@@ -1521,12 +1609,13 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
case "--no-filepath-word":
|
||||
opts.FileWord = false
|
||||
case "--info":
|
||||
opts.InfoStyle = parseInfoStyle(
|
||||
opts.InfoStyle, opts.InfoSep = parseInfoStyle(
|
||||
nextString(allArgs, &i, "info style required"))
|
||||
case "--no-info":
|
||||
opts.InfoStyle = infoHidden
|
||||
case "--inline-info":
|
||||
opts.InfoStyle = infoInline
|
||||
opts.InfoSep = defaultInfoSep
|
||||
case "--no-inline-info":
|
||||
opts.InfoStyle = infoDefault
|
||||
case "--separator":
|
||||
@@ -1535,6 +1624,16 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
case "--no-separator":
|
||||
nosep := ""
|
||||
opts.Separator = &nosep
|
||||
case "--scrollbar":
|
||||
given, bar := optionalNextString(allArgs, &i)
|
||||
if given {
|
||||
opts.Scrollbar = &bar
|
||||
} else {
|
||||
opts.Scrollbar = nil
|
||||
}
|
||||
case "--no-scrollbar":
|
||||
noBar := ""
|
||||
opts.Scrollbar = &noBar
|
||||
case "--jump-labels":
|
||||
opts.JumpLabels = nextString(allArgs, &i, "label characters required")
|
||||
validateJumpLabels = true
|
||||
@@ -1563,10 +1662,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
case "--prompt":
|
||||
opts.Prompt = nextString(allArgs, &i, "prompt string required")
|
||||
case "--pointer":
|
||||
opts.Pointer = nextString(allArgs, &i, "pointer sign string required")
|
||||
opts.Pointer = firstLine(nextString(allArgs, &i, "pointer sign string required"))
|
||||
validatePointer = true
|
||||
case "--marker":
|
||||
opts.Marker = nextString(allArgs, &i, "selected sign string required")
|
||||
opts.Marker = firstLine(nextString(allArgs, &i, "selected sign string required"))
|
||||
validateMarker = true
|
||||
case "--sync":
|
||||
opts.Sync = true
|
||||
@@ -1645,6 +1744,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
|
||||
case "--tabstop":
|
||||
opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
|
||||
case "--listen":
|
||||
opts.ListenPort = nextInt(allArgs, &i, "listen port required")
|
||||
case "--no-listen":
|
||||
opts.ListenPort = 0
|
||||
case "--clear":
|
||||
opts.ClearOnExit = true
|
||||
case "--no-clear":
|
||||
@@ -1677,10 +1780,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
} else if match, value := optString(arg, "--prompt="); match {
|
||||
opts.Prompt = value
|
||||
} else if match, value := optString(arg, "--pointer="); match {
|
||||
opts.Pointer = value
|
||||
opts.Pointer = firstLine(value)
|
||||
validatePointer = true
|
||||
} else if match, value := optString(arg, "--marker="); match {
|
||||
opts.Marker = value
|
||||
opts.Marker = firstLine(value)
|
||||
validateMarker = true
|
||||
} else if match, value := optString(arg, "-n", "--nth="); match {
|
||||
opts.Nth = splitNth(value)
|
||||
@@ -1697,9 +1800,11 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
} else if match, value := optString(arg, "--layout="); match {
|
||||
opts.Layout = parseLayout(value)
|
||||
} else if match, value := optString(arg, "--info="); match {
|
||||
opts.InfoStyle = parseInfoStyle(value)
|
||||
opts.InfoStyle, opts.InfoSep = parseInfoStyle(value)
|
||||
} else if match, value := optString(arg, "--separator="); match {
|
||||
opts.Separator = &value
|
||||
} else if match, value := optString(arg, "--scrollbar="); match {
|
||||
opts.Scrollbar = &value
|
||||
} else if match, value := optString(arg, "--toggle-sort="); match {
|
||||
parseToggleSort(opts.Keymap, value)
|
||||
} else if match, value := optString(arg, "--expect="); match {
|
||||
@@ -1711,7 +1816,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, value)
|
||||
parseKeymap(opts.Keymap, value, errorExit)
|
||||
} else if match, value := optString(arg, "--history="); match {
|
||||
setHistory(value)
|
||||
} else if match, value := optString(arg, "--history-size="); match {
|
||||
@@ -1732,6 +1837,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.Padding = parseMargin("padding", value)
|
||||
} else if match, value := optString(arg, "--tabstop="); match {
|
||||
opts.Tabstop = atoi(value)
|
||||
} else if match, value := optString(arg, "--listen="); match {
|
||||
opts.ListenPort = atoi(value)
|
||||
} else if match, value := optString(arg, "--hscroll-off="); match {
|
||||
opts.HscrollOff = atoi(value)
|
||||
} else if match, value := optString(arg, "--scroll-off="); match {
|
||||
@@ -1761,6 +1868,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
errorExit("tab stop must be a positive integer")
|
||||
}
|
||||
|
||||
if opts.ListenPort < 0 || opts.ListenPort > 65535 {
|
||||
errorExit("invalid listen port")
|
||||
}
|
||||
|
||||
if len(opts.JumpLabels) == 0 {
|
||||
errorExit("empty jump labels")
|
||||
}
|
||||
@@ -1800,10 +1911,15 @@ func postProcessOptions(opts *Options) {
|
||||
if !opts.Version && !tui.IsLightRendererSupported() && opts.Height.size > 0 {
|
||||
errorExit("--height option is currently not supported on this platform")
|
||||
}
|
||||
|
||||
if opts.Scrollbar != nil && runewidth.StringWidth(*opts.Scrollbar) > 1 {
|
||||
errorExit("scrollbar display width should be 1")
|
||||
}
|
||||
|
||||
// Default actions for CTRL-N / CTRL-P when --history is set
|
||||
if opts.History != nil {
|
||||
if _, prs := opts.Keymap[tui.CtrlP.AsEvent()]; !prs {
|
||||
opts.Keymap[tui.CtrlP.AsEvent()] = toActions(actPreviousHistory)
|
||||
opts.Keymap[tui.CtrlP.AsEvent()] = toActions(actPrevHistory)
|
||||
}
|
||||
if _, prs := opts.Keymap[tui.CtrlN.AsEvent()]; !prs {
|
||||
opts.Keymap[tui.CtrlN.AsEvent()] = toActions(actNextHistory)
|
||||
@@ -1811,7 +1927,6 @@ func postProcessOptions(opts *Options) {
|
||||
}
|
||||
|
||||
// Extend the default key map
|
||||
previewEnabled := len(opts.Preview.command) > 0 || hasPreviewAction(opts)
|
||||
keymap := defaultKeymap()
|
||||
for key, actions := range opts.Keymap {
|
||||
var lastChangePreviewWindow *action
|
||||
@@ -1822,15 +1937,6 @@ func postProcessOptions(opts *Options) {
|
||||
opts.ToggleSort = true
|
||||
case actChangePreviewWindow:
|
||||
lastChangePreviewWindow = act
|
||||
if !previewEnabled {
|
||||
// Doesn't matter
|
||||
continue
|
||||
}
|
||||
opts := previewOpts{}
|
||||
for _, arg := range strings.Split(act.a, "|") {
|
||||
// Make sure that each expression is valid
|
||||
parsePreviewWindow(&opts, arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1851,6 +1957,11 @@ func postProcessOptions(opts *Options) {
|
||||
}
|
||||
opts.Keymap = keymap
|
||||
|
||||
// If 'double-click' is left unbound, bind it to the action bound to 'enter'
|
||||
if _, prs := opts.Keymap[tui.DoubleClick.AsEvent()]; !prs {
|
||||
opts.Keymap[tui.DoubleClick.AsEvent()] = opts.Keymap[tui.CtrlM.AsEvent()]
|
||||
}
|
||||
|
||||
if opts.Height.auto {
|
||||
for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} {
|
||||
if s.percent {
|
||||
|
@@ -262,13 +262,17 @@ func TestBind(t *testing.T) {
|
||||
}
|
||||
}
|
||||
check(tui.CtrlA.AsEvent(), "", actBeginningOfLine)
|
||||
errorString := ""
|
||||
errorFn := func(e string) {
|
||||
errorString = e
|
||||
}
|
||||
parseKeymap(keymap,
|
||||
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
|
||||
"f1:execute(ls {+})+abort+execute(echo {+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
|
||||
"f1:execute(ls {+})+abort+execute(echo \n{+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
|
||||
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
|
||||
"x:Execute(foo+bar),X:execute/bar+baz/"+
|
||||
",f1:+first,f1:+top"+
|
||||
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
|
||||
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up", errorFn)
|
||||
check(tui.CtrlA.AsEvent(), "", actKillLine)
|
||||
check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown)
|
||||
check(tui.Key('c'), "", actPageUp)
|
||||
@@ -286,12 +290,15 @@ func TestBind(t *testing.T) {
|
||||
check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute)
|
||||
|
||||
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
|
||||
parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
|
||||
parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char), errorFn)
|
||||
check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
|
||||
}
|
||||
|
||||
parseKeymap(keymap, "f1:abort")
|
||||
parseKeymap(keymap, "f1:abort", errorFn)
|
||||
check(tui.F1.AsEvent(), "", actAbort)
|
||||
if len(errorString) > 0 {
|
||||
t.Errorf("error parsing keymap: %s", errorString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestColorSpec(t *testing.T) {
|
||||
@@ -354,10 +361,10 @@ func TestDefaultCtrlNP(t *testing.T) {
|
||||
f.Close()
|
||||
hist := "--history=" + f.Name()
|
||||
check([]string{hist}, tui.CtrlN, actNextHistory)
|
||||
check([]string{hist}, tui.CtrlP, actPreviousHistory)
|
||||
check([]string{hist}, tui.CtrlP, actPrevHistory)
|
||||
|
||||
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-n:accept"}, tui.CtrlP, actPrevHistory)
|
||||
|
||||
check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlN, actNextHistory)
|
||||
check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
|
||||
@@ -466,3 +473,38 @@ func TestValidateSign(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSingleActionList(t *testing.T) {
|
||||
actions := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down", func(string) {})
|
||||
if len(actions) != 4 {
|
||||
t.Errorf("Invalid number of actions parsed:%d", len(actions))
|
||||
}
|
||||
if actions[0].t != actExecute || actions[0].a != "foo+bar,baz" {
|
||||
t.Errorf("Invalid action parsed: %v", actions[0])
|
||||
}
|
||||
if actions[1].t != actUp || actions[2].t != actUp {
|
||||
t.Errorf("Invalid action parsed: %v / %v", actions[1], actions[2])
|
||||
}
|
||||
if actions[3].t != actReload || actions[3].a != "down+down" {
|
||||
t.Errorf("Invalid action parsed: %v", actions[3])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSingleActionListError(t *testing.T) {
|
||||
err := ""
|
||||
parseSingleActionList("change-query(foobar)baz", func(e string) {
|
||||
err = e
|
||||
})
|
||||
if len(err) == 0 {
|
||||
t.Errorf("Failed to detect error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaskActionContents(t *testing.T) {
|
||||
original := ":execute((f)(o)(o)(b)(a)(r))+change-query@qu@ry@+up,x:reload:hello:world"
|
||||
expected := ":execute +change-query +up,x:reload "
|
||||
masked := maskActionContents(original)
|
||||
if masked != expected {
|
||||
t.Errorf("Not masked: %s", masked)
|
||||
}
|
||||
}
|
||||
|
126
src/server.go
Normal file
126
src/server.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
crlf = "\r\n"
|
||||
httpOk = "HTTP/1.1 200 OK" + crlf
|
||||
httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf
|
||||
httpReadTimeout = 10 * time.Second
|
||||
maxContentLength = 1024 * 1024
|
||||
)
|
||||
|
||||
func startHttpServer(port int, channel chan []*action) error {
|
||||
if port == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("port not available: %d", port)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
break
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
conn.Write([]byte(handleHttpRequest(conn, channel)))
|
||||
conn.Close()
|
||||
}
|
||||
listener.Close()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Here we are writing a simplistic HTTP server without using net/http
|
||||
// package to reduce the size of the binary.
|
||||
//
|
||||
// * No --listen: 2.8MB
|
||||
// * --listen with net/http: 5.7MB
|
||||
// * --listen w/o net/http: 3.3MB
|
||||
func handleHttpRequest(conn net.Conn, channel chan []*action) string {
|
||||
contentLength := 0
|
||||
body := ""
|
||||
bad := func(message string) string {
|
||||
message += "\n"
|
||||
return httpBadRequest + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
|
||||
}
|
||||
conn.SetReadDeadline(time.Now().Add(httpReadTimeout))
|
||||
scanner := bufio.NewScanner(conn)
|
||||
scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) {
|
||||
found := bytes.Index(data, []byte(crlf))
|
||||
if found >= 0 {
|
||||
token := data[:found+len(crlf)]
|
||||
return len(token), token, nil
|
||||
}
|
||||
if atEOF || len(body)+len(data) >= contentLength {
|
||||
return 0, data, bufio.ErrFinalToken
|
||||
}
|
||||
return 0, nil, nil
|
||||
})
|
||||
|
||||
section := 0
|
||||
for scanner.Scan() {
|
||||
text := scanner.Text()
|
||||
switch section {
|
||||
case 0:
|
||||
if !strings.HasPrefix(text, "POST / HTTP") {
|
||||
return bad("invalid request method")
|
||||
}
|
||||
section++
|
||||
case 1:
|
||||
if text == crlf {
|
||||
if contentLength == 0 {
|
||||
return bad("content-length header missing")
|
||||
}
|
||||
section++
|
||||
continue
|
||||
}
|
||||
pair := strings.SplitN(text, ":", 2)
|
||||
if len(pair) == 2 && strings.ToLower(pair[0]) == "content-length" {
|
||||
length, err := strconv.Atoi(strings.TrimSpace(pair[1]))
|
||||
if err != nil || length <= 0 || length > maxContentLength {
|
||||
return bad("invalid content length")
|
||||
}
|
||||
contentLength = length
|
||||
}
|
||||
case 2:
|
||||
body += text
|
||||
}
|
||||
}
|
||||
|
||||
if len(body) < contentLength {
|
||||
return bad("incomplete request")
|
||||
}
|
||||
body = body[:contentLength]
|
||||
|
||||
errorMessage := ""
|
||||
actions := parseSingleActionList(strings.Trim(string(body), "\r\n"), func(message string) {
|
||||
errorMessage = message
|
||||
})
|
||||
if len(errorMessage) > 0 {
|
||||
return bad(errorMessage)
|
||||
}
|
||||
if len(actions) == 0 {
|
||||
return bad("no action specified")
|
||||
}
|
||||
|
||||
channel <- actions
|
||||
return httpOk
|
||||
}
|
792
src/terminal.go
792
src/terminal.go
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,8 @@ func HasFullscreenRenderer() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var DefaultBorderShape BorderShape = BorderRounded
|
||||
|
||||
func (a Attr) Merge(b Attr) Attr {
|
||||
return a | b
|
||||
}
|
||||
@@ -32,6 +34,7 @@ func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
|
||||
func (r *FullscreenRenderer) Pause(bool) {}
|
||||
func (r *FullscreenRenderer) Resume(bool, bool) {}
|
||||
func (r *FullscreenRenderer) Clear() {}
|
||||
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
|
||||
func (r *FullscreenRenderer) Refresh() {}
|
||||
func (r *FullscreenRenderer) Close() {}
|
||||
|
||||
|
103
src/tui/light.go
103
src/tui/light.go
@@ -72,7 +72,7 @@ type LightRenderer struct {
|
||||
forceBlack bool
|
||||
clearOnExit bool
|
||||
prevDownTime time.Time
|
||||
clickY []int
|
||||
clicks [][2]int
|
||||
ttyin *os.File
|
||||
buffer []byte
|
||||
origState *term.State
|
||||
@@ -174,10 +174,7 @@ func (r *LightRenderer) Init() {
|
||||
}
|
||||
}
|
||||
|
||||
if r.mouse {
|
||||
r.csi("?1000h")
|
||||
r.csi("?1006h")
|
||||
}
|
||||
r.enableMouse()
|
||||
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
|
||||
r.csi("G")
|
||||
r.csi("K")
|
||||
@@ -569,25 +566,31 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
|
||||
// ctrl := t & 0b1000
|
||||
mod := t&0b1100 > 0
|
||||
|
||||
drag := t&0b100000 > 0
|
||||
|
||||
if scroll != 0 {
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, mod}}
|
||||
}
|
||||
|
||||
double := false
|
||||
if down {
|
||||
if down && !drag {
|
||||
now := time.Now()
|
||||
if !left { // Right double click is not allowed
|
||||
r.clickY = []int{}
|
||||
r.clicks = [][2]int{}
|
||||
} else if now.Sub(r.prevDownTime) < doubleClickDuration {
|
||||
r.clickY = append(r.clickY, y)
|
||||
r.clicks = append(r.clicks, [2]int{x, y})
|
||||
} else {
|
||||
r.clickY = []int{y}
|
||||
r.clicks = [][2]int{{x, y}}
|
||||
}
|
||||
r.prevDownTime = now
|
||||
} else {
|
||||
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
|
||||
n := len(r.clicks)
|
||||
if len(r.clicks) > 1 && r.clicks[n-2][0] == r.clicks[n-1][0] && r.clicks[n-2][1] == r.clicks[n-1][1] &&
|
||||
time.Since(r.prevDownTime) < doubleClickDuration {
|
||||
double = true
|
||||
if double {
|
||||
r.clicks = [][2]int{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
|
||||
@@ -602,6 +605,7 @@ func (r *LightRenderer) rmcup() {
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Pause(clear bool) {
|
||||
r.disableMouse()
|
||||
r.restoreTerminal()
|
||||
if clear {
|
||||
if r.fullscreen {
|
||||
@@ -614,6 +618,22 @@ func (r *LightRenderer) Pause(clear bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) enableMouse() {
|
||||
if r.mouse {
|
||||
r.csi("?1000h")
|
||||
r.csi("?1002h")
|
||||
r.csi("?1006h")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) disableMouse() {
|
||||
if r.mouse {
|
||||
r.csi("?1000l")
|
||||
r.csi("?1002l")
|
||||
r.csi("?1006l")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Resume(clear bool, sigcont bool) {
|
||||
r.setupTerminal()
|
||||
if clear {
|
||||
@@ -622,13 +642,13 @@ func (r *LightRenderer) Resume(clear bool, sigcont bool) {
|
||||
} else {
|
||||
r.rmcup()
|
||||
}
|
||||
r.enableMouse()
|
||||
r.flush()
|
||||
} else if sigcont && !r.fullscreen && r.mouse {
|
||||
// NOTE: SIGCONT (Coming back from CTRL-Z):
|
||||
// It's highly likely that the offset we obtained at the beginning is
|
||||
// no longer correct, so we simply disable mouse input.
|
||||
r.csi("?1000l")
|
||||
r.csi("?1006l")
|
||||
r.disableMouse()
|
||||
r.mouse = false
|
||||
}
|
||||
}
|
||||
@@ -643,6 +663,10 @@ func (r *LightRenderer) Clear() {
|
||||
r.flush()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) NeedScrollbarRedraw() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *LightRenderer) RefreshWindows(windows []Window) {
|
||||
r.flush()
|
||||
}
|
||||
@@ -666,10 +690,7 @@ func (r *LightRenderer) Close() {
|
||||
} else if !r.fullscreen {
|
||||
r.csi("u")
|
||||
}
|
||||
if r.mouse {
|
||||
r.csi("?1000l")
|
||||
r.csi("?1006l")
|
||||
}
|
||||
r.disableMouse()
|
||||
r.flush()
|
||||
r.closePlatform()
|
||||
r.restoreTerminal()
|
||||
@@ -706,25 +727,38 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
|
||||
w.fg = r.theme.Fg.Color
|
||||
w.bg = r.theme.Bg.Color
|
||||
}
|
||||
w.drawBorder()
|
||||
w.drawBorder(false)
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorder() {
|
||||
func (w *LightWindow) DrawHBorder() {
|
||||
w.drawBorder(true)
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorder(onlyHorizontal bool) {
|
||||
switch w.border.shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble:
|
||||
w.drawBorderAround()
|
||||
w.drawBorderAround(onlyHorizontal)
|
||||
case BorderHorizontal:
|
||||
w.drawBorderHorizontal(true, true)
|
||||
case BorderVertical:
|
||||
if onlyHorizontal {
|
||||
return
|
||||
}
|
||||
w.drawBorderVertical(true, true)
|
||||
case BorderTop:
|
||||
w.drawBorderHorizontal(true, false)
|
||||
case BorderBottom:
|
||||
w.drawBorderHorizontal(false, true)
|
||||
case BorderLeft:
|
||||
if onlyHorizontal {
|
||||
return
|
||||
}
|
||||
w.drawBorderVertical(true, false)
|
||||
case BorderRight:
|
||||
if onlyHorizontal {
|
||||
return
|
||||
}
|
||||
w.drawBorderVertical(false, true)
|
||||
}
|
||||
}
|
||||
@@ -734,13 +768,14 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
|
||||
if w.preview {
|
||||
color = ColPreviewBorder
|
||||
}
|
||||
hw := runewidth.RuneWidth(w.border.horizontal)
|
||||
if top {
|
||||
w.Move(0, 0)
|
||||
w.CPrint(color, repeat(w.border.horizontal, w.width))
|
||||
w.CPrint(color, repeat(w.border.horizontal, w.width/hw))
|
||||
}
|
||||
if bottom {
|
||||
w.Move(w.height-1, 0)
|
||||
w.CPrint(color, repeat(w.border.horizontal, w.width))
|
||||
w.CPrint(color, repeat(w.border.horizontal, w.width/hw))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,21 +800,29 @@ func (w *LightWindow) drawBorderVertical(left, right bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorderAround() {
|
||||
func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
|
||||
w.Move(0, 0)
|
||||
color := ColBorder
|
||||
if w.preview {
|
||||
color = ColPreviewBorder
|
||||
}
|
||||
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.topRight))
|
||||
for y := 1; y < w.height-1; y++ {
|
||||
w.Move(y, 0)
|
||||
w.CPrint(color, string(w.border.vertical))
|
||||
w.CPrint(color, repeat(' ', w.width-2))
|
||||
w.CPrint(color, string(w.border.vertical))
|
||||
hw := runewidth.RuneWidth(w.border.horizontal)
|
||||
tcw := runewidth.RuneWidth(w.border.topLeft) + runewidth.RuneWidth(w.border.topRight)
|
||||
bcw := runewidth.RuneWidth(w.border.bottomLeft) + runewidth.RuneWidth(w.border.bottomRight)
|
||||
rem := (w.width - tcw) % hw
|
||||
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight))
|
||||
if !onlyHorizontal {
|
||||
vw := runewidth.RuneWidth(w.border.vertical)
|
||||
for y := 1; y < w.height-1; y++ {
|
||||
w.Move(y, 0)
|
||||
w.CPrint(color, string(w.border.vertical))
|
||||
w.CPrint(color, repeat(' ', w.width-vw*2))
|
||||
w.CPrint(color, string(w.border.vertical))
|
||||
}
|
||||
}
|
||||
w.Move(w.height-1, 0)
|
||||
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.bottomRight))
|
||||
rem = (w.width - bcw) % hw
|
||||
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight))
|
||||
}
|
||||
|
||||
func (w *LightWindow) csi(code string) {
|
||||
@@ -1020,7 +1063,7 @@ func (w *LightWindow) FinishFill() {
|
||||
}
|
||||
|
||||
func (w *LightWindow) Erase() {
|
||||
w.drawBorder()
|
||||
w.drawBorder(false)
|
||||
// We don't erase the window here to avoid flickering during scroll
|
||||
w.Move(0, 0)
|
||||
}
|
||||
|
119
src/tui/tcell.go
119
src/tui/tcell.go
@@ -6,8 +6,6 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"runtime"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/gdamore/tcell/v2/encoding"
|
||||
|
||||
@@ -19,6 +17,8 @@ func HasFullscreenRenderer() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var DefaultBorderShape BorderShape = BorderSharp
|
||||
|
||||
func asTcellColor(color Color) tcell.Color {
|
||||
if color == colDefault {
|
||||
return tcell.ColorDefault
|
||||
@@ -189,6 +189,10 @@ func (r *FullscreenRenderer) Clear() {
|
||||
_screen.Clear()
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Refresh() {
|
||||
// noop
|
||||
}
|
||||
@@ -218,54 +222,38 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
|
||||
case button&tcell.WheelUp != 0:
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
|
||||
case button&tcell.Button1 != 0 && !drag:
|
||||
// all potential double click events put their 'line' coordinate in the clickY array
|
||||
// double click event has two conditions, temporal and spatial, the first is checked here
|
||||
now := time.Now()
|
||||
if now.Sub(r.prevDownTime) < doubleClickDuration {
|
||||
r.clickY = append(r.clickY, y)
|
||||
} else {
|
||||
r.clickY = []int{y}
|
||||
}
|
||||
r.prevDownTime = now
|
||||
case button&tcell.Button1 != 0:
|
||||
double := false
|
||||
if !drag {
|
||||
// all potential double click events put their coordinates in the clicks array
|
||||
// double click event has two conditions, temporal and spatial, the first is checked here
|
||||
now := time.Now()
|
||||
if now.Sub(r.prevDownTime) < doubleClickDuration {
|
||||
r.clicks = append(r.clicks, [2]int{x, y})
|
||||
} else {
|
||||
r.clicks = [][2]int{{x, y}}
|
||||
}
|
||||
r.prevDownTime = now
|
||||
|
||||
// detect double clicks (also check for spatial condition)
|
||||
n := len(r.clickY)
|
||||
double := n > 1 && r.clickY[n-2] == r.clickY[n-1]
|
||||
if double {
|
||||
// make sure two consecutive double clicks require four clicks
|
||||
r.clickY = []int{}
|
||||
// detect double clicks (also check for spatial condition)
|
||||
n := len(r.clicks)
|
||||
double = n > 1 && r.clicks[n-2][0] == r.clicks[n-1][0] && r.clicks[n-2][1] == r.clicks[n-1][1]
|
||||
if double {
|
||||
// make sure two consecutive double clicks require four clicks
|
||||
r.clicks = [][2]int{}
|
||||
}
|
||||
}
|
||||
|
||||
// fire single or double click event
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}}
|
||||
case button&tcell.Button2 != 0 && !drag:
|
||||
case button&tcell.Button2 != 0:
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}}
|
||||
case runtime.GOOS != "windows":
|
||||
|
||||
default:
|
||||
// double and single taps on Windows don't quite work due to
|
||||
// the console acting on the events and not allowing us
|
||||
// to consume them.
|
||||
|
||||
left := button&tcell.Button1 != 0
|
||||
down := left || button&tcell.Button3 != 0
|
||||
double := false
|
||||
if down {
|
||||
now := time.Now()
|
||||
if !left {
|
||||
r.clickY = []int{}
|
||||
} else if now.Sub(r.prevDownTime) < doubleClickDuration {
|
||||
r.clickY = append(r.clickY, x)
|
||||
} else {
|
||||
r.clickY = []int{x}
|
||||
r.prevDownTime = now
|
||||
}
|
||||
} else {
|
||||
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
|
||||
time.Now().Sub(r.prevDownTime) < doubleClickDuration {
|
||||
double = true
|
||||
}
|
||||
}
|
||||
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
|
||||
}
|
||||
@@ -524,7 +512,7 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
|
||||
height: height,
|
||||
normal: normal,
|
||||
borderStyle: borderStyle}
|
||||
w.drawBorder()
|
||||
w.drawBorder(false)
|
||||
return w
|
||||
}
|
||||
|
||||
@@ -541,7 +529,7 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Erase() {
|
||||
fill(w.left-1, w.top, w.width+1, w.height, w.normal, ' ')
|
||||
fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ')
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Enclose(y int, x int) bool {
|
||||
@@ -682,7 +670,11 @@ func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
|
||||
return w.fillString(str, NewColorPair(fg, bg, a))
|
||||
}
|
||||
|
||||
func (w *TcellWindow) drawBorder() {
|
||||
func (w *TcellWindow) DrawHBorder() {
|
||||
w.drawBorder(true)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
||||
shape := w.borderStyle.shape
|
||||
if shape == BorderNone {
|
||||
return
|
||||
@@ -704,35 +696,52 @@ func (w *TcellWindow) drawBorder() {
|
||||
style = w.normal.style()
|
||||
}
|
||||
|
||||
hw := runewidth.RuneWidth(w.borderStyle.horizontal)
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderHorizontal, BorderTop:
|
||||
for x := left; x < right; x++ {
|
||||
max := right - 2*hw
|
||||
if shape == BorderHorizontal || shape == BorderTop {
|
||||
max = right - hw
|
||||
}
|
||||
// tcell has an issue displaying two overlapping wide runes
|
||||
// e.g. SetContent( HH )
|
||||
// SetContent( TR )
|
||||
// ==================
|
||||
// ( HH ) => TR is ignored
|
||||
for x := left; x <= max; x += hw {
|
||||
_screen.SetContent(x, top, w.borderStyle.horizontal, nil, style)
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderHorizontal, BorderBottom:
|
||||
for x := left; x < right; x++ {
|
||||
max := right - 2*hw
|
||||
if shape == BorderHorizontal || shape == BorderBottom {
|
||||
max = right - hw
|
||||
}
|
||||
for x := left; x <= max; x += hw {
|
||||
_screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style)
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderLeft:
|
||||
for y := top; y < bot; y++ {
|
||||
_screen.SetContent(left, y, w.borderStyle.vertical, nil, style)
|
||||
if !onlyHorizontal {
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderLeft:
|
||||
for y := top; y < bot; y++ {
|
||||
_screen.SetContent(left, y, w.borderStyle.vertical, nil, style)
|
||||
}
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderRight:
|
||||
for y := top; y < bot; y++ {
|
||||
_screen.SetContent(right-1, y, w.borderStyle.vertical, nil, style)
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderRight:
|
||||
vw := runewidth.RuneWidth(w.borderStyle.vertical)
|
||||
for y := top; y < bot; y++ {
|
||||
_screen.SetContent(right-vw, y, w.borderStyle.vertical, nil, style)
|
||||
}
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble:
|
||||
_screen.SetContent(left, top, w.borderStyle.topLeft, nil, style)
|
||||
_screen.SetContent(right-1, top, w.borderStyle.topRight, nil, style)
|
||||
_screen.SetContent(right-runewidth.RuneWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style)
|
||||
_screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style)
|
||||
_screen.SetContent(right-1, bot-1, w.borderStyle.bottomRight, nil, style)
|
||||
_screen.SetContent(right-runewidth.RuneWidth(w.borderStyle.bottomRight), bot-1, w.borderStyle.bottomRight, nil, style)
|
||||
}
|
||||
}
|
||||
|
104
src/tui/tui.go
104
src/tui/tui.go
@@ -91,6 +91,8 @@ const (
|
||||
Change
|
||||
BackwardEOF
|
||||
Start
|
||||
Load
|
||||
Focus
|
||||
|
||||
AltBS
|
||||
|
||||
@@ -268,8 +270,10 @@ type ColorTheme struct {
|
||||
Selected ColorAttr
|
||||
Header ColorAttr
|
||||
Separator ColorAttr
|
||||
Scrollbar ColorAttr
|
||||
Border ColorAttr
|
||||
BorderLabel ColorAttr
|
||||
PreviewLabel ColorAttr
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
@@ -304,6 +308,22 @@ const (
|
||||
BorderRight
|
||||
)
|
||||
|
||||
func (s BorderShape) HasRight() bool {
|
||||
switch s {
|
||||
case BorderNone, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s BorderShape) HasTop() bool {
|
||||
switch s {
|
||||
case BorderNone, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type BorderStyle struct {
|
||||
shape BorderShape
|
||||
horizontal rune
|
||||
@@ -391,6 +411,7 @@ type Renderer interface {
|
||||
RefreshWindows(windows []Window)
|
||||
Refresh()
|
||||
Close()
|
||||
NeedScrollbarRedraw() bool
|
||||
|
||||
GetChar() Event
|
||||
|
||||
@@ -406,6 +427,7 @@ type Window interface {
|
||||
Width() int
|
||||
Height() int
|
||||
|
||||
DrawHBorder()
|
||||
Refresh()
|
||||
FinishFill()
|
||||
Close()
|
||||
@@ -428,7 +450,7 @@ type FullscreenRenderer struct {
|
||||
mouse bool
|
||||
forceBlack bool
|
||||
prevDownTime time.Time
|
||||
clickY []int
|
||||
clicks [][2]int
|
||||
}
|
||||
|
||||
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
|
||||
@@ -437,7 +459,7 @@ func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Rende
|
||||
mouse: mouse,
|
||||
forceBlack: forceBlack,
|
||||
prevDownTime: time.Unix(0, 0),
|
||||
clickY: []int{}}
|
||||
clicks: [][2]int{}}
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -464,23 +486,21 @@ var (
|
||||
ColInfo ColorPair
|
||||
ColHeader ColorPair
|
||||
ColSeparator ColorPair
|
||||
ColScrollbar ColorPair
|
||||
ColBorder ColorPair
|
||||
ColPreview ColorPair
|
||||
ColPreviewBorder ColorPair
|
||||
ColBorderLabel ColorPair
|
||||
ColPreviewLabel ColorPair
|
||||
)
|
||||
|
||||
func EmptyTheme() *ColorTheme {
|
||||
return &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colUndefined, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
Fg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Bg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
Prompt: ColorAttr{colUndefined, AttrUndefined},
|
||||
Match: ColorAttr{colUndefined, AttrUndefined},
|
||||
Current: ColorAttr{colUndefined, AttrUndefined},
|
||||
@@ -490,9 +510,15 @@ func EmptyTheme() *ColorTheme {
|
||||
Cursor: ColorAttr{colUndefined, AttrUndefined},
|
||||
Selected: ColorAttr{colUndefined, AttrUndefined},
|
||||
Header: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Border: ColorAttr{colUndefined, AttrUndefined},
|
||||
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,13 +526,9 @@ func NoColorTheme() *ColorTheme {
|
||||
return &ColorTheme{
|
||||
Colored: false,
|
||||
Input: ColorAttr{colDefault, AttrRegular},
|
||||
Disabled: ColorAttr{colDefault, AttrRegular},
|
||||
Fg: ColorAttr{colDefault, AttrRegular},
|
||||
Bg: ColorAttr{colDefault, AttrRegular},
|
||||
PreviewFg: ColorAttr{colDefault, AttrRegular},
|
||||
PreviewBg: ColorAttr{colDefault, AttrRegular},
|
||||
DarkBg: ColorAttr{colDefault, AttrRegular},
|
||||
Gutter: ColorAttr{colDefault, AttrRegular},
|
||||
Prompt: ColorAttr{colDefault, AttrRegular},
|
||||
Match: ColorAttr{colDefault, Underline},
|
||||
Current: ColorAttr{colDefault, Reverse},
|
||||
@@ -516,9 +538,15 @@ func NoColorTheme() *ColorTheme {
|
||||
Cursor: ColorAttr{colDefault, AttrRegular},
|
||||
Selected: ColorAttr{colDefault, AttrRegular},
|
||||
Header: ColorAttr{colDefault, AttrRegular},
|
||||
Separator: ColorAttr{colDefault, AttrRegular},
|
||||
Border: ColorAttr{colDefault, AttrRegular},
|
||||
BorderLabel: ColorAttr{colDefault, AttrRegular},
|
||||
Disabled: ColorAttr{colDefault, AttrRegular},
|
||||
PreviewFg: ColorAttr{colDefault, AttrRegular},
|
||||
PreviewBg: ColorAttr{colDefault, AttrRegular},
|
||||
Gutter: ColorAttr{colDefault, AttrRegular},
|
||||
PreviewLabel: ColorAttr{colDefault, AttrRegular},
|
||||
Separator: ColorAttr{colDefault, AttrRegular},
|
||||
Scrollbar: ColorAttr{colDefault, AttrRegular},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,13 +559,9 @@ func init() {
|
||||
Default16 = &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{colBlack, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
Prompt: ColorAttr{colBlue, AttrUndefined},
|
||||
Match: ColorAttr{colGreen, AttrUndefined},
|
||||
Current: ColorAttr{colYellow, AttrUndefined},
|
||||
@@ -547,20 +571,22 @@ func init() {
|
||||
Cursor: ColorAttr{colRed, AttrUndefined},
|
||||
Selected: ColorAttr{colMagenta, AttrUndefined},
|
||||
Header: ColorAttr{colCyan, AttrUndefined},
|
||||
Separator: ColorAttr{colBlack, AttrUndefined},
|
||||
Border: ColorAttr{colBlack, AttrUndefined},
|
||||
BorderLabel: ColorAttr{colWhite, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
}
|
||||
Dark256 = &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{236, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
Prompt: ColorAttr{110, AttrUndefined},
|
||||
Match: ColorAttr{108, AttrUndefined},
|
||||
Current: ColorAttr{254, AttrUndefined},
|
||||
@@ -570,20 +596,22 @@ func init() {
|
||||
Cursor: ColorAttr{161, AttrUndefined},
|
||||
Selected: ColorAttr{168, AttrUndefined},
|
||||
Header: ColorAttr{109, AttrUndefined},
|
||||
Separator: ColorAttr{59, AttrUndefined},
|
||||
Border: ColorAttr{59, AttrUndefined},
|
||||
BorderLabel: ColorAttr{145, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
}
|
||||
Light256 = &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{251, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
Prompt: ColorAttr{25, AttrUndefined},
|
||||
Match: ColorAttr{66, AttrUndefined},
|
||||
Current: ColorAttr{237, AttrUndefined},
|
||||
@@ -593,9 +621,15 @@ func init() {
|
||||
Cursor: ColorAttr{161, AttrUndefined},
|
||||
Selected: ColorAttr{168, AttrUndefined},
|
||||
Header: ColorAttr{31, AttrUndefined},
|
||||
Separator: ColorAttr{145, AttrUndefined},
|
||||
Border: ColorAttr{145, AttrUndefined},
|
||||
BorderLabel: ColorAttr{59, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,13 +649,9 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
|
||||
return c
|
||||
}
|
||||
theme.Input = o(baseTheme.Input, theme.Input)
|
||||
theme.Disabled = o(theme.Input, o(baseTheme.Disabled, theme.Disabled))
|
||||
theme.Fg = o(baseTheme.Fg, theme.Fg)
|
||||
theme.Bg = o(baseTheme.Bg, theme.Bg)
|
||||
theme.PreviewFg = o(theme.Fg, o(baseTheme.PreviewFg, theme.PreviewFg))
|
||||
theme.PreviewBg = o(theme.Bg, o(baseTheme.PreviewBg, theme.PreviewBg))
|
||||
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
|
||||
theme.Gutter = o(theme.DarkBg, o(baseTheme.Gutter, theme.Gutter))
|
||||
theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
|
||||
theme.Match = o(baseTheme.Match, theme.Match)
|
||||
theme.Current = o(baseTheme.Current, theme.Current)
|
||||
@@ -631,10 +661,18 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
|
||||
theme.Cursor = o(baseTheme.Cursor, theme.Cursor)
|
||||
theme.Selected = o(baseTheme.Selected, theme.Selected)
|
||||
theme.Header = o(baseTheme.Header, theme.Header)
|
||||
theme.Separator = o(baseTheme.Separator, theme.Separator)
|
||||
theme.Border = o(baseTheme.Border, theme.Border)
|
||||
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel)
|
||||
|
||||
// These colors are not defined in the base themes
|
||||
theme.Disabled = o(theme.Input, theme.Disabled)
|
||||
theme.Gutter = o(theme.DarkBg, theme.Gutter)
|
||||
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
|
||||
theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
|
||||
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
|
||||
theme.Separator = o(theme.Border, theme.Separator)
|
||||
theme.Scrollbar = o(theme.Border, theme.Scrollbar)
|
||||
|
||||
initPalette(theme)
|
||||
}
|
||||
|
||||
@@ -666,8 +704,10 @@ func initPalette(theme *ColorTheme) {
|
||||
ColInfo = pair(theme.Info, theme.Bg)
|
||||
ColHeader = pair(theme.Header, theme.Bg)
|
||||
ColSeparator = pair(theme.Separator, theme.Bg)
|
||||
ColScrollbar = pair(theme.Scrollbar, theme.Bg)
|
||||
ColBorder = pair(theme.Border, theme.Bg)
|
||||
ColBorderLabel = pair(theme.BorderLabel, theme.Bg)
|
||||
ColPreviewLabel = pair(theme.PreviewLabel, theme.PreviewBg)
|
||||
ColPreview = pair(theme.PreviewFg, theme.PreviewBg)
|
||||
ColPreviewBorder = pair(theme.Border, theme.PreviewBg)
|
||||
}
|
||||
|
@@ -9,7 +9,6 @@ const (
|
||||
EvtSearchNew
|
||||
EvtSearchProgress
|
||||
EvtSearchFin
|
||||
EvtClose
|
||||
)
|
||||
|
||||
func TestEventBox(t *testing.T) {
|
||||
|
@@ -1,10 +1,72 @@
|
||||
package util
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMax(t *testing.T) {
|
||||
if Max(10, 1) != 10 {
|
||||
t.Error("Expected", 10)
|
||||
}
|
||||
if Max(-2, 5) != 5 {
|
||||
t.Error("Invalid result")
|
||||
t.Error("Expected", 5)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMax16(t *testing.T) {
|
||||
if Max16(10, 1) != 10 {
|
||||
t.Error("Expected", 10)
|
||||
}
|
||||
if Max16(-2, 5) != 5 {
|
||||
t.Error("Expected", 5)
|
||||
}
|
||||
if Max16(math.MaxInt16, 0) != math.MaxInt16 {
|
||||
t.Error("Expected", math.MaxInt16)
|
||||
}
|
||||
if Max16(0, math.MinInt16) != 0 {
|
||||
t.Error("Expected", 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMax32(t *testing.T) {
|
||||
if Max32(10, 1) != 10 {
|
||||
t.Error("Expected", 10)
|
||||
}
|
||||
if Max32(-2, 5) != 5 {
|
||||
t.Error("Expected", 5)
|
||||
}
|
||||
if Max32(math.MaxInt32, 0) != math.MaxInt32 {
|
||||
t.Error("Expected", math.MaxInt32)
|
||||
}
|
||||
if Max32(0, math.MinInt32) != 0 {
|
||||
t.Error("Expected", 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMin(t *testing.T) {
|
||||
if Min(10, 1) != 1 {
|
||||
t.Error("Expected", 1)
|
||||
}
|
||||
if Min(-2, 5) != -2 {
|
||||
t.Error("Expected", -2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMin32(t *testing.T) {
|
||||
if Min32(10, 1) != 1 {
|
||||
t.Error("Expected", 1)
|
||||
}
|
||||
if Min32(-2, 5) != -2 {
|
||||
t.Error("Expected", -2)
|
||||
}
|
||||
if Min32(math.MaxInt32, 0) != 0 {
|
||||
t.Error("Expected", 0)
|
||||
}
|
||||
if Min32(0, math.MinInt32) != math.MinInt32 {
|
||||
t.Error("Expected", math.MinInt32)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +83,55 @@ func TestContrain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContrain32(t *testing.T) {
|
||||
if Constrain32(-3, -1, 3) != -1 {
|
||||
t.Error("Expected", -1)
|
||||
}
|
||||
if Constrain32(2, -1, 3) != 2 {
|
||||
t.Error("Expected", 2)
|
||||
}
|
||||
|
||||
if Constrain32(5, -1, 3) != 3 {
|
||||
t.Error("Expected", 3)
|
||||
}
|
||||
if Constrain32(0, math.MinInt32, math.MaxInt32) != 0 {
|
||||
t.Error("Expected", 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsUint16(t *testing.T) {
|
||||
if AsUint16(5) != 5 {
|
||||
t.Error("Expected", 5)
|
||||
}
|
||||
if AsUint16(-10) != 0 {
|
||||
t.Error("Expected", 0)
|
||||
}
|
||||
if AsUint16(math.MaxUint16) != math.MaxUint16 {
|
||||
t.Error("Expected", math.MaxUint16)
|
||||
}
|
||||
if AsUint16(math.MinInt32) != 0 {
|
||||
t.Error("Expected", 0)
|
||||
}
|
||||
if AsUint16(math.MinInt16) != 0 {
|
||||
t.Error("Expected", 0)
|
||||
}
|
||||
if AsUint16(math.MaxUint16+1) != math.MaxUint16 {
|
||||
t.Error("Expected", math.MaxUint16)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDurWithIn(t *testing.T) {
|
||||
if DurWithin(time.Duration(5), time.Duration(1), time.Duration(8)) != time.Duration(5) {
|
||||
t.Error("Expected", time.Duration(0))
|
||||
}
|
||||
if DurWithin(time.Duration(0)*time.Second, time.Second, time.Duration(3)*time.Second) != time.Second {
|
||||
t.Error("Expected", time.Second)
|
||||
}
|
||||
if DurWithin(time.Duration(10)*time.Second, time.Duration(0), time.Second) != time.Second {
|
||||
t.Error("Expected", time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnce(t *testing.T) {
|
||||
o := Once(false)
|
||||
if o() {
|
||||
@@ -64,3 +175,12 @@ func TestTruncate(t *testing.T) {
|
||||
t.Errorf("Expected: 6, actual: %d", width)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepeatToFill(t *testing.T) {
|
||||
if RepeatToFill("abcde", 10, 50) != strings.Repeat("abcde", 5) {
|
||||
t.Error("Expected:", strings.Repeat("abcde", 5))
|
||||
}
|
||||
if RepeatToFill("abcde", 10, 42) != strings.Repeat("abcde", 4)+"abcde"[:2] {
|
||||
t.Error("Expected:", strings.Repeat("abcde", 4)+"abcde"[:2])
|
||||
}
|
||||
}
|
||||
|
246
test/test_go.rb
246
test/test_go.rb
@@ -7,6 +7,7 @@ require 'English'
|
||||
require 'shellwords'
|
||||
require 'erb'
|
||||
require 'tempfile'
|
||||
require 'net/http'
|
||||
|
||||
TEMPLATE = DATA.read
|
||||
UNSETS = %w[
|
||||
@@ -22,7 +23,7 @@ DEFAULT_TIMEOUT = 10
|
||||
FILE = File.expand_path(__FILE__)
|
||||
BASE = File.expand_path('..', __dir__)
|
||||
Dir.chdir(BASE)
|
||||
FZF = "FZF_DEFAULT_OPTS= FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf"
|
||||
FZF = "FZF_DEFAULT_OPTS=--no-scrollbar FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf"
|
||||
|
||||
def wait
|
||||
since = Time.now
|
||||
@@ -64,7 +65,7 @@ class Shell
|
||||
end
|
||||
|
||||
def fish
|
||||
UNSETS.map { |v| v + '= ' }.join + 'fish'
|
||||
UNSETS.map { |v| v + '= ' }.join + ' FZF_DEFAULT_OPTS=--no-scrollbar fish'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -921,7 +922,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
wait do
|
||||
assert_path_exists history_file
|
||||
assert_equal input[1..-1], File.readlines(history_file, chomp: true)
|
||||
assert_equal input[1..], File.readlines(history_file, chomp: true)
|
||||
end
|
||||
|
||||
# Update history entries (not changed on disk)
|
||||
@@ -1501,7 +1502,7 @@ class TestGoFZF < TestBase
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0), :Enter
|
||||
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0 --bind space:toggle-preview), :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
assert_equal ' 100/100', lines[1]
|
||||
@@ -1511,17 +1512,17 @@ class TestGoFZF < TestBase
|
||||
assert_path_exists tempname
|
||||
assert_equal %w[1], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| assert_equal '> 2', lines[3] }
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
assert_equal %w[1 2], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
tmux.send_keys :Down
|
||||
tmux.send_keys :Space, :Down, :Down
|
||||
tmux.until { |lines| assert_equal '> 3', lines[4] }
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
assert_equal %w[1 2 3], File.readlines(tempname, chomp: true)
|
||||
assert_equal %w[1], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
tmux.send_keys :Space, :Down
|
||||
tmux.until { |lines| assert_equal '> 4', lines[5] }
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
assert_equal %w[1 3 4], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1554,13 +1555,13 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_preview_q_no_match
|
||||
tmux.send_keys %(: | #{FZF} --preview 'echo foo {q}'), :Enter
|
||||
tmux.send_keys %(: | #{FZF} --preview 'echo foo {q} foo'), :Enter
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.until { |lines| refute_includes lines[1], ' foo ' }
|
||||
tmux.until { |lines| assert_includes lines[1], ' foo foo' }
|
||||
tmux.send_keys 'bar'
|
||||
tmux.until { |lines| assert_includes lines[1], ' foo bar ' }
|
||||
tmux.until { |lines| assert_includes lines[1], ' foo bar foo' }
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.until { |lines| refute_includes lines[1], ' foo ' }
|
||||
tmux.until { |lines| assert_includes lines[1], ' foo foo' }
|
||||
end
|
||||
|
||||
def test_preview_q_no_match_with_initial_query
|
||||
@@ -1586,6 +1587,11 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_equal '> 1', lines[-2] }
|
||||
end
|
||||
|
||||
def test_info_inline_separator
|
||||
tmux.send_keys 'seq 10 | fzf --info=inline:___ --no-separator', :Enter
|
||||
tmux.until { |lines| assert_equal '> ___10/10', lines[-1] }
|
||||
end
|
||||
|
||||
def test_change_first_last
|
||||
tmux.send_keys %(seq 1000 | #{FZF} --bind change:first,alt-Z:last), :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
@@ -1604,6 +1610,30 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys :Enter
|
||||
end
|
||||
|
||||
def test_pos
|
||||
tmux.send_keys %(seq 1000 | #{FZF} --bind 'a:pos(3),b:pos(-3),c:pos(1),d:pos(-1),e:pos(0)' --preview 'echo {}/{}'), :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.send_keys :a
|
||||
tmux.until { |lines| assert_includes lines[1], ' 3/3' }
|
||||
tmux.send_keys :b
|
||||
tmux.until { |lines| assert_includes lines[1], ' 998/998' }
|
||||
tmux.send_keys :c
|
||||
tmux.until { |lines| assert_includes lines[1], ' 1/1' }
|
||||
tmux.send_keys :d
|
||||
tmux.until { |lines| assert_includes lines[1], ' 1000/1000' }
|
||||
tmux.send_keys :e
|
||||
tmux.until { |lines| assert_includes lines[1], ' 1/1' }
|
||||
end
|
||||
|
||||
def test_put
|
||||
tmux.send_keys %(seq 1000 | #{FZF} --bind 'a:put+put,b:put+put(ravo)' --preview 'echo {q}/{q}'), :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.send_keys :a
|
||||
tmux.until { |lines| assert_includes lines[1], ' aa/aa' }
|
||||
tmux.send_keys :b
|
||||
tmux.until { |lines| assert_includes lines[1], ' aabravo/aabravo' }
|
||||
end
|
||||
|
||||
def test_accept_non_empty
|
||||
tmux.send_keys %(seq 1000 | #{fzf('--print-query --bind enter:accept-non-empty')}), :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
@@ -1764,6 +1794,32 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_equal '>', lines.last }
|
||||
end
|
||||
|
||||
def test_change_query
|
||||
tmux.send_keys %(: | #{FZF} --query foo --bind space:change-query:foobar), :Enter
|
||||
tmux.until { |lines| assert_equal 0, lines.item_count }
|
||||
tmux.until { |lines| assert_equal '> foo', lines.last }
|
||||
tmux.send_keys :Space, 'baz'
|
||||
tmux.until { |lines| assert_equal '> foobarbaz', lines.last }
|
||||
end
|
||||
|
||||
def test_transform_query
|
||||
tmux.send_keys %{#{FZF} --bind 'ctrl-r:transform-query(rev <<< {q}),ctrl-u:transform-query: tr "[:lower:]" "[:upper:]" <<< {q}' --query bar}, :Enter
|
||||
tmux.until { |lines| assert_equal '> bar', lines[-1] }
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_equal '> rab', lines[-1] }
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.until { |lines| assert_equal '> RAB', lines[-1] }
|
||||
end
|
||||
|
||||
def test_transform_prompt
|
||||
tmux.send_keys %{#{FZF} --bind 'ctrl-r:transform-query(rev <<< {q}),ctrl-u:transform-query: tr "[:lower:]" "[:upper:]" <<< {q}' --query bar}, :Enter
|
||||
tmux.until { |lines| assert_equal '> bar', lines[-1] }
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_equal '> rab', lines[-1] }
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.until { |lines| assert_equal '> RAB', lines[-1] }
|
||||
end
|
||||
|
||||
def test_clear_selection
|
||||
tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
@@ -1892,8 +1948,69 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_preview_window_follow
|
||||
tmux.send_keys "#{FZF} --preview 'seq 1000 | nl' --preview-window down:noborder:follow", :Enter
|
||||
tmux.until { |lines| assert_equal '1000 1000', lines[-1].strip }
|
||||
file = Tempfile.new('fzf-follow')
|
||||
file.sync = true
|
||||
|
||||
tmux.send_keys %(seq 100 | #{FZF} --preview 'tail -f "#{file.path}"' --preview-window follow --bind 'up:preview-up,down:preview-down,space:change-preview-window:follow|nofollow' --preview-window '~3'), :Enter
|
||||
tmux.until { |lines| lines.item_count == 100 }
|
||||
|
||||
# Write to the temporary file, and check if the preview window is showing
|
||||
# the last line of the file
|
||||
3.times { file.puts _1 } # header lines
|
||||
1000.times { file.puts _1 }
|
||||
tmux.until { |lines| assert_includes lines[1], '/1003' }
|
||||
tmux.until { |lines| assert_includes lines[-2], '999' }
|
||||
|
||||
# Scroll the preview window and fzf should stop following the file content
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines[-2], '998' }
|
||||
file.puts 'foo', 'bar'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1005'
|
||||
assert_includes lines[-2], '998'
|
||||
end
|
||||
|
||||
# Scroll back to the bottom and fzf should start following the file again
|
||||
%w[999 foo bar].each do |item|
|
||||
wait do
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| assert_includes lines[-2], item }
|
||||
end
|
||||
end
|
||||
file.puts 'baz'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1006'
|
||||
assert_includes lines[-2], 'baz'
|
||||
end
|
||||
|
||||
# Scroll upwards to stop following
|
||||
tmux.send_keys :Up
|
||||
wait { assert_includes lines[-2], 'bar' }
|
||||
file.puts 'aaa'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1007'
|
||||
assert_includes lines[-2], 'bar'
|
||||
end
|
||||
|
||||
# Manually enable following
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_includes lines[-2], 'aaa' }
|
||||
file.puts 'bbb'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1008'
|
||||
assert_includes lines[-2], 'bbb'
|
||||
end
|
||||
|
||||
# Disable following
|
||||
tmux.send_keys :Space
|
||||
file.puts 'ccc', 'ddd'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1010'
|
||||
assert_includes lines[-2], 'bbb'
|
||||
end
|
||||
rescue StandardError
|
||||
file.close
|
||||
file.unlink
|
||||
end
|
||||
|
||||
def test_toggle_preview_wrap
|
||||
@@ -2119,6 +2236,15 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_includes lines[1], '4' }
|
||||
end
|
||||
|
||||
def test_reload_sync
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind 'load:reload-sync(sleep 1; seq 1000)+unbind(load)'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.send_keys '00'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
# After 1 second
|
||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||
end
|
||||
|
||||
def test_scroll_off
|
||||
tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.item_count }
|
||||
@@ -2320,13 +2446,13 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys "seq 3 | fzf --height ~100% --border=vertical --preview 'seq {}' --preview-window left,5,border-right --padding 1 --exit-0 --header $'hello\\nworld' --header-lines=2", :Enter
|
||||
expected = <<~OUTPUT
|
||||
│
|
||||
│ 1 │> 3
|
||||
│ 2 │ 2
|
||||
│ 3 │ 1
|
||||
│ │ hello
|
||||
│ │ world
|
||||
│ │ 1/1 ─
|
||||
│ │>
|
||||
│ 1 │ > 3
|
||||
│ 2 │ 2
|
||||
│ 3 │ 1
|
||||
│ │ hello
|
||||
│ │ world
|
||||
│ │ 1/1 ─
|
||||
│ │ >
|
||||
│
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
@@ -2345,19 +2471,38 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_start_event
|
||||
tmux.send_keys 'seq 100 | fzf --multi --sync --preview-window border-none --bind "start:select-all+last+preview(echo welcome)"', :Enter
|
||||
tmux.send_keys 'seq 100 | fzf --multi --sync --preview-window hidden:border-none --bind "start:select-all+last+preview(echo welcome)"', :Enter
|
||||
tmux.until do |lines|
|
||||
assert_match(/>100.*welcome/, lines[0])
|
||||
assert_includes(lines[-2], '100/100 (100)')
|
||||
end
|
||||
end
|
||||
|
||||
def test_focus_event
|
||||
tmux.send_keys 'seq 100 | fzf --bind "focus:transform-prompt(echo [[{}]])"', :Enter
|
||||
tmux.until { |lines| assert_includes(lines[-1], '[[1]]') }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes(lines[-1], '[[2]]') }
|
||||
tmux.send_keys :X
|
||||
tmux.until { |lines| assert_includes(lines[-1], '[[]]') }
|
||||
end
|
||||
|
||||
def test_labels_center
|
||||
tmux.send_keys ': | fzf --border --border-label foobar --preview : --preview-label barfoo', :Enter
|
||||
tmux.send_keys 'echo x | fzf --border --border-label foobar --preview : --preview-label barfoo --bind "space:change-border-label(foobarfoo)+change-preview-label(barfoobar),enter:transform-border-label(echo foo{}foo)+transform-preview-label(echo bar{}bar)"', :Enter
|
||||
tmux.until do
|
||||
assert_includes(_1[0], '─foobar─')
|
||||
assert_includes(_1[1], '─barfoo─')
|
||||
end
|
||||
tmux.send_keys :space
|
||||
tmux.until do
|
||||
assert_includes(_1[0], '─foobarfoo─')
|
||||
assert_includes(_1[1], '─barfoobar─')
|
||||
end
|
||||
tmux.send_keys :Enter
|
||||
tmux.until do
|
||||
assert_includes(_1[0], '─fooxfoo─')
|
||||
assert_includes(_1[1], '─barxbar─')
|
||||
end
|
||||
end
|
||||
|
||||
def test_labels_left
|
||||
@@ -2408,6 +2553,41 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys 'seq 100 | fzf -q55 --no-separator', :Enter
|
||||
tmux.until { assert(_1[-2] == ' 1/100') }
|
||||
end
|
||||
|
||||
def test_prev_next_selected
|
||||
tmux.send_keys 'seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected', :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||
tmux.send_keys :BTab, :BTab, :Up, :BTab
|
||||
tmux.until { |lines| assert_equal 3, lines.select_count }
|
||||
tmux.send_keys 'C-n'
|
||||
tmux.until { |lines| assert_includes lines, '>>4' }
|
||||
tmux.send_keys 'C-n'
|
||||
tmux.until { |lines| assert_includes lines, '>>2' }
|
||||
tmux.send_keys 'C-n'
|
||||
tmux.until { |lines| assert_includes lines, '>>1' }
|
||||
tmux.send_keys 'C-n'
|
||||
tmux.until { |lines| assert_includes lines, '>>4' }
|
||||
tmux.send_keys 'C-p'
|
||||
tmux.until { |lines| assert_includes lines, '>>1' }
|
||||
tmux.send_keys 'C-p'
|
||||
tmux.until { |lines| assert_includes lines, '>>2' }
|
||||
end
|
||||
|
||||
def test_listen
|
||||
tmux.send_keys 'seq 10 | fzf --listen 6266', :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||
Net::HTTP.post(URI('http://localhost:6266'), 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
|
||||
end
|
||||
|
||||
def test_toggle_alternative_preview_window
|
||||
tmux.send_keys "seq 10 | #{FZF} --bind space:toggle-preview --preview-window '<100000(hidden,up,border-none)' --preview 'echo /{}/{}/'", :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||
tmux.until { |lines| refute_includes lines, '/1/1/' }
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_includes lines, '/1/1/' }
|
||||
end
|
||||
end
|
||||
|
||||
module TestShell
|
||||
@@ -2478,7 +2658,7 @@ module TestShell
|
||||
tmux.prepare
|
||||
tmux.send_keys :Escape, :c
|
||||
lines = tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
expected = lines.reverse.find { |l| l.start_with?('> ') }[2..-1]
|
||||
expected = lines.reverse.find { |l| l.start_with?('> ') }[2..]
|
||||
tmux.send_keys :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys :pwd, :Enter
|
||||
@@ -2531,7 +2711,7 @@ module TestShell
|
||||
|
||||
def test_ctrl_r_multiline
|
||||
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
|
||||
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..-1] }
|
||||
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
|
||||
tmux.prepare
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_equal '>', lines[-1] }
|
||||
@@ -2540,7 +2720,7 @@ module TestShell
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert lines[-1]&.end_with?('bar"') }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..-1] }
|
||||
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
|
||||
end
|
||||
|
||||
def test_ctrl_r_abort
|
||||
@@ -2731,7 +2911,7 @@ module CompletionTest
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) { |lines| assert_match(/cat .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines[-1]) }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal %w[test3 test4], lines[-2..-1] }
|
||||
tmux.until { |lines| assert_equal %w[test3 test4], lines[-2..] }
|
||||
end
|
||||
|
||||
def test_custom_completion_api
|
||||
@@ -2821,7 +3001,7 @@ class TestFish < TestBase
|
||||
end
|
||||
|
||||
def new_shell
|
||||
tmux.send_keys 'env FZF_TMUX=1 fish', :Enter
|
||||
tmux.send_keys 'env FZF_TMUX=1 FZF_DEFAULT_OPTS=--no-scrollbar fish', :Enter
|
||||
tmux.send_keys 'function fish_prompt; end; clear', :Enter
|
||||
tmux.until { |lines| assert_empty lines }
|
||||
end
|
||||
@@ -2840,6 +3020,8 @@ unset <%= UNSETS.join(' ') %>
|
||||
unset $(env | sed -n /^_fzf_orig/s/=.*//p)
|
||||
unset $(declare -F | sed -n "/_fzf/s/.*-f //p")
|
||||
|
||||
export FZF_DEFAULT_OPTS=--no-scrollbar
|
||||
|
||||
# Setup fzf
|
||||
# ---------
|
||||
if [[ ! "$PATH" == *<%= BASE %>/bin* ]]; then
|
||||
|
Reference in New Issue
Block a user