m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-14 22:33:47 -05:00

Compare commits

...

16 Commits

Author SHA1 Message Date
Junegunn Choi
f97d275413 0.50.0 2024-04-15 00:02:27 +09:00
Junegunn Choi
3acb4ca90e Fix streaming filter mode by not running reader callback concurrently
Close #3728
2024-04-14 23:34:25 +09:00
Junegunn Choi
e86b81bbf5 Improve search performance by limiting the search scope
Find the last occurrence of the last character in the pattern and
perform the search algorithm only up to that point.

The effectiveness of this mechanism depends a lot on the shape of the
input and the pattern.
2024-04-14 11:48:44 +09:00
Junegunn Choi
a5447b8b75 Improve search performance by pre-calculating bonus matrix
This gives yet another 5% boost.
2024-04-14 11:47:06 +09:00
Junegunn Choi
7ce6452d83 Improve search performance by pre-calculating character classes
This simple optmization can give more than 15% performance boost
in some scenarios.
2024-04-14 11:47:05 +09:00
junegunn
5643a306bd Deploying to master from @ junegunn/fzf@3c877c504b 🚀 2024-04-14 00:03:45 +00:00
Charlie Vieth
3c877c504b Enable profiling options when 'pprof' tag is set (#2813)
This commit enables cpu, mem, block, and mutex profling of the FZF
executable. To support flushing the profiles at program exit it adds
util.AtExit to register "at exit" functions and mandates that util.Exit
is used instead of os.Exit to stop the program.

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-04-13 14:58:11 +09:00
Junegunn Choi
892d1acccb Fix tcell build 2024-04-13 14:47:04 +09:00
Junegunn Choi
1a9c282f76 Fix unit tests 2024-04-13 14:40:43 +09:00
Junegunn Choi
fd1ba46f77 Export $FZF_KEY environment variable to child processes
It's the name of the last key pressed.

Related #3412
2024-04-13 14:00:16 +09:00
Junegunn Choi
a4745626dd Add jump and jump-cancel events
Close #3412

    # Default behavior
    fzf --bind space:jump

    # Same as jump-accept action
    fzf --bind space:jump,jump:accept

    # Accept on jump, abort on cancel
    fzf --bind space:jump,jump:accept,jump-cancel:abort

    # Change header on jump-cancel
    fzf --bind 'space:change-header(Type jump label)+jump,jump-cancel:change-header:Jump cancelled'
2024-04-10 20:17:12 +09:00
dependabot[bot]
17bb7ad278 Bump golang.org/x/term from 0.18.0 to 0.19.0 (#3718)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.18.0 to 0.19.0.
- [Commits](https://github.com/golang/term/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 00:51:29 +09:00
Junegunn Choi
152988c17b [shell] Revert interactiveness checks for eval
So that there's no error even when the scripts are mistakenly evaluated
in non-interactive sessions.

  bash -c 'eval "$(fzf --bash)"; echo done'
  zsh -c 'eval "$(fzf --zsh)"; echo done'

* https://github.com/junegunn/fzf/pull/3675#issuecomment-2044860901
* f103aa4753
2024-04-10 00:46:09 +09:00
Junegunn Choi
4cd37fc02b Disable line wrapping during rendering
Prevent unwanted line wraps that break the layout when the actual
display width of a character is different than expected.
2024-04-09 00:26:25 +09:00
LangLangBart
69b9d674a3 chore: Add new option in issue checklist and modify requirements (#3715) 2024-04-07 10:25:12 +09:00
junegunn
bad8061547 Deploying to master from @ junegunn/fzf@62963dcefd 🚀 2024-04-07 00:01:37 +00:00
38 changed files with 996 additions and 266 deletions

View File

@@ -15,6 +15,8 @@ body:
required: true required: true
- label: I have searched through the existing issues - label: I have searched through the existing issues
required: true required: true
- label: For bug reports, I have checked if the bug is reproducible in the latest version of fzf
required: false
- type: input - type: input
attributes: attributes:

View File

@@ -24,13 +24,23 @@ make build
make release make release
``` ```
> :warning: Makefile uses git commands to determine the version and the > [!WARNING]
> revision information for `fzf --version`. So if you're building fzf from an > Makefile uses git commands to determine the version and the revision
> information for `fzf --version`. So if you're building fzf from an
> environment where its git information is not available, you have to manually > environment where its git information is not available, you have to manually
> set `$FZF_VERSION` and `$FZF_REVISION`. > set `$FZF_VERSION` and `$FZF_REVISION`.
> >
> e.g. `FZF_VERSION=0.24.0 FZF_REVISION=tarball make` > e.g. `FZF_VERSION=0.24.0 FZF_REVISION=tarball make`
> [!TIP]
> To build fzf with profiling options enabled, set `TAGS=pprof`
>
> ```sh
> TAGS=pprof make clean install
> fzf --profile-cpu /tmp/cpu.pprof --profile-mem /tmp/mem.pprof \
> --profile-block /tmp/block.pprof --profile-mutex /tmp/mutex.pprof
> ```
Third-party libraries used Third-party libraries used
-------------------------- --------------------------

View File

@@ -1,6 +1,43 @@
CHANGELOG CHANGELOG
========= =========
0.50.0
------
- Search performance optimization. You can observe 50%+ improvement in some scenarios.
```
$ rg --line-number --no-heading --smart-case . > $DATA
$ wc < $DATA
5520118 26862362 897487793
$ hyperfine -w 1 -L bin fzf-0.49.0,fzf-7ce6452,fzf-a5447b8,fzf '{bin} --filter "///" < $DATA | head -30'
Summary
fzf --filter "///" < $DATA | head -30 ran
1.16 ± 0.03 times faster than fzf-a5447b8 --filter "///" < $DATA | head -30
1.23 ± 0.03 times faster than fzf-7ce6452 --filter "///" < $DATA | head -30
1.52 ± 0.03 times faster than fzf-0.49.0 --filter "///" < $DATA | head -30
```
- Added `jump` and `jump-cancel` events that are triggered when leaving `jump` mode
```sh
# Default behavior
fzf --bind space:jump
# Same as jump-accept action
fzf --bind space:jump,jump:accept
# Accept on jump, abort on cancel
fzf --bind space:jump,jump:accept,jump-cancel:abort
# Change header on jump-cancel
fzf --bind 'space:change-header(Type jump label)+jump,jump-cancel:change-header:Jump cancelled'
```
- Added a new environment variable `$FZF_KEY` exported to the child processes. It's the name of the last key pressed.
```sh
fzf --bind 'space:jump,jump:accept,jump-cancel:transform:[[ $FZF_KEY =~ ctrl-c ]] && echo abort'
```
- fzf can be built with profiling options. See [BUILD.md](BUILD.md) for more information.
- Bug fixes
0.49.0 0.49.0
------ ------
- Ingestion performance improved by around 40% (more or less depending on options) - Ingestion performance improved by around 40% (more or less depending on options)

View File

@@ -79,6 +79,7 @@ all: target/$(BINARY)
test: $(SOURCES) test: $(SOURCES)
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1) [ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \ SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
github.com/junegunn/fzf \
github.com/junegunn/fzf/src \ github.com/junegunn/fzf/src \
github.com/junegunn/fzf/src/algo \ github.com/junegunn/fzf/src/algo \
github.com/junegunn/fzf/src/tui \ github.com/junegunn/fzf/src/tui \

File diff suppressed because one or more lines are too long

4
go.mod
View File

@@ -6,8 +6,8 @@ require (
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-shellwords v1.0.12
github.com/rivo/uniseg v0.4.7 github.com/rivo/uniseg v0.4.7
golang.org/x/sys v0.18.0 golang.org/x/sys v0.19.0
golang.org/x/term v0.18.0 golang.org/x/term v0.19.0
) )
require ( require (

8
go.sum
View File

@@ -36,14 +36,14 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/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/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=

View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.49.0 version=0.50.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2

View File

@@ -1,4 +1,4 @@
$version="0.49.0" $version="0.50.0"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition $fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition

View File

@@ -9,7 +9,7 @@ import (
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version string = "0.49" var version string = "0.50"
var revision string = "devel" var revision string = "devel"
//go:embed shell/key-bindings.bash //go:embed shell/key-bindings.bash

158
main_test.go Normal file
View File

@@ -0,0 +1,158 @@
package main
import (
"fmt"
"go/ast"
"go/build"
"go/importer"
"go/parser"
"go/token"
"go/types"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"testing"
)
func loadPackages(t *testing.T) []*build.Package {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
var pkgs []*build.Package
seen := make(map[string]bool)
err = filepath.WalkDir(wd, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
name := d.Name()
if d.IsDir() {
if name == "" || name[0] == '.' || name[0] == '_' || name == "vendor" || name == "tmp" {
return filepath.SkipDir
}
return nil
}
if d.Type().IsRegular() && filepath.Ext(name) == ".go" && !strings.HasSuffix(name, "_test.go") {
dir := filepath.Dir(path)
if !seen[dir] {
pkg, err := build.ImportDir(dir, build.ImportComment)
if err != nil {
return fmt.Errorf("%s: %s", dir, err)
}
if pkg.ImportPath == "" || pkg.ImportPath == "." {
importPath, err := filepath.Rel(wd, dir)
if err != nil {
t.Fatal(err)
}
pkg.ImportPath = filepath.ToSlash(filepath.Join("github.com/junegunn/fzf", importPath))
}
pkgs = append(pkgs, pkg)
seen[dir] = true
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
sort.Slice(pkgs, func(i, j int) bool {
return pkgs[i].ImportPath < pkgs[j].ImportPath
})
return pkgs
}
var sourceImporter = importer.ForCompiler(token.NewFileSet(), "source", nil)
func checkPackageForOsExit(t *testing.T, bpkg *build.Package, allowed map[string]int) (errOsExit bool) {
var files []*ast.File
fset := token.NewFileSet()
for _, name := range bpkg.GoFiles {
filename := filepath.Join(bpkg.Dir, name)
af, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
t.Fatal(err)
}
files = append(files, af)
}
info := types.Info{
Uses: make(map[*ast.Ident]types.Object),
}
conf := types.Config{
Importer: sourceImporter,
}
_, err := conf.Check(bpkg.Name, fset, files, &info)
if err != nil {
t.Fatal(err)
}
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
for id, obj := range info.Uses {
if obj.Pkg() != nil && obj.Pkg().Name() == "os" && obj.Name() == "Exit" {
pos := fset.Position(id.Pos())
name, err := filepath.Rel(wd, pos.Filename)
if err != nil {
t.Log(err)
name = pos.Filename
}
name = filepath.ToSlash(name)
// Check if the usage is allowed
if allowed[name] > 0 {
allowed[name]--
continue
}
t.Errorf("os.Exit referenced at: %s:%d:%d", name, pos.Line, pos.Column)
errOsExit = true
}
}
return errOsExit
}
// Enforce that src/util.Exit() is used instead of os.Exit by prohibiting
// references to it anywhere else in the fzf code base.
func TestOSExitNotAllowed(t *testing.T) {
if testing.Short() {
t.Skip("skipping: short test")
}
allowed := map[string]int{
"src/util/atexit.go": 1, // os.Exit allowed 1 time in "atexit.go"
}
var errOsExit bool
for _, pkg := range loadPackages(t) {
t.Run(pkg.ImportPath, func(t *testing.T) {
if checkPackageForOsExit(t, pkg, allowed) {
errOsExit = true
}
})
}
if t.Failed() && errOsExit {
var names []string
for name := range allowed {
names = append(names, fmt.Sprintf("%q", name))
}
sort.Strings(names)
const errMsg = `
Test failed because os.Exit was referenced outside of the following files:
%s
Use github.com/junegunn/fzf/src/util.Exit() instead to exit the program.
This is enforced because calling os.Exit() prevents the functions
registered with util.AtExit() from running.`
t.Errorf(errMsg, strings.Join(names, "\n "))
}
}

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "Apr 2024" "fzf 0.49.0" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Apr 2024" "fzf 0.50.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf-tmux - open fzf in tmux split pane fzf-tmux - open fzf in tmux split pane

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Apr 2024" "fzf 0.49.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Apr 2024" "fzf 0.50.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -191,7 +191,7 @@ actions are affected:
\fBkill-word\fR \fBkill-word\fR
.TP .TP
.BI "--jump-labels=" "CHARS" .BI "--jump-labels=" "CHARS"
Label characters for \fBjump\fR and \fBjump-accept\fR Label characters for \fBjump\fR mode.
.SS Layout .SS Layout
.TP .TP
.BI "--height=" "[~]HEIGHT[%]" .BI "--height=" "[~]HEIGHT[%]"
@@ -982,6 +982,8 @@ fzf exports the following environment variables to its child processes.
.br .br
.BR FZF_ACTION " The name of the last action performed" .BR FZF_ACTION " The name of the last action performed"
.br .br
.BR FZF_KEY " The name of the last key pressed"
.br
.BR FZF_PORT " Port number when --listen option is used" .BR FZF_PORT " Port number when --listen option is used"
.br .br
@@ -1066,7 +1068,7 @@ e.g.
.br .br
\fIspace\fR \fIspace\fR
.br .br
\fIbspace\fR (\fIbs\fR) \fIbackspace\fR (\fIbspace\fR \fIbs\fR)
.br .br
\fIalt-up\fR \fIalt-up\fR
.br .br
@@ -1080,15 +1082,15 @@ e.g.
.br .br
\fIalt-space\fR \fIalt-space\fR
.br .br
\fIalt-bspace\fR (\fIalt-bs\fR) \fIalt-backspace\fR (\fIalt-bspace\fR \fIalt-bs\fR)
.br .br
\fItab\fR \fItab\fR
.br .br
\fIbtab\fR (\fIshift-tab\fR) \fIshift-tab\fR (\fIbtab\fR)
.br .br
\fIesc\fR \fIesc\fR
.br .br
\fIdel\fR \fIdelete\fR (\fIdel\fR)
.br .br
\fIup\fR \fIup\fR
.br .br
@@ -1104,9 +1106,9 @@ e.g.
.br .br
\fIinsert\fR \fIinsert\fR
.br .br
\fIpgup\fR (\fIpage-up\fR) \fIpage-up\fR (\fIpgup\fR)
.br .br
\fIpgdn\fR (\fIpage-down\fR) \fIpage-down\fR (\fIpgdn\fR)
.br .br
\fIshift-up\fR \fIshift-up\fR
.br .br
@@ -1160,6 +1162,7 @@ e.g.
\fB# Move cursor to the last item and select all items \fB# Move cursor to the last item and select all items
seq 1000 | fzf --multi --sync --bind start:last+select-all\fR seq 1000 | fzf --multi --sync --bind start:last+select-all\fR
.RE .RE
\fIload\fR \fIload\fR
.RS .RS
Triggered when the input stream is complete and the initial processing of the Triggered when the input stream is complete and the initial processing of the
@@ -1169,6 +1172,7 @@ e.g.
\fB# Change the prompt to "loaded" when the input stream is complete \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 (seq 10; sleep 1; seq 11 20) | fzf --prompt 'Loading> ' --bind 'load:change-prompt:Loaded> '\fR
.RE .RE
\fIresize\fR \fIresize\fR
.RS .RS
Triggered when the terminal size is changed. Triggered when the terminal size is changed.
@@ -1176,6 +1180,7 @@ Triggered when the terminal size is changed.
e.g. e.g.
\fBfzf --bind 'resize:transform-header:echo Resized: ${FZF_COLUMNS}x${FZF_LINES}'\fR \fBfzf --bind 'resize:transform-header:echo Resized: ${FZF_COLUMNS}x${FZF_LINES}'\fR
.RE .RE
\fIresult\fR \fIresult\fR
.RS .RS
Triggered when the filtering for the current query is complete and the result list is ready. Triggered when the filtering for the current query is complete and the result list is ready.
@@ -1209,6 +1214,7 @@ e.g.
# Beware not to introduce an infinite loop # Beware not to introduce an infinite loop
seq 10 | fzf --bind 'focus:up' --cycle\fR seq 10 | fzf --bind 'focus:up' --cycle\fR
.RE .RE
\fIone\fR \fIone\fR
.RS .RS
Triggered when there's only one match. \fBone:accept\fR binding is comparable Triggered when there's only one match. \fBone:accept\fR binding is comparable
@@ -1220,6 +1226,7 @@ e.g.
\fB# Automatically select the only match \fB# Automatically select the only match
seq 10 | fzf --bind one:accept\fR seq 10 | fzf --bind one:accept\fR
.RE .RE
\fIzero\fR \fIzero\fR
.RS .RS
Triggered when there's no match. \fBzero:abort\fR binding is comparable to Triggered when there's no match. \fBzero:abort\fR binding is comparable to
@@ -1241,6 +1248,22 @@ e.g.
\fBfzf --bind backward-eof:abort\fR \fBfzf --bind backward-eof:abort\fR
.RE .RE
\fIjump\fR
.RS
Triggered when successfully jumped to the target item in \fBjump\fR mode.
e.g.
\fBfzf --bind space:jump,jump:accept\fR
.RE
\fIjump-cancel\fR
.RS
Triggered when \fBjump\fR mode is cancelled.
e.g.
\fBfzf --bind space:jump,jump:accept,jump-cancel:abort\fR
.RE
.SS AVAILABLE ACTIONS: .SS AVAILABLE ACTIONS:
A key or an event can be bound to one or more of the following actions. A key or an event can be bound to one or more of the following actions.
@@ -1283,7 +1306,6 @@ A key or an event can be bound to one or more of the following actions.
\fBforward-word\fR \fIalt-f shift-right\fR \fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR \fBignore\fR
\fBjump\fR (EasyMotion-like 2-keystroke movement) \fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBjump-accept\fR (jump and accept)
\fBkill-line\fR \fBkill-line\fR
\fBkill-word\fR \fIalt-d\fR \fBkill-word\fR \fIalt-d\fR
\fBlast\fR (move to the last match; same as \fBpos(-1)\fR) \fBlast\fR (move to the last match; same as \fBpos(-1)\fR)

View File

@@ -9,7 +9,7 @@
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
[[ $- =~ i ]] || return 0 if [[ $- =~ i ]]; then
# To use custom commands instead of find, override _fzf_compgen_{path,dir} # To use custom commands instead of find, override _fzf_compgen_{path,dir}
@@ -581,3 +581,5 @@ _fzf_setup_completion 'var' export unset printenv
_fzf_setup_completion 'alias' unalias _fzf_setup_completion 'alias' unalias
_fzf_setup_completion 'host' telnet _fzf_setup_completion 'host' telnet
_fzf_setup_completion 'proc' kill _fzf_setup_completion 'proc' kill
fi

View File

@@ -9,7 +9,7 @@
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
[[ -o interactive ]] || return 0 if [[ -o interactive ]]; then
# Both branches of the following `if` do the same thing -- define # Both branches of the following `if` do the same thing -- define
@@ -351,3 +351,5 @@ bindkey '^I' fzf-completion
eval $__fzf_completion_options eval $__fzf_completion_options
'unset' '__fzf_completion_options' 'unset' '__fzf_completion_options'
} }
fi

View File

@@ -11,7 +11,7 @@
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
[[ $- =~ i ]] || return 0 if [[ $- =~ i ]]; then
# Key bindings # Key bindings
@@ -132,3 +132,5 @@ if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then
bind -m vi-command '"\ec": "\C-z\ec\C-z"' bind -m vi-command '"\ec": "\C-z\ec\C-z"'
bind -m vi-insert '"\ec": "\C-z\ec\C-z"' bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
fi fi
fi

View File

@@ -11,7 +11,7 @@
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
[[ -o interactive ]] || return 0 if [[ -o interactive ]]; then
# Key bindings # Key bindings
@@ -119,3 +119,5 @@ bindkey -M viins '^R' fzf-history-widget
eval $__fzf_key_bindings_options eval $__fzf_key_bindings_options
'unset' '__fzf_key_bindings_options' 'unset' '__fzf_key_bindings_options'
} }
fi

View File

@@ -153,6 +153,12 @@ var (
bonusBoundaryDelimiter int16 = bonusBoundary + 1 bonusBoundaryDelimiter int16 = bonusBoundary + 1
initialCharClass charClass = charWhite initialCharClass charClass = charWhite
// A minor optimization that can give 15%+ performance boost
asciiCharClasses [unicode.MaxASCII + 1]charClass
// A minor optimization that can give yet another 5% performance boost
bonusMatrix [charNumber + 1][charNumber + 1]int16
) )
type charClass int type charClass int
@@ -187,6 +193,27 @@ func Init(scheme string) bool {
default: default:
return false return false
} }
for i := 0; i <= unicode.MaxASCII; i++ {
char := rune(i)
c := charNonWord
if char >= 'a' && char <= 'z' {
c = charLower
} else if char >= 'A' && char <= 'Z' {
c = charUpper
} else if char >= '0' && char <= '9' {
c = charNumber
} else if strings.ContainsRune(whiteChars, char) {
c = charWhite
} else if strings.ContainsRune(delimiterChars, char) {
c = charDelimiter
}
asciiCharClasses[i] = c
}
for i := 0; i <= int(charNumber); i++ {
for j := 0; j <= int(charNumber); j++ {
bonusMatrix[i][j] = bonusFor(charClass(i), charClass(j))
}
}
return true return true
} }
@@ -214,21 +241,6 @@ func alloc32(offset int, slab *util.Slab, size int) (int, []int32) {
return offset, make([]int32, size) return offset, make([]int32, size)
} }
func charClassOfAscii(char rune) charClass {
if char >= 'a' && char <= 'z' {
return charLower
} else if char >= 'A' && char <= 'Z' {
return charUpper
} else if char >= '0' && char <= '9' {
return charNumber
} else if strings.ContainsRune(whiteChars, char) {
return charWhite
} else if strings.ContainsRune(delimiterChars, char) {
return charDelimiter
}
return charNonWord
}
func charClassOfNonAscii(char rune) charClass { func charClassOfNonAscii(char rune) charClass {
if unicode.IsLower(char) { if unicode.IsLower(char) {
return charLower return charLower
@@ -248,7 +260,7 @@ func charClassOfNonAscii(char rune) charClass {
func charClassOf(char rune) charClass { func charClassOf(char rune) charClass {
if char <= unicode.MaxASCII { if char <= unicode.MaxASCII {
return charClassOfAscii(char) return asciiCharClasses[char]
} }
return charClassOfNonAscii(char) return charClassOfNonAscii(char)
} }
@@ -287,7 +299,7 @@ func bonusAt(input *util.Chars, idx int) int16 {
if idx == 0 { if idx == 0 {
return bonusBoundaryWhite return bonusBoundaryWhite
} }
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx))) return bonusMatrix[charClassOf(input.Get(idx-1))][charClassOf(input.Get(idx))]
} }
func normalizeRune(r rune) rune { func normalizeRune(r rune) rune {
@@ -340,30 +352,45 @@ func isAscii(runes []rune) bool {
return true return true
} }
func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) int { func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) (int, int) {
// Can't determine // Can't determine
if !input.IsBytes() { if !input.IsBytes() {
return 0 return 0, input.Length()
} }
// Not possible // Not possible
if !isAscii(pattern) { if !isAscii(pattern) {
return -1 return -1, -1
} }
firstIdx, idx := 0, 0 firstIdx, idx, lastIdx := 0, 0, 0
var b byte
for pidx := 0; pidx < len(pattern); pidx++ { for pidx := 0; pidx < len(pattern); pidx++ {
idx = trySkip(input, caseSensitive, byte(pattern[pidx]), idx) b = byte(pattern[pidx])
idx = trySkip(input, caseSensitive, b, idx)
if idx < 0 { if idx < 0 {
return -1 return -1, -1
} }
if pidx == 0 && idx > 0 { if pidx == 0 && idx > 0 {
// Step back to find the right bonus point // Step back to find the right bonus point
firstIdx = idx - 1 firstIdx = idx - 1
} }
lastIdx = idx
idx++ idx++
} }
return firstIdx
// Find the last appearance of the last character of the pattern to limit the search scope
bu := b
if !caseSensitive && b >= 'a' && b <= 'z' {
bu = b - 32
}
scope := input.Bytes()[lastIdx:]
for offset := len(scope) - 1; offset > 0; offset-- {
if scope[offset] == b || scope[offset] == bu {
return firstIdx, lastIdx + offset + 1
}
}
return firstIdx, lastIdx + 1
} }
func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []int16) { func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []int16) {
@@ -412,6 +439,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
return Result{0, 0, 0}, posArray(withPos, M) return Result{0, 0, 0}, posArray(withPos, M)
} }
N := input.Length() N := input.Length()
if M > N {
return Result{-1, -1, 0}, nil
}
// Since O(nm) algorithm can be prohibitively expensive for large input, // Since O(nm) algorithm can be prohibitively expensive for large input,
// we fall back to the greedy algorithm. // we fall back to the greedy algorithm.
@@ -420,10 +450,12 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
} }
// Phase 1. Optimized search for ASCII string // Phase 1. Optimized search for ASCII string
idx := asciiFuzzyIndex(input, pattern, caseSensitive) minIdx, maxIdx := asciiFuzzyIndex(input, pattern, caseSensitive)
if idx < 0 { if minIdx < 0 {
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }
// fmt.Println(N, maxIdx, idx, maxIdx-idx, input.ToString())
N = maxIdx - minIdx
// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages // Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
offset16 := 0 offset16 := 0
@@ -436,20 +468,19 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
offset32, F := alloc32(offset32, slab, M) offset32, F := alloc32(offset32, slab, M)
// Rune array // Rune array
_, T := alloc32(offset32, slab, N) _, T := alloc32(offset32, slab, N)
input.CopyRunes(T) input.CopyRunes(T, minIdx)
// Phase 2. Calculate bonus for each point // Phase 2. Calculate bonus for each point
maxScore, maxScorePos := int16(0), 0 maxScore, maxScorePos := int16(0), 0
pidx, lastIdx := 0, 0 pidx, lastIdx := 0, 0
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), initialCharClass, false pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), initialCharClass, false
Tsub := T[idx:] for off, char := range T {
H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)]
for off, char := range Tsub {
var class charClass var class charClass
if char <= unicode.MaxASCII { if char <= unicode.MaxASCII {
class = charClassOfAscii(char) class = asciiCharClasses[char]
if !caseSensitive && class == charUpper { if !caseSensitive && class == charUpper {
char += 32 char += 32
T[off] = char
} }
} else { } else {
class = charClassOfNonAscii(char) class = charClassOfNonAscii(char)
@@ -459,28 +490,28 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
if normalize { if normalize {
char = normalizeRune(char) char = normalizeRune(char)
} }
T[off] = char
} }
Tsub[off] = char bonus := bonusMatrix[prevClass][class]
bonus := bonusFor(prevClass, class) B[off] = bonus
Bsub[off] = bonus
prevClass = class prevClass = class
if char == pchar { if char == pchar {
if pidx < M { if pidx < M {
F[pidx] = int32(idx + off) F[pidx] = int32(off)
pidx++ pidx++
pchar = pattern[util.Min(pidx, M-1)] pchar = pattern[util.Min(pidx, M-1)]
} }
lastIdx = idx + off lastIdx = off
} }
if char == pchar0 { if char == pchar0 {
score := scoreMatch + bonus*bonusFirstCharMultiplier score := scoreMatch + bonus*bonusFirstCharMultiplier
H0sub[off] = score H0[off] = score
C0sub[off] = 1 C0[off] = 1
if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) { if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {
maxScore, maxScorePos = score, idx+off maxScore, maxScorePos = score, off
if forward && bonus >= bonusBoundary { if forward && bonus >= bonusBoundary {
break break
} }
@@ -488,24 +519,24 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
inGap = false inGap = false
} else { } else {
if inGap { if inGap {
H0sub[off] = util.Max16(prevH0+scoreGapExtension, 0) H0[off] = util.Max16(prevH0+scoreGapExtension, 0)
} else { } else {
H0sub[off] = util.Max16(prevH0+scoreGapStart, 0) H0[off] = util.Max16(prevH0+scoreGapStart, 0)
} }
C0sub[off] = 0 C0[off] = 0
inGap = true inGap = true
} }
prevH0 = H0sub[off] prevH0 = H0[off]
} }
if pidx != M { if pidx != M {
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }
if M == 1 { if M == 1 {
result := Result{maxScorePos, maxScorePos + 1, int(maxScore)} result := Result{minIdx + maxScorePos, minIdx + maxScorePos + 1, int(maxScore)}
if !withPos { if !withPos {
return result, nil return result, nil
} }
pos := []int{maxScorePos} pos := []int{minIdx + maxScorePos}
return result, &pos return result, &pos
} }
@@ -602,7 +633,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
} }
if s > s1 && (s > s2 || s == s2 && preferMatch) { if s > s1 && (s > s2 || s == s2 && preferMatch) {
*pos = append(*pos, j) *pos = append(*pos, j+minIdx)
if i == 0 { if i == 0 {
break break
} }
@@ -615,7 +646,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
// Start offset we return here is only relevant when begin tiebreak is used. // Start offset we return here is only relevant when begin tiebreak is used.
// However finding the accurate offset requires backtracking, and we don't // However finding the accurate offset requires backtracking, and we don't
// want to pay extra cost for the option that has lost its importance. // want to pay extra cost for the option that has lost its importance.
return Result{j, maxScorePos + 1, int(maxScore)}, pos return Result{minIdx + j, minIdx + maxScorePos + 1, int(maxScore)}, pos
} }
// Implement the same sorting criteria as V2 // Implement the same sorting criteria as V2
@@ -645,7 +676,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
*pos = append(*pos, idx) *pos = append(*pos, idx)
} }
score += scoreMatch score += scoreMatch
bonus := bonusFor(prevClass, class) bonus := bonusMatrix[prevClass][class]
if consecutive == 0 { if consecutive == 0 {
firstBonus = bonus firstBonus = bonus
} else { } else {
@@ -683,7 +714,8 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
if len(pattern) == 0 { if len(pattern) == 0 {
return Result{0, 0, 0}, nil return Result{0, 0, 0}, nil
} }
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 { idx, _ := asciiFuzzyIndex(text, pattern, caseSensitive)
if idx < 0 {
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }
@@ -777,7 +809,8 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 { idx, _ := asciiFuzzyIndex(text, pattern, caseSensitive)
if idx < 0 {
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }

View File

@@ -9,6 +9,10 @@ import (
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
func init() {
Init("default")
}
func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) { func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) {
assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score) assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score)
} }

View File

@@ -3,7 +3,7 @@ package fzf
import ( import (
"fmt" "fmt"
"os" "sync"
"time" "time"
"unsafe" "unsafe"
@@ -29,6 +29,8 @@ func sbytes(data string) []byte {
// Run starts fzf // Run starts fzf
func Run(opts *Options, version string, revision string) { func Run(opts *Options, version string, revision string) {
defer util.RunAtExitFuncs()
sort := opts.Sort > 0 sort := opts.Sort > 0
sortCriteria = opts.Criteria sortCriteria = opts.Criteria
@@ -38,7 +40,7 @@ func Run(opts *Options, version string, revision string) {
} else { } else {
fmt.Println(version) fmt.Println(version)
} }
os.Exit(exitOk) util.Exit(exitOk)
} }
// Event channel // Event channel
@@ -163,14 +165,17 @@ func Run(opts *Options, version string, revision string) {
found := false found := false
if streamingFilter { if streamingFilter {
slab := util.MakeSlab(slab16Size, slab32Size) slab := util.MakeSlab(slab16Size, slab32Size)
mutex := sync.Mutex{}
reader := NewReader( reader := NewReader(
func(runes []byte) bool { func(runes []byte) bool {
item := Item{} item := Item{}
if chunkList.trans(&item, runes) { if chunkList.trans(&item, runes) {
mutex.Lock()
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil { if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
opts.Printer(item.text.ToString()) opts.Printer(item.text.ToString())
found = true found = true
} }
mutex.Unlock()
} }
return false return false
}, eventBox, opts.ReadZero, false) }, eventBox, opts.ReadZero, false)
@@ -189,9 +194,9 @@ func Run(opts *Options, version string, revision string) {
} }
} }
if found { if found {
os.Exit(exitOk) util.Exit(exitOk)
} }
os.Exit(exitNoMatch) util.Exit(exitNoMatch)
} }
// Synchronous search // Synchronous search
@@ -270,7 +275,7 @@ func Run(opts *Options, version string, revision string) {
if reading { if reading {
reader.terminate() reader.terminate()
} }
os.Exit(value.(int)) util.Exit(value.(int))
case EvtReadNew, EvtReadFin: case EvtReadNew, EvtReadFin:
if evt == EvtReadFin && nextCommand != nil { if evt == EvtReadFin && nextCommand != nil {
restart(*nextCommand, nextEnviron) restart(*nextCommand, nextEnviron)
@@ -372,9 +377,9 @@ func Run(opts *Options, version string, revision string) {
opts.Printer(val.Get(i).item.AsString(opts.Ansi)) opts.Printer(val.Get(i).item.AsString(opts.Ansi))
} }
if count > 0 { if count > 0 {
os.Exit(exitOk) util.Exit(exitOk)
} }
os.Exit(exitNoMatch) util.Exit(exitNoMatch)
} }
determine(val.final) determine(val.final)
} }

View File

@@ -52,7 +52,7 @@ const usage = `usage: fzf [options]
--hscroll-off=COLS Number of screen columns to keep to the right of the --hscroll-off=COLS Number of screen columns to keep to the right of the
highlighted substring (default: 10) highlighted substring (default: 10)
--filepath-word Make word-wise movements respect path separators --filepath-word Make word-wise movements respect path separators
--jump-labels=CHARS Label characters for jump and jump-accept --jump-labels=CHARS Label characters for jump mode
Layout Layout
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given --height=[~]HEIGHT[%] Display fzf window below the cursor with the given
@@ -363,6 +363,10 @@ type Options struct {
WalkerRoot string WalkerRoot string
WalkerSkip []string WalkerSkip []string
Version bool Version bool
CPUProfile string
MEMProfile string
BlockProfile string
MutexProfile string
} }
func filterNonEmpty(input []string) []string { func filterNonEmpty(input []string) []string {
@@ -454,14 +458,14 @@ func defaultOptions() *Options {
func help(code int) { func help(code int) {
os.Stdout.WriteString(usage) os.Stdout.WriteString(usage)
os.Exit(code) util.Exit(code)
} }
var errorContext = "" var errorContext = ""
func errorExit(msg string) { func errorExit(msg string) {
os.Stderr.WriteString(errorContext + msg + "\n") os.Stderr.WriteString(errorContext + msg + "\n")
os.Exit(exitError) util.Exit(exitError)
} }
func optString(arg string, prefixes ...string) (bool, string) { func optString(arg string, prefixes ...string) (bool, string) {
@@ -666,8 +670,8 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
add(tui.CtrlM) add(tui.CtrlM)
case "space": case "space":
chords[tui.Key(' ')] = key chords[tui.Key(' ')] = key
case "bspace", "bs": case "backspace", "bspace", "bs":
add(tui.BSpace) add(tui.Backspace)
case "ctrl-space": case "ctrl-space":
add(tui.CtrlSpace) add(tui.CtrlSpace)
case "ctrl-delete": case "ctrl-delete":
@@ -698,12 +702,16 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
add(tui.One) add(tui.One)
case "zero": case "zero":
add(tui.Zero) add(tui.Zero)
case "jump":
add(tui.Jump)
case "jump-cancel":
add(tui.JumpCancel)
case "alt-enter", "alt-return": case "alt-enter", "alt-return":
chords[tui.CtrlAltKey('m')] = key chords[tui.CtrlAltKey('m')] = key
case "alt-space": case "alt-space":
chords[tui.AltKey(' ')] = key chords[tui.AltKey(' ')] = key
case "alt-bs", "alt-bspace": case "alt-bs", "alt-bspace", "alt-backspace":
add(tui.AltBS) add(tui.AltBackspace)
case "alt-up": case "alt-up":
add(tui.AltUp) add(tui.AltUp)
case "alt-down": case "alt-down":
@@ -715,11 +723,11 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
case "tab": case "tab":
add(tui.Tab) add(tui.Tab)
case "btab", "shift-tab": case "btab", "shift-tab":
add(tui.BTab) add(tui.ShiftTab)
case "esc": case "esc":
add(tui.ESC) add(tui.Esc)
case "del": case "delete", "del":
add(tui.Del) add(tui.Delete)
case "home": case "home":
add(tui.Home) add(tui.Home)
case "end": case "end":
@@ -727,27 +735,27 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
case "insert": case "insert":
add(tui.Insert) add(tui.Insert)
case "pgup", "page-up": case "pgup", "page-up":
add(tui.PgUp) add(tui.PageUp)
case "pgdn", "page-down": case "pgdn", "page-down":
add(tui.PgDn) add(tui.PageDown)
case "alt-shift-up", "shift-alt-up": case "alt-shift-up", "shift-alt-up":
add(tui.AltSUp) add(tui.AltShiftUp)
case "alt-shift-down", "shift-alt-down": case "alt-shift-down", "shift-alt-down":
add(tui.AltSDown) add(tui.AltShiftDown)
case "alt-shift-left", "shift-alt-left": case "alt-shift-left", "shift-alt-left":
add(tui.AltSLeft) add(tui.AltShiftLeft)
case "alt-shift-right", "shift-alt-right": case "alt-shift-right", "shift-alt-right":
add(tui.AltSRight) add(tui.AltShiftRight)
case "shift-up": case "shift-up":
add(tui.SUp) add(tui.ShiftUp)
case "shift-down": case "shift-down":
add(tui.SDown) add(tui.ShiftDown)
case "shift-left": case "shift-left":
add(tui.SLeft) add(tui.ShiftLeft)
case "shift-right": case "shift-right":
add(tui.SRight) add(tui.ShiftRight)
case "shift-delete": case "shift-delete":
add(tui.SDelete) add(tui.ShiftDelete)
case "left-click": case "left-click":
add(tui.LeftClick) add(tui.LeftClick)
case "right-click": case "right-click":
@@ -1974,6 +1982,14 @@ func parseOptions(opts *Options, allArgs []string) {
opts.WalkerSkip = filterNonEmpty(strings.Split(nextString(allArgs, &i, "directory names to ignore required"), ",")) opts.WalkerSkip = filterNonEmpty(strings.Split(nextString(allArgs, &i, "directory names to ignore required"), ","))
case "--version": case "--version":
opts.Version = true opts.Version = true
case "--profile-cpu":
opts.CPUProfile = nextString(allArgs, &i, "file path required: cpu")
case "--profile-mem":
opts.MEMProfile = nextString(allArgs, &i, "file path required: mem")
case "--profile-block":
opts.BlockProfile = nextString(allArgs, &i, "file path required: block")
case "--profile-mutex":
opts.MutexProfile = nextString(allArgs, &i, "file path required: mutex")
case "--": case "--":
// Ignored // Ignored
default: default:
@@ -2243,10 +2259,8 @@ func postProcessOptions(opts *Options) {
theme.Spinner = boldify(theme.Spinner) theme.Spinner = boldify(theme.Spinner)
} }
if opts.Scheme != "default" {
processScheme(opts) processScheme(opts)
} }
}
func expectsArbitraryString(opt string) bool { func expectsArbitraryString(opt string) bool {
switch opt { switch opt {
@@ -2299,6 +2313,11 @@ func ParseOptions() *Options {
errorContext = "" errorContext = ""
parseOptions(opts, os.Args[1:]) parseOptions(opts, os.Args[1:])
if err := opts.initProfiling(); err != nil {
errorExit("failed to start pprof profiles: " + err.Error())
}
postProcessOptions(opts) postProcessOptions(opts)
return opts return opts
} }

11
src/options_no_pprof.go Normal file
View File

@@ -0,0 +1,11 @@
//go:build !pprof
// +build !pprof
package fzf
func (o *Options) initProfiling() error {
if o.CPUProfile != "" || o.MEMProfile != "" || o.BlockProfile != "" || o.MutexProfile != "" {
errorExit("error: profiling not supported: FZF must be built with '-tags=pprof' to enable profiling")
}
return nil
}

73
src/options_pprof.go Normal file
View File

@@ -0,0 +1,73 @@
//go:build pprof
// +build pprof
package fzf
import (
"fmt"
"os"
"runtime"
"runtime/pprof"
"github.com/junegunn/fzf/src/util"
)
func (o *Options) initProfiling() error {
if o.CPUProfile != "" {
f, err := os.Create(o.CPUProfile)
if err != nil {
return fmt.Errorf("could not create CPU profile: %w", err)
}
if err := pprof.StartCPUProfile(f); err != nil {
return fmt.Errorf("could not start CPU profile: %w", err)
}
util.AtExit(func() {
pprof.StopCPUProfile()
if err := f.Close(); err != nil {
fmt.Fprintln(os.Stderr, "Error: closing cpu profile:", err)
}
})
}
stopProfile := func(name string, f *os.File) {
if err := pprof.Lookup(name).WriteTo(f, 0); err != nil {
fmt.Fprintf(os.Stderr, "Error: could not write %s profile: %v\n", name, err)
}
if err := f.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Error: closing %s profile: %v\n", name, err)
}
}
if o.MEMProfile != "" {
f, err := os.Create(o.MEMProfile)
if err != nil {
return fmt.Errorf("could not create MEM profile: %w", err)
}
util.AtExit(func() {
runtime.GC()
stopProfile("allocs", f)
})
}
if o.BlockProfile != "" {
runtime.SetBlockProfileRate(1)
f, err := os.Create(o.BlockProfile)
if err != nil {
return fmt.Errorf("could not create BLOCK profile: %w", err)
}
util.AtExit(func() { stopProfile("block", f) })
}
if o.MutexProfile != "" {
runtime.SetMutexProfileFraction(1)
f, err := os.Create(o.MutexProfile)
if err != nil {
return fmt.Errorf("could not create MUTEX profile: %w", err)
}
util.AtExit(func() { stopProfile("mutex", f) })
}
return nil
}

89
src/options_pprof_test.go Normal file
View File

@@ -0,0 +1,89 @@
//go:build pprof
// +build pprof
package fzf
import (
"bytes"
"flag"
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/junegunn/fzf/src/util"
)
// runInitProfileTests is an internal flag used TestInitProfiling
var runInitProfileTests = flag.Bool("test-init-profile", false, "run init profile tests")
func TestInitProfiling(t *testing.T) {
if testing.Short() {
t.Skip("short test")
}
// Run this test in a separate process since it interferes with
// profiling and modifies the global atexit state. Without this
// running `go test -bench . -cpuprofile cpu.out` will fail.
if !*runInitProfileTests {
t.Parallel()
// Make sure we are not the child process.
if os.Getenv("_FZF_CHILD_PROC") != "" {
t.Fatal("already running as child process!")
}
cmd := exec.Command(os.Args[0],
"-test.timeout", "30s",
"-test.run", "^"+t.Name()+"$",
"-test-init-profile",
)
cmd.Env = append(os.Environ(), "_FZF_CHILD_PROC=1")
out, err := cmd.CombinedOutput()
out = bytes.TrimSpace(out)
if err != nil {
t.Fatalf("Child test process failed: %v:\n%s", err, out)
}
// Make sure the test actually ran
if bytes.Contains(out, []byte("no tests to run")) {
t.Fatalf("Failed to run test %q:\n%s", t.Name(), out)
}
return
}
// Child process
tempdir := t.TempDir()
t.Cleanup(util.RunAtExitFuncs)
o := Options{
CPUProfile: filepath.Join(tempdir, "cpu.prof"),
MEMProfile: filepath.Join(tempdir, "mem.prof"),
BlockProfile: filepath.Join(tempdir, "block.prof"),
MutexProfile: filepath.Join(tempdir, "mutex.prof"),
}
if err := o.initProfiling(); err != nil {
t.Fatal(err)
}
profiles := []string{
o.CPUProfile,
o.MEMProfile,
o.BlockProfile,
o.MutexProfile,
}
for _, name := range profiles {
if _, err := os.Stat(name); err != nil {
t.Errorf("Failed to create profile %s: %v", filepath.Base(name), err)
}
}
util.RunAtExitFuncs()
for _, name := range profiles {
if _, err := os.Stat(name); err != nil {
t.Errorf("Failed to write profile %s: %v", filepath.Base(name), err)
}
}
}

View File

@@ -170,8 +170,8 @@ func TestParseKeys(t *testing.T) {
check(tui.CtrlM, "Return") check(tui.CtrlM, "Return")
checkEvent(tui.Key(' '), "space") checkEvent(tui.Key(' '), "space")
check(tui.Tab, "tab") check(tui.Tab, "tab")
check(tui.BTab, "btab") check(tui.ShiftTab, "btab")
check(tui.ESC, "esc") check(tui.Esc, "esc")
check(tui.Up, "up") check(tui.Up, "up")
check(tui.Down, "down") check(tui.Down, "down")
check(tui.Left, "left") check(tui.Left, "left")
@@ -182,16 +182,16 @@ func TestParseKeys(t *testing.T) {
t.Error(11) t.Error(11)
} }
check(tui.Tab, "Ctrl-I") check(tui.Tab, "Ctrl-I")
check(tui.PgUp, "page-up") check(tui.PageUp, "page-up")
check(tui.PgDn, "Page-Down") check(tui.PageDown, "Page-Down")
check(tui.Home, "Home") check(tui.Home, "Home")
check(tui.End, "End") check(tui.End, "End")
check(tui.AltBS, "Alt-BSpace") check(tui.AltBackspace, "Alt-BSpace")
check(tui.SLeft, "shift-left") check(tui.ShiftLeft, "shift-left")
check(tui.SRight, "shift-right") check(tui.ShiftRight, "shift-right")
check(tui.BTab, "shift-tab") check(tui.ShiftTab, "shift-tab")
check(tui.CtrlM, "Enter") check(tui.CtrlM, "Enter")
check(tui.BSpace, "bspace") check(tui.Backspace, "bspace")
} }
func TestParseKeysWithComma(t *testing.T) { func TestParseKeysWithComma(t *testing.T) {

View File

@@ -173,6 +173,12 @@ func (r *Reader) feed(src io.Reader) {
} }
} else { } else {
// Could not find the delimiter in the buffer // Could not find the delimiter in the buffer
// NOTE: We can further optimize this by keeping track of the cursor
// position in the slab so that a straddling item that doesn't go
// beyond the boundary of a slab doesn't need to be copied to
// another buffer. However, the performance gain is negligible in
// practice (< 0.1%) and is not
// worth the added complexity.
leftover = append(leftover, buf...) leftover = append(leftover, buf...)
break break
} }

View File

@@ -293,6 +293,7 @@ type Terminal struct {
executing *util.AtomicBool executing *util.AtomicBool
termSize tui.TermSize termSize tui.TermSize
lastAction actionType lastAction actionType
lastKey string
lastFocus int32 lastFocus int32
areaLines int areaLines int
areaColumns int areaColumns int
@@ -408,7 +409,7 @@ const (
actOffsetUp actOffsetUp
actOffsetDown actOffsetDown
actJump actJump
actJumpAccept actJumpAccept // XXX Deprecated in favor of jump:accept binding
actPrintQuery actPrintQuery
actRefreshPreview actRefreshPreview
actReplaceQuery actReplaceQuery
@@ -460,14 +461,7 @@ const (
) )
func (a actionType) Name() string { func (a actionType) Name() string {
name := "" return util.ToKebabCase(a.String()[3:])
for i, r := range a.String()[3:] {
if i > 0 && r >= 'A' && r <= 'Z' {
name += "-"
}
name += string(r)
}
return strings.ToLower(name)
} }
func processExecution(action actionType) bool { func processExecution(action actionType) bool {
@@ -546,14 +540,14 @@ func defaultKeymap() map[tui.Event][]*action {
add(tui.CtrlC, actAbort) add(tui.CtrlC, actAbort)
add(tui.CtrlG, actAbort) add(tui.CtrlG, actAbort)
add(tui.CtrlQ, actAbort) add(tui.CtrlQ, actAbort)
add(tui.ESC, actAbort) add(tui.Esc, actAbort)
add(tui.CtrlD, actDeleteCharEof) add(tui.CtrlD, actDeleteCharEof)
add(tui.CtrlE, actEndOfLine) add(tui.CtrlE, actEndOfLine)
add(tui.CtrlF, actForwardChar) add(tui.CtrlF, actForwardChar)
add(tui.CtrlH, actBackwardDeleteChar) add(tui.CtrlH, actBackwardDeleteChar)
add(tui.BSpace, actBackwardDeleteChar) add(tui.Backspace, actBackwardDeleteChar)
add(tui.Tab, actToggleDown) add(tui.Tab, actToggleDown)
add(tui.BTab, actToggleUp) add(tui.ShiftTab, actToggleUp)
add(tui.CtrlJ, actDown) add(tui.CtrlJ, actDown)
add(tui.CtrlK, actUp) add(tui.CtrlK, actUp)
add(tui.CtrlL, actClearScreen) add(tui.CtrlL, actClearScreen)
@@ -568,11 +562,11 @@ func defaultKeymap() map[tui.Event][]*action {
} }
addEvent(tui.AltKey('b'), actBackwardWord) addEvent(tui.AltKey('b'), actBackwardWord)
add(tui.SLeft, actBackwardWord) add(tui.ShiftLeft, actBackwardWord)
addEvent(tui.AltKey('f'), actForwardWord) addEvent(tui.AltKey('f'), actForwardWord)
add(tui.SRight, actForwardWord) add(tui.ShiftRight, actForwardWord)
addEvent(tui.AltKey('d'), actKillWord) addEvent(tui.AltKey('d'), actKillWord)
add(tui.AltBS, actBackwardKillWord) add(tui.AltBackspace, actBackwardKillWord)
add(tui.Up, actUp) add(tui.Up, actUp)
add(tui.Down, actDown) add(tui.Down, actDown)
@@ -581,12 +575,12 @@ func defaultKeymap() map[tui.Event][]*action {
add(tui.Home, actBeginningOfLine) add(tui.Home, actBeginningOfLine)
add(tui.End, actEndOfLine) add(tui.End, actEndOfLine)
add(tui.Del, actDeleteChar) add(tui.Delete, actDeleteChar)
add(tui.PgUp, actPageUp) add(tui.PageUp, actPageUp)
add(tui.PgDn, actPageDown) add(tui.PageDown, actPageDown)
add(tui.SUp, actPreviewUp) add(tui.ShiftUp, actPreviewUp)
add(tui.SDown, actPreviewDown) add(tui.ShiftDown, actPreviewDown)
add(tui.Mouse, actMouse) add(tui.Mouse, actMouse)
add(tui.LeftClick, actClick) add(tui.LeftClick, actClick)
@@ -851,6 +845,7 @@ func (t *Terminal) environ() []string {
} }
env = append(env, "FZF_QUERY="+string(t.input)) env = append(env, "FZF_QUERY="+string(t.input))
env = append(env, "FZF_ACTION="+t.lastAction.Name()) env = append(env, "FZF_ACTION="+t.lastAction.Name())
env = append(env, "FZF_KEY="+t.lastKey)
env = append(env, "FZF_PROMPT="+string(t.promptString)) env = append(env, "FZF_PROMPT="+string(t.promptString))
env = append(env, "FZF_PREVIEW_LABEL="+t.previewLabelOpts.label) env = append(env, "FZF_PREVIEW_LABEL="+t.previewLabelOpts.label)
env = append(env, "FZF_BORDER_LABEL="+t.borderLabelOpts.label) env = append(env, "FZF_BORDER_LABEL="+t.borderLabelOpts.label)
@@ -3290,6 +3285,7 @@ func (t *Terminal) Loop() {
t.mutex.Lock() t.mutex.Lock()
previousInput := t.input previousInput := t.input
previousCx := t.cx previousCx := t.cx
t.lastKey = event.KeyName()
events := []util.EventType{} events := []util.EventType{}
req := func(evts ...util.EventType) { req := func(evts ...util.EventType) {
for _, event := range evts { for _, event := range evts {
@@ -4108,6 +4104,9 @@ func (t *Terminal) Loop() {
// Break out of jump mode if any action is submitted to the server // Break out of jump mode if any action is submitted to the server
if t.jumping != jumpDisabled { if t.jumping != jumpDisabled {
t.jumping = jumpDisabled t.jumping = jumpDisabled
if acts, prs := t.keymap[tui.JumpCancel.AsEvent()]; prs && !doActions(acts) {
continue
}
req(reqList) req(reqList)
} }
if len(actions) == 0 { if len(actions) == 0 {
@@ -4121,19 +4120,17 @@ func (t *Terminal) Loop() {
t.truncateQuery() t.truncateQuery()
queryChanged = string(previousInput) != string(t.input) queryChanged = string(previousInput) != string(t.input)
changed = changed || queryChanged changed = changed || queryChanged
if onChanges, prs := t.keymap[tui.Change.AsEvent()]; queryChanged && prs { if onChanges, prs := t.keymap[tui.Change.AsEvent()]; queryChanged && prs && !doActions(onChanges) {
if !doActions(onChanges) {
continue continue
} }
} if onEOFs, prs := t.keymap[tui.BackwardEOF.AsEvent()]; beof && prs && !doActions(onEOFs) {
if onEOFs, prs := t.keymap[tui.BackwardEOF.AsEvent()]; beof && prs {
if !doActions(onEOFs) {
continue continue
} }
}
} else { } else {
jumpEvent := tui.JumpCancel
if event.Type == tui.Rune { if event.Type == tui.Rune {
if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() { if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() {
jumpEvent = tui.Jump
t.cy = idx + t.offset t.cy = idx + t.offset
if t.jumping == jumpAcceptEnabled { if t.jumping == jumpAcceptEnabled {
req(reqClose) req(reqClose)
@@ -4141,6 +4138,9 @@ func (t *Terminal) Loop() {
} }
} }
t.jumping = jumpDisabled t.jumping = jumpDisabled
if acts, prs := t.keymap[jumpEvent.AsEvent()]; prs && !doActions(acts) {
continue
}
req(reqList) req(reqList)
} }

120
src/tui/eventtype_string.go Normal file
View File

@@ -0,0 +1,120 @@
// Code generated by "stringer -type=EventType"; DO NOT EDIT.
package tui
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[Rune-0]
_ = x[CtrlA-1]
_ = x[CtrlB-2]
_ = x[CtrlC-3]
_ = x[CtrlD-4]
_ = x[CtrlE-5]
_ = x[CtrlF-6]
_ = x[CtrlG-7]
_ = x[CtrlH-8]
_ = x[Tab-9]
_ = x[CtrlJ-10]
_ = x[CtrlK-11]
_ = x[CtrlL-12]
_ = x[CtrlM-13]
_ = x[CtrlN-14]
_ = x[CtrlO-15]
_ = x[CtrlP-16]
_ = x[CtrlQ-17]
_ = x[CtrlR-18]
_ = x[CtrlS-19]
_ = x[CtrlT-20]
_ = x[CtrlU-21]
_ = x[CtrlV-22]
_ = x[CtrlW-23]
_ = x[CtrlX-24]
_ = x[CtrlY-25]
_ = x[CtrlZ-26]
_ = x[Esc-27]
_ = x[CtrlSpace-28]
_ = x[CtrlDelete-29]
_ = x[CtrlBackSlash-30]
_ = x[CtrlRightBracket-31]
_ = x[CtrlCaret-32]
_ = x[CtrlSlash-33]
_ = x[ShiftTab-34]
_ = x[Backspace-35]
_ = x[Delete-36]
_ = x[PageUp-37]
_ = x[PageDown-38]
_ = x[Up-39]
_ = x[Down-40]
_ = x[Left-41]
_ = x[Right-42]
_ = x[Home-43]
_ = x[End-44]
_ = x[Insert-45]
_ = x[ShiftUp-46]
_ = x[ShiftDown-47]
_ = x[ShiftLeft-48]
_ = x[ShiftRight-49]
_ = x[ShiftDelete-50]
_ = x[F1-51]
_ = x[F2-52]
_ = x[F3-53]
_ = x[F4-54]
_ = x[F5-55]
_ = x[F6-56]
_ = x[F7-57]
_ = x[F8-58]
_ = x[F9-59]
_ = x[F10-60]
_ = x[F11-61]
_ = x[F12-62]
_ = x[AltBackspace-63]
_ = x[AltUp-64]
_ = x[AltDown-65]
_ = x[AltLeft-66]
_ = x[AltRight-67]
_ = x[AltShiftUp-68]
_ = x[AltShiftDown-69]
_ = x[AltShiftLeft-70]
_ = x[AltShiftRight-71]
_ = x[Alt-72]
_ = x[CtrlAlt-73]
_ = x[Invalid-74]
_ = x[Mouse-75]
_ = x[DoubleClick-76]
_ = x[LeftClick-77]
_ = x[RightClick-78]
_ = x[SLeftClick-79]
_ = x[SRightClick-80]
_ = x[ScrollUp-81]
_ = x[ScrollDown-82]
_ = x[SScrollUp-83]
_ = x[SScrollDown-84]
_ = x[PreviewScrollUp-85]
_ = x[PreviewScrollDown-86]
_ = x[Resize-87]
_ = x[Change-88]
_ = x[BackwardEOF-89]
_ = x[Start-90]
_ = x[Load-91]
_ = x[Focus-92]
_ = x[One-93]
_ = x[Zero-94]
_ = x[Result-95]
_ = x[Jump-96]
_ = x[JumpCancel-97]
}
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancel"
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 458, 467, 477, 487, 498, 506, 516, 525, 536, 551, 568, 574, 580, 591, 596, 600, 605, 608, 612, 618, 622, 632}
func (i EventType) String() string {
if i < 0 || i >= EventType(len(_EventType_index)-1) {
return "EventType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _EventType_name[_EventType_index[i]:_EventType_index[i+1]]
}

View File

@@ -71,7 +71,7 @@ func (r *LightRenderer) csi(code string) string {
func (r *LightRenderer) flush() { func (r *LightRenderer) flush() {
if r.queued.Len() > 0 { if r.queued.Len() > 0 {
fmt.Fprint(os.Stderr, "\x1b[?25l"+r.queued.String()+"\x1b[?25h") fmt.Fprint(os.Stderr, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h")
r.queued.Reset() r.queued.Reset()
} }
} }
@@ -245,7 +245,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
} }
retries := 0 retries := 0
if c == ESC.Int() || nonblock { if c == Esc.Int() || nonblock {
retries = r.escDelay / escPollInterval retries = r.escDelay / escPollInterval
} }
buffer = append(buffer, byte(c)) buffer = append(buffer, byte(c))
@@ -260,7 +260,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
continue continue
} }
break break
} else if c == ESC.Int() && pc != c { } else if c == Esc.Int() && pc != c {
retries = r.escDelay / escPollInterval retries = r.escDelay / escPollInterval
} else { } else {
retries = 0 retries = 0
@@ -300,7 +300,7 @@ func (r *LightRenderer) GetChar() Event {
case CtrlQ.Byte(): case CtrlQ.Byte():
return Event{CtrlQ, 0, nil} return Event{CtrlQ, 0, nil}
case 127: case 127:
return Event{BSpace, 0, nil} return Event{Backspace, 0, nil}
case 0: case 0:
return Event{CtrlSpace, 0, nil} return Event{CtrlSpace, 0, nil}
case 28: case 28:
@@ -311,7 +311,7 @@ func (r *LightRenderer) GetChar() Event {
return Event{CtrlCaret, 0, nil} return Event{CtrlCaret, 0, nil}
case 31: case 31:
return Event{CtrlSlash, 0, nil} return Event{CtrlSlash, 0, nil}
case ESC.Byte(): case Esc.Byte():
ev := r.escSequence(&sz) ev := r.escSequence(&sz)
// Second chance // Second chance
if ev.Type == Invalid { if ev.Type == Invalid {
@@ -327,7 +327,7 @@ func (r *LightRenderer) GetChar() Event {
} }
char, rsz := utf8.DecodeRune(r.buffer) char, rsz := utf8.DecodeRune(r.buffer)
if char == utf8.RuneError { if char == utf8.RuneError {
return Event{ESC, 0, nil} return Event{Esc, 0, nil}
} }
sz = rsz sz = rsz
return Event{Rune, char, nil} return Event{Rune, char, nil}
@@ -335,7 +335,7 @@ func (r *LightRenderer) GetChar() Event {
func (r *LightRenderer) escSequence(sz *int) Event { func (r *LightRenderer) escSequence(sz *int) Event {
if len(r.buffer) < 2 { if len(r.buffer) < 2 {
return Event{ESC, 0, nil} return Event{Esc, 0, nil}
} }
loc := offsetRegexpBegin.FindIndex(r.buffer) loc := offsetRegexpBegin.FindIndex(r.buffer)
@@ -349,15 +349,15 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return CtrlAltKey(rune(r.buffer[1] + 'a' - 1)) return CtrlAltKey(rune(r.buffer[1] + 'a' - 1))
} }
alt := false alt := false
if len(r.buffer) > 2 && r.buffer[1] == ESC.Byte() { if len(r.buffer) > 2 && r.buffer[1] == Esc.Byte() {
r.buffer = r.buffer[1:] r.buffer = r.buffer[1:]
alt = true alt = true
} }
switch r.buffer[1] { switch r.buffer[1] {
case ESC.Byte(): case Esc.Byte():
return Event{ESC, 0, nil} return Event{Esc, 0, nil}
case 127: case 127:
return Event{AltBS, 0, nil} return Event{AltBackspace, 0, nil}
case '[', 'O': case '[', 'O':
if len(r.buffer) < 3 { if len(r.buffer) < 3 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
@@ -386,7 +386,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
} }
return Event{Up, 0, nil} return Event{Up, 0, nil}
case 'Z': case 'Z':
return Event{BTab, 0, nil} return Event{ShiftTab, 0, nil}
case 'H': case 'H':
return Event{Home, 0, nil} return Event{Home, 0, nil}
case 'F': case 'F':
@@ -434,7 +434,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{Invalid, 0, nil} // INS return Event{Invalid, 0, nil} // INS
case '3': case '3':
if r.buffer[3] == '~' { if r.buffer[3] == '~' {
return Event{Del, 0, nil} return Event{Delete, 0, nil}
} }
if len(r.buffer) == 6 && r.buffer[5] == '~' { if len(r.buffer) == 6 && r.buffer[5] == '~' {
*sz = 6 *sz = 6
@@ -442,16 +442,16 @@ func (r *LightRenderer) escSequence(sz *int) Event {
case '5': case '5':
return Event{CtrlDelete, 0, nil} return Event{CtrlDelete, 0, nil}
case '2': case '2':
return Event{SDelete, 0, nil} return Event{ShiftDelete, 0, nil}
} }
} }
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
case '4': case '4':
return Event{End, 0, nil} return Event{End, 0, nil}
case '5': case '5':
return Event{PgUp, 0, nil} return Event{PageUp, 0, nil}
case '6': case '6':
return Event{PgDn, 0, nil} return Event{PageDown, 0, nil}
case '7': case '7':
return Event{Home, 0, nil} return Event{Home, 0, nil}
case '8': case '8':
@@ -489,16 +489,29 @@ func (r *LightRenderer) escSequence(sz *int) Event {
} }
*sz = 6 *sz = 6
switch r.buffer[4] { switch r.buffer[4] {
case '1', '2', '3', '5': case '1', '2', '3', '4', '5':
// Kitty iTerm2 WezTerm
// SHIFT-ARROW "\e[1;2D"
// ALT-SHIFT-ARROW "\e[1;4D" "\e[1;10D" "\e[1;4D"
// CTRL-SHIFT-ARROW "\e[1;6D" N/A
// CMD-SHIFT-ARROW "\e[1;10D" N/A N/A ("\e[1;2D")
alt := r.buffer[4] == '3' alt := r.buffer[4] == '3'
altShift := r.buffer[4] == '1' && r.buffer[5] == '0'
char := r.buffer[5] char := r.buffer[5]
if altShift { altShift := false
if r.buffer[4] == '1' && r.buffer[5] == '0' {
altShift = true
if len(r.buffer) < 7 { if len(r.buffer) < 7 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
*sz = 7 *sz = 7
char = r.buffer[6] char = r.buffer[6]
} else if r.buffer[4] == '4' {
altShift = true
if len(r.buffer) < 6 {
return Event{Invalid, 0, nil}
}
*sz = 6
char = r.buffer[5]
} }
switch char { switch char {
case 'A': case 'A':
@@ -506,33 +519,33 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{AltUp, 0, nil} return Event{AltUp, 0, nil}
} }
if altShift { if altShift {
return Event{AltSUp, 0, nil} return Event{AltShiftUp, 0, nil}
} }
return Event{SUp, 0, nil} return Event{ShiftUp, 0, nil}
case 'B': case 'B':
if alt { if alt {
return Event{AltDown, 0, nil} return Event{AltDown, 0, nil}
} }
if altShift { if altShift {
return Event{AltSDown, 0, nil} return Event{AltShiftDown, 0, nil}
} }
return Event{SDown, 0, nil} return Event{ShiftDown, 0, nil}
case 'C': case 'C':
if alt { if alt {
return Event{AltRight, 0, nil} return Event{AltRight, 0, nil}
} }
if altShift { if altShift {
return Event{AltSRight, 0, nil} return Event{AltShiftRight, 0, nil}
} }
return Event{SRight, 0, nil} return Event{ShiftRight, 0, nil}
case 'D': case 'D':
if alt { if alt {
return Event{AltLeft, 0, nil} return Event{AltLeft, 0, nil}
} }
if altShift { if altShift {
return Event{AltSLeft, 0, nil} return Event{AltShiftLeft, 0, nil}
} }
return Event{SLeft, 0, nil} return Event{ShiftLeft, 0, nil}
} }
} // r.buffer[4] } // r.buffer[4]
} // r.buffer[3] } // r.buffer[3]

View File

@@ -58,7 +58,7 @@ func openTtyIn() *os.File {
} }
} }
fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice) fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
os.Exit(2) util.Exit(2)
} }
return in return in
} }

View File

@@ -320,16 +320,16 @@ func (r *FullscreenRenderer) GetChar() Event {
switch ev.Rune() { switch ev.Rune() {
case 0: case 0:
if ctrl { if ctrl {
return Event{BSpace, 0, nil} return Event{Backspace, 0, nil}
} }
case rune(tcell.KeyCtrlH): case rune(tcell.KeyCtrlH):
switch { switch {
case ctrl: case ctrl:
return keyfn('h') return keyfn('h')
case alt: case alt:
return Event{AltBS, 0, nil} return Event{AltBackspace, 0, nil}
case none, shift: case none, shift:
return Event{BSpace, 0, nil} return Event{Backspace, 0, nil}
} }
} }
case tcell.KeyCtrlI: case tcell.KeyCtrlI:
@@ -382,17 +382,17 @@ func (r *FullscreenRenderer) GetChar() Event {
// section 3: (Alt)+Backspace2 // section 3: (Alt)+Backspace2
case tcell.KeyBackspace2: case tcell.KeyBackspace2:
if alt { if alt {
return Event{AltBS, 0, nil} return Event{AltBackspace, 0, nil}
} }
return Event{BSpace, 0, nil} return Event{Backspace, 0, nil}
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right) // section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
case tcell.KeyUp: case tcell.KeyUp:
if altShift { if altShift {
return Event{AltSUp, 0, nil} return Event{AltShiftUp, 0, nil}
} }
if shift { if shift {
return Event{SUp, 0, nil} return Event{ShiftUp, 0, nil}
} }
if alt { if alt {
return Event{AltUp, 0, nil} return Event{AltUp, 0, nil}
@@ -400,10 +400,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Up, 0, nil} return Event{Up, 0, nil}
case tcell.KeyDown: case tcell.KeyDown:
if altShift { if altShift {
return Event{AltSDown, 0, nil} return Event{AltShiftDown, 0, nil}
} }
if shift { if shift {
return Event{SDown, 0, nil} return Event{ShiftDown, 0, nil}
} }
if alt { if alt {
return Event{AltDown, 0, nil} return Event{AltDown, 0, nil}
@@ -411,10 +411,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Down, 0, nil} return Event{Down, 0, nil}
case tcell.KeyLeft: case tcell.KeyLeft:
if altShift { if altShift {
return Event{AltSLeft, 0, nil} return Event{AltShiftLeft, 0, nil}
} }
if shift { if shift {
return Event{SLeft, 0, nil} return Event{ShiftLeft, 0, nil}
} }
if alt { if alt {
return Event{AltLeft, 0, nil} return Event{AltLeft, 0, nil}
@@ -422,10 +422,10 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Left, 0, nil} return Event{Left, 0, nil}
case tcell.KeyRight: case tcell.KeyRight:
if altShift { if altShift {
return Event{AltSRight, 0, nil} return Event{AltShiftRight, 0, nil}
} }
if shift { if shift {
return Event{SRight, 0, nil} return Event{ShiftRight, 0, nil}
} }
if alt { if alt {
return Event{AltRight, 0, nil} return Event{AltRight, 0, nil}
@@ -442,17 +442,17 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{CtrlDelete, 0, nil} return Event{CtrlDelete, 0, nil}
} }
if shift { if shift {
return Event{SDelete, 0, nil} return Event{ShiftDelete, 0, nil}
} }
return Event{Del, 0, nil} return Event{Delete, 0, nil}
case tcell.KeyEnd: case tcell.KeyEnd:
return Event{End, 0, nil} return Event{End, 0, nil}
case tcell.KeyPgUp: case tcell.KeyPgUp:
return Event{PgUp, 0, nil} return Event{PageUp, 0, nil}
case tcell.KeyPgDn: case tcell.KeyPgDn:
return Event{PgDn, 0, nil} return Event{PageDown, 0, nil}
case tcell.KeyBacktab: case tcell.KeyBacktab:
return Event{BTab, 0, nil} return Event{ShiftTab, 0, nil}
case tcell.KeyF1: case tcell.KeyF1:
return Event{F1, 0, nil} return Event{F1, 0, nil}
case tcell.KeyF2: case tcell.KeyF2:
@@ -498,7 +498,7 @@ func (r *FullscreenRenderer) GetChar() Event {
// section 7: Esc // section 7: Esc
case tcell.KeyEsc: case tcell.KeyEsc:
return Event{ESC, 0, nil} return Event{Esc, 0, nil}
} }
} }

View File

@@ -102,22 +102,22 @@ func TestGetCharEventKey(t *testing.T) {
// KeyBackspace2 is alias for KeyDEL = 0x7F (ASCII) (allegedly unused by Windows) // KeyBackspace2 is alias for KeyDEL = 0x7F (ASCII) (allegedly unused by Windows)
// KeyDelete = 0x2E (VK_DELETE constant in Windows) // KeyDelete = 0x2E (VK_DELETE constant in Windows)
// KeyBackspace is alias for KeyBS = 0x08 (ASCII) (implicit alias with KeyCtrlH) // KeyBackspace is alias for KeyBS = 0x08 (ASCII) (implicit alias with KeyCtrlH)
{giveKey{tcell.KeyBackspace2, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated {giveKey{tcell.KeyBackspace2, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated
{giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // fabricated {giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // fabricated
{giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Del, 0, nil}}, {giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Delete, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Del, 0, nil}}, {giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Delete, 0, nil}},
{giveKey{tcell.KeyBackspace, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyBackspace, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyBS, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyBS, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyCtrlH, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyCtrlH, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{BSpace, 0, nil}}, // actual "Backspace" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{Backspace, 0, nil}}, // actual "Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Alt+Backspace" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Alt+Backspace" keystroke
{giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Backspace" keystroke {giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Shift+Backspace" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Shift+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+H" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+H" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Alt+H" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Alt+H" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+Shift+H" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+Shift+H" keystroke
@@ -126,8 +126,8 @@ func TestGetCharEventKey(t *testing.T) {
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right) // section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}}, {giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}}, {giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{SLeft, 0, nil}}, {giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{ShiftLeft, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltSRight, 0, nil}}, {giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftRight, 0, nil}},
{giveKey{tcell.KeyUpLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyUpLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyUpRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyUpRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyDownLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyDownLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
@@ -161,11 +161,11 @@ func TestGetCharEventKey(t *testing.T) {
// section 7: Esc // section 7: Esc
// KeyEsc and KeyEscape are aliases for KeyESC // KeyEsc and KeyEscape are aliases for KeyESC
{giveKey{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated {giveKey{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}, wantKey{Esc, 0, nil}}, // fabricated
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModNone}, wantKey{ESC, 0, nil}}, // unhandled {giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModNone}, wantKey{Esc, 0, nil}}, // unhandled
{giveKey{tcell.KeyEscape, rune(tcell.KeyEscape), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyEscape, rune(tcell.KeyEscape), tcell.ModNone}, wantKey{Esc, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // actual Ctrl+[ keystroke {giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModCtrl}, wantKey{Esc, 0, nil}}, // actual Ctrl+[ keystroke
{giveKey{tcell.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{Esc, 0, nil}}, // fabricated, unhandled
// section 8: Invalid // section 8: Invalid
{giveKey{tcell.KeyRune, 'a', tcell.ModMeta}, wantKey{Rune, 'a', nil}}, // fabricated {giveKey{tcell.KeyRune, 'a', tcell.ModMeta}, wantKey{Rune, 'a', nil}}, // fabricated
@@ -259,7 +259,7 @@ Quick reference
37 LeftClick 37 LeftClick
38 RightClick 38 RightClick
39 BTab 39 BTab
40 BSpace 40 Backspace
41 Del 41 Del
42 PgUp 42 PgUp
43 PgDn 43 PgDn
@@ -272,7 +272,7 @@ Quick reference
50 Insert 50 Insert
51 SUp 51 SUp
52 SDown 52 SDown
53 SLeft 53 ShiftLeft
54 SRight 54 SRight
55 F1 55 F1
56 F2 56 F2
@@ -288,15 +288,15 @@ Quick reference
66 F12 66 F12
67 Change 67 Change
68 BackwardEOF 68 BackwardEOF
69 AltBS 69 AltBackspace
70 AltUp 70 AltUp
71 AltDown 71 AltDown
72 AltLeft 72 AltLeft
73 AltRight 73 AltRight
74 AltSUp 74 AltSUp
75 AltSDown 75 AltSDown
76 AltSLeft 76 AltShiftLeft
77 AltSRight 77 AltShiftRight
78 Alt 78 Alt
79 CtrlAlt 79 CtrlAlt
.. ..

View File

@@ -6,10 +6,13 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
) )
// Types of user action // Types of user action
//
//go:generate stringer -type=EventType
type EventType int type EventType int
const ( const (
@@ -41,7 +44,7 @@ const (
CtrlX CtrlX
CtrlY CtrlY
CtrlZ CtrlZ
ESC Esc
CtrlSpace CtrlSpace
CtrlDelete CtrlDelete
@@ -51,27 +54,12 @@ const (
CtrlCaret CtrlCaret
CtrlSlash CtrlSlash
Invalid ShiftTab
Resize Backspace
Mouse
DoubleClick
LeftClick
RightClick
SLeftClick
SRightClick
ScrollUp
ScrollDown
SScrollUp
SScrollDown
PreviewScrollUp
PreviewScrollDown
BTab Delete
BSpace PageUp
PageDown
Del
PgUp
PgDn
Up Up
Down Down
@@ -81,11 +69,11 @@ const (
End End
Insert Insert
SUp ShiftUp
SDown ShiftDown
SLeft ShiftLeft
SRight ShiftRight
SDelete ShiftDelete
F1 F1
F2 F2
@@ -100,6 +88,38 @@ const (
F11 F11
F12 F12
AltBackspace
AltUp
AltDown
AltLeft
AltRight
AltShiftUp
AltShiftDown
AltShiftLeft
AltShiftRight
Alt
CtrlAlt
Invalid
Mouse
DoubleClick
LeftClick
RightClick
SLeftClick
SRightClick
ScrollUp
ScrollDown
SScrollUp
SScrollDown
PreviewScrollUp
PreviewScrollDown
// Events
Resize
Change Change
BackwardEOF BackwardEOF
Start Start
@@ -108,21 +128,8 @@ const (
One One
Zero Zero
Result Result
Jump
AltBS JumpCancel
AltUp
AltDown
AltLeft
AltRight
AltSUp
AltSDown
AltSLeft
AltSRight
Alt
CtrlAlt
) )
func (t EventType) AsEvent() Event { func (t EventType) AsEvent() Event {
@@ -142,6 +149,31 @@ func (e Event) Comparable() Event {
return Event{e.Type, e.Char, nil} return Event{e.Type, e.Char, nil}
} }
func (e Event) KeyName() string {
if e.Type >= Invalid {
return ""
}
switch e.Type {
case Rune:
return string(e.Char)
case Alt:
return "alt-" + string(e.Char)
case CtrlAlt:
return "ctrl-alt-" + string(e.Char)
case CtrlBackSlash:
return "ctrl-\\"
case CtrlRightBracket:
return "ctrl-]"
case CtrlCaret:
return "ctrl-^"
case CtrlSlash:
return "ctrl-/"
}
return util.ToKebabCase(e.Type.String())
}
func Key(r rune) Event { func Key(r rune) Event {
return Event{Rune, r, nil} return Event{Rune, r, nil}
} }
@@ -646,7 +678,7 @@ func NoColorTheme() *ColorTheme {
func errorExit(message string) { func errorExit(message string) {
fmt.Fprintln(os.Stderr, message) fmt.Fprintln(os.Stderr, message)
os.Exit(2) util.Exit(2)
} }
func init() { func init() {

38
src/util/atexit.go Normal file
View File

@@ -0,0 +1,38 @@
package util
import (
"os"
"sync"
)
var atExitFuncs []func()
// AtExit registers the function fn to be called on program termination.
// The functions will be called in reverse order they were registered.
func AtExit(fn func()) {
if fn == nil {
panic("AtExit called with nil func")
}
once := &sync.Once{}
atExitFuncs = append(atExitFuncs, func() {
once.Do(fn)
})
}
// RunAtExitFuncs runs any functions registered with AtExit().
func RunAtExitFuncs() {
fns := atExitFuncs
for i := len(fns) - 1; i >= 0; i-- {
fns[i]()
}
}
// Exit executes any functions registered with AtExit() then exits the program
// with os.Exit(code).
//
// NOTE: It must be used instead of os.Exit() since calling os.Exit() terminates
// the program before any of the AtExit functions can run.
func Exit(code int) {
defer os.Exit(code)
RunAtExitFuncs()
}

24
src/util/atexit_test.go Normal file
View File

@@ -0,0 +1,24 @@
package util
import (
"reflect"
"testing"
)
func TestAtExit(t *testing.T) {
want := []int{3, 2, 1, 0}
var called []int
for i := 0; i < 4; i++ {
n := i
AtExit(func() { called = append(called, n) })
}
RunAtExitFuncs()
if !reflect.DeepEqual(called, want) {
t.Errorf("AtExit: want call order: %v got: %v", want, called)
}
RunAtExitFuncs()
if !reflect.DeepEqual(called, want) {
t.Error("AtExit: should only call exit funcs once")
}
}

View File

@@ -178,12 +178,12 @@ func (chars *Chars) ToRunes() []rune {
return runes return runes
} }
func (chars *Chars) CopyRunes(dest []rune) { func (chars *Chars) CopyRunes(dest []rune, from int) {
if runes := chars.optionalRunes(); runes != nil { if runes := chars.optionalRunes(); runes != nil {
copy(dest, runes) copy(dest, runes[from:])
return return
} }
for idx, b := range chars.slice[:len(dest)] { for idx, b := range chars.slice[from:][:len(dest)] {
dest[idx] = rune(b) dest[idx] = rune(b)
} }
} }

View File

@@ -176,3 +176,15 @@ func RepeatToFill(str string, length int, limit int) string {
} }
return output return output
} }
// ToKebabCase converts the given CamelCase string to kebab-case
func ToKebabCase(s string) string {
name := ""
for i, r := range s {
if i > 0 && r >= 'A' && r <= 'Z' {
name += "-"
}
name += string(r)
}
return strings.ToLower(name)
}

View File

@@ -1468,6 +1468,19 @@ class TestGoFZF < TestBase
assert_equal '3', readonce.chomp assert_equal '3', readonce.chomp
end end
def test_jump_events
tmux.send_keys "seq 1000 | #{fzf("--multi --jump-labels 12345 --bind 'ctrl-j:jump,jump:preview(echo jumped to {}),jump-cancel:preview(echo jump cancelled at {})'")}", :Enter
tmux.until { |lines| assert_equal ' 1000/1000 (0)', lines[-2] }
tmux.send_keys 'C-j'
tmux.until { |lines| assert_includes lines[-7], '5 5' }
tmux.send_keys '3'
tmux.until { |lines| assert(lines.any? { _1.include?('jumped to 3') }) }
tmux.send_keys 'C-j'
tmux.until { |lines| assert_includes lines[-7], '5 5' }
tmux.send_keys 'C-c'
tmux.until { |lines| assert(lines.any? { _1.include?('jump cancelled at 3') }) }
end
def test_pointer def test_pointer
tmux.send_keys "seq 10 | #{fzf("--pointer '>>'")}", :Enter tmux.send_keys "seq 10 | #{fzf("--pointer '>>'")}", :Enter
# Assert that specified pointer is displayed # Assert that specified pointer is displayed