m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-11 12:53:48 -05:00

Compare commits

...

24 Commits

Author SHA1 Message Date
Junegunn Choi
bcda25a513 0.52.0 2024-05-08 00:15:30 +09:00
Junegunn Choi
8256fcde15 Minor fixup 2024-05-07 23:49:30 +09:00
Junegunn Choi
af65aa298a Add color names: selected-{fg,bg,hl} 2024-05-07 23:38:06 +09:00
Junegunn Choi
6834d17844 [vim] Revert 7411da8d5a
Fix #3777
2024-05-07 20:16:18 +09:00
Junegunn Choi
ed511d7867 [install] tar --no-same-owner
Close #3776
2024-05-07 20:00:13 +09:00
Junegunn Choi
cd8d736a9f [shell] Add $FZF_COMPLETION_{DIR,PATH}_OPTS
To allow separately overriding 'walker' options.

Close #3778
2024-05-07 19:31:56 +09:00
Junegunn Choi
0952b2dfd4 Rename --cursor-line to --highlight-line 2024-05-07 19:22:39 +09:00
Junegunn Choi
4bedd33c59 Refactor the code to remove global variables 2024-05-07 16:58:17 +09:00
Junegunn Choi
c5fb0c43f9 Add --cursor-line to highlight the whole current line
Similar to 'set cursorline' of Vim.
2024-05-07 01:34:35 +09:00
Junegunn Choi
9e4780510e Add current-{fg,bg,hl} as synonyms for {fg,bg,hl}+ 2024-05-07 01:26:25 +09:00
Junegunn Choi
e8405f40fe Refactor the code so that fzf can be used as a library (#3769) 2024-05-07 01:06:42 +09:00
dependabot[bot]
065b9e6fb2 Bump golang.org/x/term from 0.19.0 to 0.20.0 (#3774)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.19.0 to 0.20.0.
- [Commits](https://github.com/golang/term/compare/v0.19.0...v0.20.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-05-06 22:51:10 +09:00
dependabot[bot]
98141ca7d8 Bump crate-ci/typos from 1.20.10 to 1.21.0 (#3772)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.20.10 to 1.21.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.20.10...v1.21.0)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  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-05-06 22:50:50 +09:00
Junegunn Choi
501577ab28 Fix flaky test case 2024-05-06 13:59:16 +09:00
Junegunn Choi
5669f48343 Do not enable delayed expansion mode when running cmd.exe
And simplify the argument escaping code. Fix #3764.

This may breaks some existing use cases, but the mode causes too much
trouble when escaping arguments and it makes some things not possible.

  # Now you can pass special characters to rg process without any escaping problems: &|<>()@^%!
  fzf --ansi --disabled --bind "change:reload:rg --column --line-number --no-heading --color=always --smart-case -- {q}"

  # No sudden expansion of the arguments on '!'
  fzf --disabled --preview "echo {q} {n} {}" --query "&|<>()@^%!" --prompt "&|<>()@^%!"
2024-05-06 13:46:06 +09:00
Junegunn Choi
24ff66d4a9 Fix change-preview reset by change-preview-window
Fix #3770
2024-05-06 09:40:02 +09:00
Junegunn Choi
bf184449bc Count $FZF_CLICK_HEADER_LINE from top to bottom
Regardless of `--layout`.

https://github.com/junegunn/fzf/pull/3768#issuecomment-2094806558
2024-05-06 09:27:58 +09:00
Kuremu
7b98c2c653 Add click-header event for reporting clicks within header (#3768)
Sets $FZF_CLICK_HEADER_LINE and $FZF_CLICK_HEADER_COLUMN env vars with
coordinates of the last click inside and relative to the header and
fires click-header event.

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-05-05 18:56:43 +09:00
Junegunn Choi
b6add2a257 Fix rendering of preview window border of tcell renderer
(sleep 1; find .) |
    go run -tags tcell main.go --bind 'space:change-preview-window(60%|70%|80%|90%|border-left|border-right|border-vertical|border-top|border-horizontal|border-bottom|border-sharp|border-double|border-block|hidden|left|up|down|right|up|down|)' \
        --preview 'cat {}' --color bg:red,preview-bg:blue \
        --border --margin 3
2024-05-05 17:09:00 +09:00
Junegunn Choi
2bd41f1330 Reduce flicking when changing the size of the preview window with --border
(sleep 1; find .) |
    fzf --bind 'space:change-preview-window(60%|70%|80%|90%|border-left|border-right|border-vertical|border-top|border-horizontal|border-bottom|border-sharp|border-double|border-block|hidden|left|up|down|right|up|down|)' \
        --preview 'cat {}' --color bg:red,preview-bg:blue \
        --border --margin 3
2024-05-05 16:49:30 +09:00
Junegunn Choi
c37cd11ca5 Remove unnecessary flicking when changing the size of the preview window
fzf --bind 'space:change-preview-window(60%|70%|80%|90%|hidden|)' --preview 'cat {}'
2024-05-05 11:10:54 +09:00
Junegunn Choi
9dee8edc0c Clear characters on 1-column margin after the preview window on the left 2024-05-05 11:06:50 +09:00
Junegunn Choi
f6aa28c380 Fix --info inline-right not properly clearing the previous output
(seq 100000; sleep 1) | fzf --info inline-right --bind load:change-query:x
2024-05-03 12:18:34 +09:00
cyqsimon
dba1644518 Fix unreliable GOOS detection (#3763)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-05-02 20:52:27 +09:00
46 changed files with 1497 additions and 1068 deletions

View File

@@ -7,4 +7,4 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: crate-ci/typos@v1.20.10 - uses: crate-ci/typos@v1.21.0

View File

@@ -1,6 +1,34 @@
CHANGELOG CHANGELOG
========= =========
0.52.0
------
- Added `--highlight-line` to highlight the whole current line (à la `set cursorline` of Vim)
- Added color names for selected lines: `selected-fg`, `selected-bg`, and `selected-hl`
```sh
fzf --border --multi --info inline-right --layout reverse --marker ▏ --pointer ▌ --prompt '▌ ' \
--highlight-line --color gutter:-1,selected-bg:238,selected-fg:146,current-fg:189
```
- Added `click-header` event that is triggered when the header section is clicked. When the event is triggered, `$FZF_CLICK_HEADER_COLUMN` and `$FZF_CLICK_HEADER_LINE` are set.
```sh
fd --type f |
fzf --header $'[Files] [Directories]' --header-first \
--bind 'click-header:transform:
(( FZF_CLICK_HEADER_COLUMN <= 7 )) && echo "reload(fd --type f)"
(( FZF_CLICK_HEADER_COLUMN >= 9 )) && echo "reload(fd --type d)"
'
```
- Add `$FZF_COMPLETION_{DIR,PATH}_OPTS` for separately customizing the behavior of fuzzy completion
```sh
# Set --walker options without 'follow' not to follow symbolic links
FZF_COMPLETION_PATH_OPTS="--walker=file,dir,hidden"
FZF_COMPLETION_DIR_OPTS="--walker=dir,hidden"
```
- Fixed Windows argument escaping
- Bug fixes and improvements
- The code was heavily refactored to allow using fzf as a library in Go programs. The API is still experimental and subject to change.
- https://gist.github.com/junegunn/193990b65be48a38aac6ac49d5669170
0.51.0 0.51.0
------ ------
- Added a new environment variable `$FZF_POS` exported to the child processes. It's the vertical position of the cursor in the list starting from 1. - Added a new environment variable `$FZF_POS` exported to the child processes. It's the vertical position of the cursor in the list starting from 1.

View File

@@ -1,6 +1,6 @@
SHELL := bash SHELL := bash
GO ?= go GO ?= go
GOOS ?= $(word 1, $(subst /, " ", $(word 4, $(shell go version)))) GOOS ?= $(shell $(GO) env GOOS)
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST))) MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
ROOT_DIR := $(shell dirname $(MAKEFILE)) ROOT_DIR := $(shell dirname $(MAKEFILE))
@@ -79,7 +79,6 @@ 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 \

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.19.0 golang.org/x/sys v0.20.0
golang.org/x/term v0.19.0 golang.org/x/term v0.20.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.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.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.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
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.51.0 version=0.52.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -115,7 +115,7 @@ link_fzf_in_path() {
try_curl() { try_curl() {
command -v curl > /dev/null && command -v curl > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then if [[ $1 =~ tar.gz$ ]]; then
curl -fL $1 | tar -xzf - curl -fL $1 | tar --no-same-owner -xzf -
else else
local temp=${TMPDIR:-/tmp}/fzf.zip local temp=${TMPDIR:-/tmp}/fzf.zip
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp" curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
@@ -125,7 +125,7 @@ try_curl() {
try_wget() { try_wget() {
command -v wget > /dev/null && command -v wget > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then if [[ $1 =~ tar.gz$ ]]; then
wget -O - $1 | tar -xzf - wget -O - $1 | tar --no-same-owner -xzf -
else else
local temp=${TMPDIR:-/tmp}/fzf.zip local temp=${TMPDIR:-/tmp}/fzf.zip
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp" wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"

View File

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

35
main.go
View File

@@ -3,14 +3,15 @@ package main
import ( import (
_ "embed" _ "embed"
"fmt" "fmt"
"os"
"strings" "strings"
fzf "github.com/junegunn/fzf/src" fzf "github.com/junegunn/fzf/src"
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version string = "0.51" var version = "0.52"
var revision string = "devel" var revision = "devel"
//go:embed shell/key-bindings.bash //go:embed shell/key-bindings.bash
var bashKeyBindings []byte var bashKeyBindings []byte
@@ -33,9 +34,21 @@ func printScript(label string, content []byte) {
fmt.Println("### end: " + label + " ###") fmt.Println("### end: " + label + " ###")
} }
func exit(code int, err error) {
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
}
os.Exit(code)
}
func main() { func main() {
protector.Protect() protector.Protect()
options := fzf.ParseOptions()
options, err := fzf.ParseOptions(true, os.Args[1:])
if err != nil {
exit(fzf.ExitError, err)
return
}
if options.Bash { if options.Bash {
printScript("key-bindings.bash", bashKeyBindings) printScript("key-bindings.bash", bashKeyBindings)
printScript("completion.bash", bashCompletion) printScript("completion.bash", bashCompletion)
@@ -51,5 +64,19 @@ func main() {
fmt.Println("fzf_key_bindings") fmt.Println("fzf_key_bindings")
return return
} }
fzf.Run(options, version, revision) if options.Help {
fmt.Print(fzf.Usage)
return
}
if options.Version {
if len(revision) > 0 {
fmt.Printf("%s (%s)\n", version, revision)
} else {
fmt.Println(version)
}
return
}
code, err := fzf.Run(options)
exit(code, err)
} }

View File

@@ -1,174 +0,0 @@
package main
import (
"bytes"
"fmt"
"go/ast"
"go/build"
"go/importer"
"go/parser"
"go/token"
"go/types"
"io/fs"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"testing"
)
func loadPackages(t *testing.T) []*build.Package {
// If GOROOT is not set, use `go env GOROOT` to determine it since it
// performs more work than just runtime.GOROOT(). For context, running
// the tests with the "-trimpath" flag causes GOROOT to not be set.
ctxt := &build.Default
if ctxt.GOROOT == "" {
cmd := exec.Command("go", "env", "GOROOT")
out, err := cmd.CombinedOutput()
out = bytes.TrimSpace(out)
if err != nil {
t.Fatalf("error running command: %q: %v\n%s", cmd.Args, err, out)
}
ctxt.GOROOT = string(out)
}
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 := ctxt.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 "May 2024" "fzf 0.51.0" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "May 2024" "fzf 0.52.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 "May 2024" "fzf 0.51.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "May 2024" "fzf 0.52.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -464,14 +464,17 @@ color mappings.
.B COLOR NAMES: .B COLOR NAMES:
\fBfg \fRText \fBfg \fRText
\fBselected-fg \fRSelected line text
\fBpreview-fg \fRPreview window text \fBpreview-fg \fRPreview window text
\fBbg \fRBackground \fBbg \fRBackground
\fBselected-bg \fRSelected line background
\fBpreview-bg \fRPreview window background \fBpreview-bg \fRPreview window background
\fBhl \fRHighlighted substrings \fBhl \fRHighlighted substrings
\fBfg+ \fRText (current line) \fBselected-hl \fRHighlighted substrings in the selected line
\fBbg+ \fRBackground (current line) \fBcurrent-fg (fg+) \fRText (current line)
\fBcurrent-bg (bg+) \fRBackground (current line)
\fBgutter \fRGutter on the left \fBgutter \fRGutter on the left
\fBhl+ \fRHighlighted substrings (current line) \fBcurrent-hl (hl+) \fRHighlighted substrings (current line)
\fBquery \fRQuery string \fBquery \fRQuery string
\fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR) \fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR)
\fBinfo \fRInfo line (match counters) \fBinfo \fRInfo line (match counters)
@@ -534,6 +537,9 @@ color mappings.
--color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR --color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR
.RE .RE
.TP .TP
.B "--highlight-line"
Highlight the whole current line
.TP
.B "--no-bold" .B "--no-bold"
Do not use bold text Do not use bold text
.TP .TP
@@ -821,7 +827,7 @@ e.g. \fBfzf --multi | fzf --sync\fR
.B "--with-shell=STR" .B "--with-shell=STR"
Shell command and flags to start child processes with. On *nix Systems, the Shell command and flags to start child processes with. On *nix Systems, the
default value is \fB$SHELL -c\fR if \fB$SHELL\fR is set, otherwise \fBsh -c\fR. default value is \fB$SHELL -c\fR if \fB$SHELL\fR is set, otherwise \fBsh -c\fR.
On Windows, the default value is \fBcmd /v:on/s/c\fR when \fB$SHELL\fR is not On Windows, the default value is \fBcmd /s/c\fR when \fB$SHELL\fR is not
set. set.
.RS .RS
@@ -1278,6 +1284,15 @@ e.g.
\fBfzf --bind space:jump,jump:accept,jump-cancel:abort\fR \fBfzf --bind space:jump,jump:accept,jump-cancel:abort\fR
.RE .RE
\fIclick-header\fR
.RS
Triggered when a mouse click occurs within the header. Sets \fBFZF_CLICK_HEADER_LINE\fR and \fBFZF_CLICK_HEADER_COLUMN\fR environment variables starting from 1.
e.g.
\fBprintf "head1\\nhead2" | fzf --header-lines=2 --bind 'click-header:transform-prompt:printf ${FZF_CLICK_HEADER_LINE}x${FZF_CLICK_HEADER_COLUMN}'\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.

View File

@@ -59,12 +59,9 @@ if s:is_win
return iconv(a:str, &encoding, 'cp'.s:codepage) return iconv(a:str, &encoding, 'cp'.s:codepage)
endfunction endfunction
function! s:wrap_cmds(cmds) function! s:wrap_cmds(cmds)
return map([ return map(['@echo off']
\ '@echo off',
\ 'setlocal enabledelayedexpansion']
\ + (has('gui_running') ? ['set TERM= > nul'] : []) \ + (has('gui_running') ? ['set TERM= > nul'] : [])
\ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds]) \ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds]),
\ + ['endlocal'],
\ '<SID>enc_to_cp(v:val."\r")') \ '<SID>enc_to_cp(v:val."\r")')
endfunction endfunction
else else
@@ -83,8 +80,6 @@ else
endfunction endfunction
endif endif
let s:cmd_control_chars = ['&', '|', '<', '>', '(', ')', '@', '^', '!']
function! s:shellesc_cmd(arg) function! s:shellesc_cmd(arg)
let e = '"' let e = '"'
let slashes = 0 let slashes = 0
@@ -94,10 +89,6 @@ function! s:shellesc_cmd(arg)
elseif c ==# '"' elseif c ==# '"'
let e .= repeat('\', slashes + 1) let e .= repeat('\', slashes + 1)
let slashes = 0 let slashes = 0
elseif c ==# '%'
let e .= '%'
elseif index(s:cmd_control_chars, c) >= 0
let e .= '^'
else else
let slashes = 0 let slashes = 0
endif endif
@@ -517,19 +508,19 @@ try
endif endif
if has_key(dict, 'source') if has_key(dict, 'source')
let source = remove(dict, 'source') let source = dict.source
let type = type(source) let type = type(source)
if type == 1 if type == 1
let source_command = source let prefix = '('.source.')|'
elseif type == 3 elseif type == 3
let temps.input = s:fzf_tempname() let temps.input = s:fzf_tempname()
call s:writefile(source, temps.input) call s:writefile(source, temps.input)
let source_command = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input) let prefix = (s:is_win ? 'type ' : 'command cat ').fzf#shellescape(temps.input).'|'
else else
throw 'Invalid source type' throw 'Invalid source type'
endif endif
else else
let source_command = '' let prefix = ''
endif endif
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) || has_key(dict, 'tmux') let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) || has_key(dict, 'tmux')
@@ -553,11 +544,7 @@ try
endif endif
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options' " Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr]) let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
let prev_default_command = $FZF_DEFAULT_COMMAND let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if len(source_command)
let $FZF_DEFAULT_COMMAND = source_command
endif
let command = (use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if use_term if use_term
return s:execute_term(dict, command, temps) return s:execute_term(dict, command, temps)
@@ -568,14 +555,6 @@ try
call s:callback(dict, lines) call s:callback(dict, lines)
return lines return lines
finally finally
if exists('source_command') && len(source_command)
if len(prev_default_command)
let $FZF_DEFAULT_COMMAND = prev_default_command
else
let $FZF_DEFAULT_COMMAND = ''
silent! execute 'unlet $FZF_DEFAULT_COMMAND'
endif
endif
let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote] let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote]
endtry endtry
endfunction endfunction
@@ -605,8 +584,8 @@ function! s:fzf_tmux(dict)
endif endif
endfor endfor
endif endif
return printf('LINES=%d COLUMNS=%d %s %s - --', return printf('LINES=%d COLUMNS=%d %s %s %s --',
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size) \ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
endfunction endfunction
function! s:splittable(dict) function! s:splittable(dict)
@@ -736,7 +715,8 @@ function! s:execute(dict, command, use_height, temps) abort
let a:temps.shellscript = shellscript let a:temps.shellscript = shellscript
endif endif
if a:use_height if a:use_height
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s < /dev/tty 2> /dev/tty', &lines, command)) let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty'
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin))
else else
execute 'silent !'.command execute 'silent !'.command
endif endif

View File

@@ -4,10 +4,12 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ completion.bash # /_/ /___/_/ completion.bash
# #
# - $FZF_TMUX (default: 0) # - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: empty) # - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
# - $FZF_COMPLETION_DIR_OPTS (default: empty)
if [[ $- =~ i ]]; then if [[ $- =~ i ]]; then
@@ -297,8 +299,14 @@ __fzf_generic_path_completion() {
if declare -F "$1" > /dev/null; then if declare -F "$1" > /dev/null; then
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover" eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover"
else else
[[ $1 =~ dir ]] && walker=dir,follow || walker=file,dir,follow,hidden if [[ $1 =~ dir ]]; then
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" walker=dir,follow
rest=${FZF_COMPLETION_DIR_OPTS-}
else
walker=file,dir,follow,hidden
rest=${FZF_COMPLETION_PATH_OPTS-}
fi
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" $rest
fi | while read -r item; do fi | while read -r item; do
printf "%q " "${item%$3}$3" printf "%q " "${item%$3}$3"
done done

View File

@@ -4,10 +4,12 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ completion.zsh # /_/ /___/_/ completion.zsh
# #
# - $FZF_TMUX (default: 0) # - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: '-d 40%') # - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
# - $FZF_COMPLETION_DIR_OPTS (default: empty)
# Both branches of the following `if` do the same thing -- define # Both branches of the following `if` do the same thing -- define
@@ -160,8 +162,14 @@ __fzf_generic_path_completion() {
if declare -f "$compgen" > /dev/null; then if declare -f "$compgen" > /dev/null; then
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
else else
[[ $compgen =~ dir ]] && walker=dir,follow || walker=file,dir,follow,hidden if [[ $compgen =~ dir ]]; then
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" < /dev/tty walker=dir,follow
rest=${FZF_COMPLETION_DIR_OPTS-}
else
walker=file,dir,follow,hidden
rest=${FZF_COMPLETION_PATH_OPTS-}
fi
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
fi | while read item; do fi | while read item; do
item="${item%$suffix}$suffix" item="${item%$suffix}$suffix"
echo -n "${(q)item} " echo -n "${(q)item} "

View File

@@ -37,92 +37,93 @@ func _() {
_ = x[actDeleteChar-26] _ = x[actDeleteChar-26]
_ = x[actDeleteCharEof-27] _ = x[actDeleteCharEof-27]
_ = x[actEndOfLine-28] _ = x[actEndOfLine-28]
_ = x[actForwardChar-29] _ = x[actFatal-29]
_ = x[actForwardWord-30] _ = x[actForwardChar-30]
_ = x[actKillLine-31] _ = x[actForwardWord-31]
_ = x[actKillWord-32] _ = x[actKillLine-32]
_ = x[actUnixLineDiscard-33] _ = x[actKillWord-33]
_ = x[actUnixWordRubout-34] _ = x[actUnixLineDiscard-34]
_ = x[actYank-35] _ = x[actUnixWordRubout-35]
_ = x[actBackwardKillWord-36] _ = x[actYank-36]
_ = x[actSelectAll-37] _ = x[actBackwardKillWord-37]
_ = x[actDeselectAll-38] _ = x[actSelectAll-38]
_ = x[actToggle-39] _ = x[actDeselectAll-39]
_ = x[actToggleSearch-40] _ = x[actToggle-40]
_ = x[actToggleAll-41] _ = x[actToggleSearch-41]
_ = x[actToggleDown-42] _ = x[actToggleAll-42]
_ = x[actToggleUp-43] _ = x[actToggleDown-43]
_ = x[actToggleIn-44] _ = x[actToggleUp-44]
_ = x[actToggleOut-45] _ = x[actToggleIn-45]
_ = x[actToggleTrack-46] _ = x[actToggleOut-46]
_ = x[actToggleTrackCurrent-47] _ = x[actToggleTrack-47]
_ = x[actToggleHeader-48] _ = x[actToggleTrackCurrent-48]
_ = x[actTrackCurrent-49] _ = x[actToggleHeader-49]
_ = x[actUntrackCurrent-50] _ = x[actTrackCurrent-50]
_ = x[actDown-51] _ = x[actUntrackCurrent-51]
_ = x[actUp-52] _ = x[actDown-52]
_ = x[actPageUp-53] _ = x[actUp-53]
_ = x[actPageDown-54] _ = x[actPageUp-54]
_ = x[actPosition-55] _ = x[actPageDown-55]
_ = x[actHalfPageUp-56] _ = x[actPosition-56]
_ = x[actHalfPageDown-57] _ = x[actHalfPageUp-57]
_ = x[actOffsetUp-58] _ = x[actHalfPageDown-58]
_ = x[actOffsetDown-59] _ = x[actOffsetUp-59]
_ = x[actJump-60] _ = x[actOffsetDown-60]
_ = x[actJumpAccept-61] _ = x[actJump-61]
_ = x[actPrintQuery-62] _ = x[actJumpAccept-62]
_ = x[actRefreshPreview-63] _ = x[actPrintQuery-63]
_ = x[actReplaceQuery-64] _ = x[actRefreshPreview-64]
_ = x[actToggleSort-65] _ = x[actReplaceQuery-65]
_ = x[actShowPreview-66] _ = x[actToggleSort-66]
_ = x[actHidePreview-67] _ = x[actShowPreview-67]
_ = x[actTogglePreview-68] _ = x[actHidePreview-68]
_ = x[actTogglePreviewWrap-69] _ = x[actTogglePreview-69]
_ = x[actTransform-70] _ = x[actTogglePreviewWrap-70]
_ = x[actTransformBorderLabel-71] _ = x[actTransform-71]
_ = x[actTransformHeader-72] _ = x[actTransformBorderLabel-72]
_ = x[actTransformPreviewLabel-73] _ = x[actTransformHeader-73]
_ = x[actTransformPrompt-74] _ = x[actTransformPreviewLabel-74]
_ = x[actTransformQuery-75] _ = x[actTransformPrompt-75]
_ = x[actPreview-76] _ = x[actTransformQuery-76]
_ = x[actChangePreview-77] _ = x[actPreview-77]
_ = x[actChangePreviewWindow-78] _ = x[actChangePreview-78]
_ = x[actPreviewTop-79] _ = x[actChangePreviewWindow-79]
_ = x[actPreviewBottom-80] _ = x[actPreviewTop-80]
_ = x[actPreviewUp-81] _ = x[actPreviewBottom-81]
_ = x[actPreviewDown-82] _ = x[actPreviewUp-82]
_ = x[actPreviewPageUp-83] _ = x[actPreviewDown-83]
_ = x[actPreviewPageDown-84] _ = x[actPreviewPageUp-84]
_ = x[actPreviewHalfPageUp-85] _ = x[actPreviewPageDown-85]
_ = x[actPreviewHalfPageDown-86] _ = x[actPreviewHalfPageUp-86]
_ = x[actPrevHistory-87] _ = x[actPreviewHalfPageDown-87]
_ = x[actPrevSelected-88] _ = x[actPrevHistory-88]
_ = x[actPut-89] _ = x[actPrevSelected-89]
_ = x[actNextHistory-90] _ = x[actPut-90]
_ = x[actNextSelected-91] _ = x[actNextHistory-91]
_ = x[actExecute-92] _ = x[actNextSelected-92]
_ = x[actExecuteSilent-93] _ = x[actExecute-93]
_ = x[actExecuteMulti-94] _ = x[actExecuteSilent-94]
_ = x[actSigStop-95] _ = x[actExecuteMulti-95]
_ = x[actFirst-96] _ = x[actSigStop-96]
_ = x[actLast-97] _ = x[actFirst-97]
_ = x[actReload-98] _ = x[actLast-98]
_ = x[actReloadSync-99] _ = x[actReload-99]
_ = x[actDisableSearch-100] _ = x[actReloadSync-100]
_ = x[actEnableSearch-101] _ = x[actDisableSearch-101]
_ = x[actSelect-102] _ = x[actEnableSearch-102]
_ = x[actDeselect-103] _ = x[actSelect-103]
_ = x[actUnbind-104] _ = x[actDeselect-104]
_ = x[actRebind-105] _ = x[actUnbind-105]
_ = x[actBecome-106] _ = x[actRebind-106]
_ = x[actResponse-107] _ = x[actBecome-107]
_ = x[actShowHeader-108] _ = x[actResponse-108]
_ = x[actHideHeader-109] _ = x[actShowHeader-109]
_ = x[actHideHeader-110]
} }
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader" const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 413, 427, 438, 449, 467, 484, 491, 510, 522, 536, 545, 560, 572, 585, 596, 607, 619, 633, 654, 669, 684, 701, 708, 713, 722, 733, 744, 757, 772, 783, 796, 803, 816, 829, 846, 861, 874, 888, 902, 918, 938, 950, 973, 991, 1015, 1033, 1050, 1060, 1076, 1098, 1111, 1127, 1139, 1153, 1169, 1187, 1207, 1229, 1243, 1258, 1264, 1278, 1293, 1303, 1319, 1334, 1344, 1352, 1359, 1368, 1381, 1397, 1412, 1421, 1432, 1441, 1450, 1459, 1470, 1483, 1496} var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 692, 709, 716, 721, 730, 741, 752, 765, 780, 791, 804, 811, 824, 837, 854, 869, 882, 896, 910, 926, 946, 958, 981, 999, 1023, 1041, 1058, 1068, 1084, 1106, 1119, 1135, 1147, 1161, 1177, 1195, 1215, 1237, 1251, 1266, 1272, 1286, 1301, 1311, 1327, 1342, 1352, 1360, 1367, 1376, 1389, 1405, 1420, 1429, 1440, 1449, 1458, 1467, 1478, 1491, 1504}
func (i actionType) String() string { func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) { if i < 0 || i >= actionType(len(_actionType_index)-1) {

View File

@@ -152,7 +152,7 @@ var (
// Extra bonus for word boundary after slash, colon, semi-colon, and comma // Extra bonus for word boundary after slash, colon, semi-colon, and comma
bonusBoundaryDelimiter int16 = bonusBoundary + 1 bonusBoundaryDelimiter int16 = bonusBoundary + 1
initialCharClass charClass = charWhite initialCharClass = charWhite
// A minor optimization that can give 15%+ performance boost // A minor optimization that can give 15%+ performance boost
asciiCharClasses [unicode.MaxASCII + 1]charClass asciiCharClasses [unicode.MaxASCII + 1]charClass

View File

@@ -3,7 +3,7 @@
package algo package algo
var normalized map[rune]rune = map[rune]rune{ var normalized = map[rune]rune{
0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER 0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER
0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER 0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER
0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER 0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER

View File

@@ -292,7 +292,7 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
func parseAnsiCode(s string, delimiter byte) (int, byte, string) { func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
var remaining string var remaining string
i := -1 var i int
if delimiter == 0 { if delimiter == 0 {
// Faster than strings.IndexAny(";:") // Faster than strings.IndexAny(";:")
i = strings.IndexByte(s, ';') i = strings.IndexByte(s, ';')
@@ -312,7 +312,7 @@ func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
// Inlined version of strconv.Atoi() that only handles positive // Inlined version of strconv.Atoi() that only handles positive
// integers and does not allocate on error. // integers and does not allocate on error.
code := 0 code := 0
for _, ch := range sbytes(s) { for _, ch := range stringBytes(s) {
ch -= '0' ch -= '0'
if ch > 9 { if ch > 9 {
return -1, delimiter, remaining return -1, delimiter, remaining
@@ -350,7 +350,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state256 := 0 state256 := 0
ptr := &state.fg ptr := &state.fg
var delimiter byte = 0 var delimiter byte
count := 0 count := 0
for len(ansiCode) != 0 { for len(ansiCode) != 0 {
var num int var num int

View File

@@ -12,8 +12,14 @@ type ChunkCache struct {
} }
// NewChunkCache returns a new ChunkCache // NewChunkCache returns a new ChunkCache
func NewChunkCache() ChunkCache { func NewChunkCache() *ChunkCache {
return ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)} return &ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
}
func (cc *ChunkCache) Clear() {
cc.mutex.Lock()
cc.cache = make(map[*Chunk]*queryCache)
cc.mutex.Unlock()
} }
// Add adds the list to the cache // Add adds the list to the cache

View File

@@ -67,9 +67,9 @@ const (
) )
const ( const (
exitCancel = -1 ExitCancel = -1
exitOk = 0 ExitOk = 0
exitNoMatch = 1 ExitNoMatch = 1
exitError = 2 ExitError = 2
exitInterrupt = 130 ExitInterrupt = 130
) )

View File

@@ -2,10 +2,8 @@
package fzf package fzf
import ( import (
"fmt"
"sync" "sync"
"time" "time"
"unsafe"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@@ -19,30 +17,24 @@ Matcher -> EvtSearchFin -> Terminal (update list)
Matcher -> EvtHeader -> Terminal (update header) Matcher -> EvtHeader -> Terminal (update header)
*/ */
func ustring(data []byte) string {
return unsafe.String(unsafe.SliceData(data), len(data))
}
func sbytes(data string) []byte {
return unsafe.Slice(unsafe.StringData(data), len(data))
}
// Run starts fzf // Run starts fzf
func Run(opts *Options, version string, revision string) { func Run(opts *Options) (int, error) {
if err := postProcessOptions(opts); err != nil {
return ExitError, err
}
defer util.RunAtExitFuncs() defer util.RunAtExitFuncs()
// Output channel given
if opts.Output != nil {
opts.Printer = func(str string) {
opts.Output <- str
}
}
sort := opts.Sort > 0 sort := opts.Sort > 0
sortCriteria = opts.Criteria sortCriteria = opts.Criteria
if opts.Version {
if len(revision) > 0 {
fmt.Printf("%s (%s)\n", version, revision)
} else {
fmt.Println(version)
}
util.Exit(exitOk)
}
// Event channel // Event channel
eventBox := util.NewEventBox() eventBox := util.NewEventBox()
@@ -56,16 +48,16 @@ func Run(opts *Options, version string, revision string) {
if opts.Theme.Colored { if opts.Theme.Colored {
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
prevLineAnsiState = lineAnsiState prevLineAnsiState = lineAnsiState
trimmed, offsets, newState := extractColor(ustring(data), lineAnsiState, nil) trimmed, offsets, newState := extractColor(byteString(data), lineAnsiState, nil)
lineAnsiState = newState lineAnsiState = newState
return util.ToChars(sbytes(trimmed)), offsets return util.ToChars(stringBytes(trimmed)), offsets
} }
} else { } else {
// When color is disabled but ansi option is given, // When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input // we simply strip out ANSI codes from the input
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, _, _ := extractColor(ustring(data), nil, nil) trimmed, _, _ := extractColor(byteString(data), nil, nil)
return util.ToChars(sbytes(trimmed)), nil return util.ToChars(stringBytes(trimmed)), nil
} }
} }
} }
@@ -77,7 +69,7 @@ func Run(opts *Options, version string, revision string) {
if len(opts.WithNth) == 0 { if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(item *Item, data []byte) bool { chunkList = NewChunkList(func(item *Item, data []byte) bool {
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, ustring(data)) header = append(header, byteString(data))
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return false return false
} }
@@ -88,7 +80,7 @@ func Run(opts *Options, version string, revision string) {
}) })
} else { } else {
chunkList = NewChunkList(func(item *Item, data []byte) bool { chunkList = NewChunkList(func(item *Item, data []byte) bool {
tokens := Tokenize(ustring(data), opts.Delimiter) tokens := Tokenize(byteString(data), opts.Delimiter)
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 { if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
var ansiState *ansiState var ansiState *ansiState
if prevLineAnsiState != nil { if prevLineAnsiState != nil {
@@ -112,7 +104,7 @@ func Run(opts *Options, version string, revision string) {
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return false return false
} }
item.text, item.colors = ansiProcessor(sbytes(transformed)) item.text, item.colors = ansiProcessor(stringBytes(transformed))
item.text.TrimTrailingWhitespaces() item.text.TrimTrailingWhitespaces()
item.text.Index = itemIndex item.text.Index = itemIndex
item.origText = &data item.origText = &data
@@ -131,7 +123,7 @@ func Run(opts *Options, version string, revision string) {
reader = NewReader(func(data []byte) bool { reader = NewReader(func(data []byte) bool {
return chunkList.Push(data) return chunkList.Push(data)
}, eventBox, executor, opts.ReadZero, opts.Filter == nil) }, eventBox, executor, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip) go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
} }
// Matcher // Matcher
@@ -147,14 +139,16 @@ func Run(opts *Options, version string, revision string) {
forward = true forward = true
} }
} }
cache := NewChunkCache()
patternCache := make(map[string]*Pattern)
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
return BuildPattern( return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos, opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, opts.Nth, opts.Delimiter, runes) opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
} }
inputRevision := 0 inputRevision := 0
snapshotRevision := 0 snapshotRevision := 0
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox, inputRevision) matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
// Filtering mode // Filtering mode
if opts.Filter != nil { if opts.Filter != nil {
@@ -182,7 +176,7 @@ func Run(opts *Options, version string, revision string) {
} }
return false return false
}, eventBox, executor, opts.ReadZero, false) }, eventBox, executor, opts.ReadZero, false)
reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip) reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
} else { } else {
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin) eventBox.WaitFor(EvtReadFin)
@@ -197,9 +191,9 @@ func Run(opts *Options, version string, revision string) {
} }
} }
if found { if found {
util.Exit(exitOk) return ExitOk, nil
} }
util.Exit(exitNoMatch) return ExitNoMatch, nil
} }
// Synchronous search // Synchronous search
@@ -210,9 +204,13 @@ func Run(opts *Options, version string, revision string) {
// Go interactive // Go interactive
go matcher.Loop() go matcher.Loop()
defer matcher.Stop()
// Terminal I/O // Terminal I/O
terminal := NewTerminal(opts, eventBox, executor) terminal, err := NewTerminal(opts, eventBox, executor)
if err != nil {
return ExitError, err
}
maxFit := 0 // Maximum number of items that can fit on screen maxFit := 0 // Maximum number of items that can fit on screen
padHeight := 0 padHeight := 0
heightUnknown := opts.Height.auto heightUnknown := opts.Height.auto
@@ -229,7 +227,7 @@ func Run(opts *Options, version string, revision string) {
// Event coordination // Event coordination
reading := true reading := true
ticks := 0 ticks := 0
var nextCommand *string var nextCommand *commandSpec
var nextEnviron []string var nextEnviron []string
eventBox.Watch(EvtReadNew) eventBox.Watch(EvtReadNew)
total := 0 total := 0
@@ -250,7 +248,7 @@ func Run(opts *Options, version string, revision string) {
useSnapshot := false useSnapshot := false
var snapshot []*Chunk var snapshot []*Chunk
var count int var count int
restart := func(command string, environ []string) { restart := func(command commandSpec, environ []string) {
reading = true reading = true
chunkList.Clear() chunkList.Clear()
itemIndex = 0 itemIndex = 0
@@ -258,6 +256,9 @@ func Run(opts *Options, version string, revision string) {
header = make([]string, 0, opts.HeaderLines) header = make([]string, 0, opts.HeaderLines)
go reader.restart(command, environ) go reader.restart(command, environ)
} }
exitCode := ExitOk
stop := false
for { for {
delay := true delay := true
ticks++ ticks++
@@ -278,7 +279,11 @@ func Run(opts *Options, version string, revision string) {
if reading { if reading {
reader.terminate() reader.terminate()
} }
util.Exit(value.(int)) quitSignal := value.(quitSignal)
exitCode = quitSignal.code
err = quitSignal.err
stop = true
return
case EvtReadNew, EvtReadFin: case EvtReadNew, EvtReadFin:
if evt == EvtReadFin && nextCommand != nil { if evt == EvtReadFin && nextCommand != nil {
restart(*nextCommand, nextEnviron) restart(*nextCommand, nextEnviron)
@@ -309,7 +314,7 @@ func Run(opts *Options, version string, revision string) {
matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision) matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision)
case EvtSearchNew: case EvtSearchNew:
var command *string var command *commandSpec
var environ []string var environ []string
var changed bool var changed bool
switch val := value.(type) { switch val := value.(type) {
@@ -378,10 +383,11 @@ func Run(opts *Options, version string, revision string) {
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
opts.Printer(val.Get(i).item.AsString(opts.Ansi)) opts.Printer(val.Get(i).item.AsString(opts.Ansi))
} }
if count > 0 { if count == 0 {
util.Exit(exitOk) exitCode = ExitNoMatch
} }
util.Exit(exitNoMatch) stop = true
return
} }
determine(val.final) determine(val.final)
} }
@@ -392,6 +398,9 @@ func Run(opts *Options, version string, revision string) {
} }
events.Clear() events.Clear()
}) })
if stop {
break
}
if delay && reading { if delay && reading {
dur := util.DurWithin( dur := util.DurWithin(
time.Duration(ticks)*coordinatorDelayStep, time.Duration(ticks)*coordinatorDelayStep,
@@ -399,4 +408,5 @@ func Run(opts *Options, version string, revision string) {
time.Sleep(dur) time.Sleep(dur)
} }
} }
return exitCode, err
} }

35
src/functions.go Normal file
View File

@@ -0,0 +1,35 @@
package fzf
import (
"os"
"strings"
"unsafe"
)
func writeTemporaryFile(data []string, printSep string) string {
f, err := os.CreateTemp("", "fzf-preview-*")
if err != nil {
// Unable to create temporary file
// FIXME: Should we terminate the program?
return ""
}
defer f.Close()
f.WriteString(strings.Join(data, printSep))
f.WriteString(printSep)
return f.Name()
}
func removeFiles(files []string) {
for _, filename := range files {
os.Remove(filename)
}
}
func stringBytes(data string) []byte {
return unsafe.Slice(unsafe.StringData(data), len(data))
}
func byteString(data []byte) string {
return unsafe.String(unsafe.SliceData(data), len(data))
}

View File

@@ -21,6 +21,7 @@ type MatchRequest struct {
// Matcher is responsible for performing search // Matcher is responsible for performing search
type Matcher struct { type Matcher struct {
cache *ChunkCache
patternBuilder func([]rune) *Pattern patternBuilder func([]rune) *Pattern
sort bool sort bool
tac bool tac bool
@@ -38,10 +39,11 @@ const (
) )
// NewMatcher returns a new Matcher // NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern, func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher { sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher {
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions) partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
return &Matcher{ return &Matcher{
cache: cache,
patternBuilder: patternBuilder, patternBuilder: patternBuilder,
sort: sort, sort: sort,
tac: tac, tac: tac,
@@ -60,8 +62,13 @@ func (m *Matcher) Loop() {
for { for {
var request MatchRequest var request MatchRequest
stop := false
m.reqBox.Wait(func(events *util.Events) { m.reqBox.Wait(func(events *util.Events) {
for _, val := range *events { for t, val := range *events {
if t == reqQuit {
stop = true
return
}
switch val := val.(type) { switch val := val.(type) {
case MatchRequest: case MatchRequest:
request = val request = val
@@ -71,12 +78,15 @@ func (m *Matcher) Loop() {
} }
events.Clear() events.Clear()
}) })
if stop {
break
}
if request.sort != m.sort || request.revision != m.revision { if request.sort != m.sort || request.revision != m.revision {
m.sort = request.sort m.sort = request.sort
m.revision = request.revision m.revision = request.revision
m.mergerCache = make(map[string]*Merger) m.mergerCache = make(map[string]*Merger)
clearChunkCache() m.cache.Clear()
} }
// Restart search // Restart search
@@ -236,3 +246,7 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
} }
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision}) m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision})
} }
func (m *Matcher) Stop() {
m.reqBox.Set(reqQuit, nil)
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,9 +3,11 @@
package fzf package fzf
import "errors"
func (o *Options) initProfiling() error { func (o *Options) initProfiling() error {
if o.CPUProfile != "" || o.MEMProfile != "" || o.BlockProfile != "" || o.MutexProfile != "" { 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 errors.New("error: profiling not supported: FZF must be built with '-tags=pprof' to enable profiling")
} }
return nil return nil
} }

View File

@@ -80,7 +80,7 @@ func TestDelimiterRegexRegexCaret(t *testing.T) {
func TestSplitNth(t *testing.T) { func TestSplitNth(t *testing.T) {
{ {
ranges := splitNth("..") ranges, _ := splitNth("..")
if len(ranges) != 1 || if len(ranges) != 1 ||
ranges[0].begin != rangeEllipsis || ranges[0].begin != rangeEllipsis ||
ranges[0].end != rangeEllipsis { ranges[0].end != rangeEllipsis {
@@ -88,7 +88,7 @@ func TestSplitNth(t *testing.T) {
} }
} }
{ {
ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1") ranges, _ := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
if len(ranges) != 10 || if len(ranges) != 10 ||
ranges[0].begin != rangeEllipsis || ranges[0].end != 3 || ranges[0].begin != rangeEllipsis || ranges[0].end != 3 ||
ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis || ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis ||
@@ -137,7 +137,7 @@ func TestIrrelevantNth(t *testing.T) {
} }
func TestParseKeys(t *testing.T) { func TestParseKeys(t *testing.T) {
pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "") pairs, _ := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
checkEvent := func(e tui.Event, s string) { checkEvent := func(e tui.Event, s string) {
if pairs[e] != s { if pairs[e] != s {
t.Errorf("%s != %s", pairs[e], s) t.Errorf("%s != %s", pairs[e], s)
@@ -163,7 +163,7 @@ func TestParseKeys(t *testing.T) {
checkEvent(tui.AltKey(' '), "alt-SPACE") checkEvent(tui.AltKey(' '), "alt-SPACE")
// Synonyms // Synonyms
pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "") pairs, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
if len(pairs) != 9 { if len(pairs) != 9 {
t.Error(9) t.Error(9)
} }
@@ -177,7 +177,7 @@ func TestParseKeys(t *testing.T) {
check(tui.Left, "left") check(tui.Left, "left")
check(tui.Right, "right") check(tui.Right, "right")
pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "") pairs, _ = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
if len(pairs) != 11 { if len(pairs) != 11 {
t.Error(11) t.Error(11)
} }
@@ -206,40 +206,40 @@ func TestParseKeysWithComma(t *testing.T) {
} }
} }
pairs := parseKeyChords(",", "") pairs, _ := parseKeyChords(",", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",,a,b", "") pairs, _ = parseKeyChords(",,a,b", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,b,,", "") pairs, _ = parseKeyChords("a,b,,", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,,,b", "") pairs, _ = parseKeyChords("a,,,b", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,,,b,c", "") pairs, _ = parseKeyChords("a,,,b,c", "")
checkN(len(pairs), 4) checkN(len(pairs), 4)
check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key('c'), "c") check(pairs, tui.Key('c'), "c")
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",,,", "") pairs, _ = parseKeyChords(",,,", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",ALT-,,", "") pairs, _ = parseKeyChords(",ALT-,,", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, tui.AltKey(','), "ALT-,") check(pairs, tui.AltKey(','), "ALT-,")
} }
@@ -262,17 +262,13 @@ func TestBind(t *testing.T) {
} }
} }
check(tui.CtrlA.AsEvent(), "", actBeginningOfLine) check(tui.CtrlA.AsEvent(), "", actBeginningOfLine)
errorString := ""
errorFn := func(e string) {
errorString = e
}
parseKeymap(keymap, parseKeymap(keymap,
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+ "ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
"f1:execute(ls {+})+abort+execute(echo \n{+})+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 (,),[,],/,:,@,%,{};,"+ "alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
"x:Execute(foo+bar),X:execute/bar+baz/"+ "x:Execute(foo+bar),X:execute/bar+baz/"+
",f1:+first,f1:+top"+ ",f1:+first,f1:+top"+
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up", errorFn) ",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
check(tui.CtrlA.AsEvent(), "", actKillLine) check(tui.CtrlA.AsEvent(), "", actKillLine)
check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown) check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown)
check(tui.Key('c'), "", actPageUp) check(tui.Key('c'), "", actPageUp)
@@ -290,20 +286,17 @@ func TestBind(t *testing.T) {
check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute) check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute)
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char), errorFn) parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute) check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
} }
parseKeymap(keymap, "f1:abort", errorFn) parseKeymap(keymap, "f1:abort")
check(tui.F1.AsEvent(), "", actAbort) check(tui.F1.AsEvent(), "", actAbort)
if len(errorString) > 0 {
t.Errorf("error parsing keymap: %s", errorString)
}
} }
func TestColorSpec(t *testing.T) { func TestColorSpec(t *testing.T) {
theme := tui.Dark256 theme := tui.Dark256
dark := parseTheme(theme, "dark") dark, _ := parseTheme(theme, "dark")
if *dark != *theme { if *dark != *theme {
t.Errorf("colors should be equivalent") t.Errorf("colors should be equivalent")
} }
@@ -311,7 +304,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("point should not be equivalent") t.Errorf("point should not be equivalent")
} }
light := parseTheme(theme, "dark,light") light, _ := parseTheme(theme, "dark,light")
if *light == *theme { if *light == *theme {
t.Errorf("should not be equivalent") t.Errorf("should not be equivalent")
} }
@@ -322,7 +315,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("point should not be equivalent") t.Errorf("point should not be equivalent")
} }
customized := parseTheme(theme, "fg:231,bg:232") customized, _ := parseTheme(theme, "fg:231,bg:232")
if customized.Fg.Color != 231 || customized.Bg.Color != 232 { if customized.Fg.Color != 231 || customized.Bg.Color != 232 {
t.Errorf("color not customized") t.Errorf("color not customized")
} }
@@ -335,7 +328,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized) t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
} }
customized = parseTheme(theme, "fg:231,dark,bg:232") customized, _ = parseTheme(theme, "fg:231,dark,bg:232")
if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg { if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
t.Errorf("color not customized") t.Errorf("color not customized")
} }
@@ -475,7 +468,7 @@ func TestValidateSign(t *testing.T) {
} }
func TestParseSingleActionList(t *testing.T) { func TestParseSingleActionList(t *testing.T) {
actions := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down", func(string) {}) actions, _ := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down")
if len(actions) != 4 { if len(actions) != 4 {
t.Errorf("Invalid number of actions parsed:%d", len(actions)) t.Errorf("Invalid number of actions parsed:%d", len(actions))
} }
@@ -491,11 +484,8 @@ func TestParseSingleActionList(t *testing.T) {
} }
func TestParseSingleActionListError(t *testing.T) { func TestParseSingleActionListError(t *testing.T) {
err := "" _, err := parseSingleActionList("change-query(foobar)baz")
parseSingleActionList("change-query(foobar)baz", func(e string) { if err == nil {
err = e
})
if len(err) == 0 {
t.Errorf("Failed to detect error") t.Errorf("Failed to detect error")
} }
} }

View File

@@ -60,32 +60,17 @@ type Pattern struct {
delimiter Delimiter delimiter Delimiter
nth []Range nth []Range
procFun map[termType]algo.Algo procFun map[termType]algo.Algo
cache *ChunkCache
} }
var ( var _splitRegex *regexp.Regexp
_patternCache map[string]*Pattern
_splitRegex *regexp.Regexp
_cache ChunkCache
)
func init() { func init() {
_splitRegex = regexp.MustCompile(" +") _splitRegex = regexp.MustCompile(" +")
clearPatternCache()
clearChunkCache()
}
func clearPatternCache() {
// We can uniquely identify the pattern for a given string since
// search mode and caseMode do not change while the program is running
_patternCache = make(map[string]*Pattern)
}
func clearChunkCache() {
_cache = NewChunkCache()
} }
// BuildPattern builds Pattern object from the given arguments // BuildPattern builds Pattern object from the given arguments
func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool, func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string var asString string
@@ -98,7 +83,9 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
asString = string(runes) asString = string(runes)
} }
cached, found := _patternCache[asString] // We can uniquely identify the pattern for a given string since
// search mode and caseMode do not change while the program is running
cached, found := patternCache[asString]
if found { if found {
return cached return cached
} }
@@ -153,6 +140,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
cacheable: cacheable, cacheable: cacheable,
nth: nth, nth: nth,
delimiter: delimiter, delimiter: delimiter,
cache: cache,
procFun: make(map[termType]algo.Algo)} procFun: make(map[termType]algo.Algo)}
ptr.cacheKey = ptr.buildCacheKey() ptr.cacheKey = ptr.buildCacheKey()
@@ -162,7 +150,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
ptr.procFun[termPrefix] = algo.PrefixMatch ptr.procFun[termPrefix] = algo.PrefixMatch
ptr.procFun[termSuffix] = algo.SuffixMatch ptr.procFun[termSuffix] = algo.SuffixMatch
_patternCache[asString] = ptr patternCache[asString] = ptr
return ptr return ptr
} }
@@ -282,18 +270,18 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
// ChunkCache: Exact match // ChunkCache: Exact match
cacheKey := p.CacheKey() cacheKey := p.CacheKey()
if p.cacheable { if p.cacheable {
if cached := _cache.Lookup(chunk, cacheKey); cached != nil { if cached := p.cache.Lookup(chunk, cacheKey); cached != nil {
return cached return cached
} }
} }
// Prefix/suffix cache // Prefix/suffix cache
space := _cache.Search(chunk, cacheKey) space := p.cache.Search(chunk, cacheKey)
matches := p.matchChunk(chunk, space, slab) matches := p.matchChunk(chunk, space, slab)
if p.cacheable { if p.cacheable {
_cache.Add(chunk, cacheKey, matches) p.cache.Add(chunk, cacheKey, matches)
} }
return matches return matches
} }

View File

@@ -64,10 +64,15 @@ func TestParseTermsEmpty(t *testing.T) {
} }
} }
func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
withPos, cacheable, nth, delimiter, runes)
}
func TestExact(t *testing.T) { func TestExact(t *testing.T) {
defer clearPatternCache() pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true,
clearPatternCache()
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true,
[]Range{}, Delimiter{}, []rune("'abc")) []Range{}, Delimiter{}, []rune("'abc"))
chars := util.ToChars([]byte("aabbcc abc")) chars := util.ToChars([]byte("aabbcc abc"))
res, pos := algo.ExactMatchNaive( res, pos := algo.ExactMatchNaive(
@@ -81,9 +86,7 @@ func TestExact(t *testing.T) {
} }
func TestEqual(t *testing.T) { func TestEqual(t *testing.T) {
defer clearPatternCache() pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$"))
clearPatternCache()
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$"))
match := func(str string, sidxExpected int, eidxExpected int) { match := func(str string, sidxExpected int, eidxExpected int) {
chars := util.ToChars([]byte(str)) chars := util.ToChars([]byte(str))
@@ -104,19 +107,12 @@ func TestEqual(t *testing.T) {
} }
func TestCaseSensitivity(t *testing.T) { func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache() pat1 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() pat2 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc")) pat3 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() pat4 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc")) pat5 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() pat6 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache()
pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
if string(pat1.text) != "abc" || pat1.caseSensitive != false || if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
string(pat2.text) != "Abc" || pat2.caseSensitive != true || string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
@@ -129,7 +125,7 @@ func TestCaseSensitivity(t *testing.T) {
} }
func TestOrigTextAndTransformed(t *testing.T) { func TestOrigTextAndTransformed(t *testing.T) {
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg")) pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize("junegunn", Delimiter{}) tokens := Tokenize("junegunn", Delimiter{})
trans := Transform(tokens, []Range{{1, 1}}) trans := Transform(tokens, []Range{{1, 1}})
@@ -163,15 +159,13 @@ func TestOrigTextAndTransformed(t *testing.T) {
func TestCacheKey(t *testing.T) { func TestCacheKey(t *testing.T) {
test := func(extended bool, patStr string, expected string, cacheable bool) { test := func(extended bool, patStr string, expected string, cacheable bool) {
clearPatternCache() pat := buildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr))
pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr))
if pat.CacheKey() != expected { if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
} }
if pat.cacheable != cacheable { if pat.cacheable != cacheable {
t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr) t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr)
} }
clearPatternCache()
} }
test(false, "foo !bar", "foo !bar", true) test(false, "foo !bar", "foo !bar", true)
test(false, "foo | bar !baz", "foo | bar !baz", true) test(false, "foo | bar !baz", "foo | bar !baz", true)
@@ -187,15 +181,13 @@ func TestCacheKey(t *testing.T) {
func TestCacheable(t *testing.T) { func TestCacheable(t *testing.T) {
test := func(fuzzy bool, str string, expected string, cacheable bool) { test := func(fuzzy bool, str string, expected string, cacheable bool) {
clearPatternCache() pat := buildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str))
pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str))
if pat.CacheKey() != expected { if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
} }
if cacheable != pat.cacheable { if cacheable != pat.cacheable {
t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable) t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable)
} }
clearPatternCache()
} }
test(true, "foo bar", "foo\tbar", true) test(true, "foo bar", "foo\tbar", true)
test(true, "foo 'bar", "foo\tbar", false) test(true, "foo 'bar", "foo\tbar", false)

View File

@@ -3,6 +3,4 @@
package protector package protector
// Protect calls OS specific protections like pledge on OpenBSD // Protect calls OS specific protections like pledge on OpenBSD
func Protect() { func Protect() {}
return
}

View File

@@ -86,18 +86,34 @@ func (r *Reader) terminate() {
r.mutex.Unlock() r.mutex.Unlock()
} }
func (r *Reader) restart(command string, environ []string) { func (r *Reader) restart(command commandSpec, environ []string) {
r.event = int32(EvtReady) r.event = int32(EvtReady)
r.startEventPoller() r.startEventPoller()
success := r.readFromCommand(command, environ) success := r.readFromCommand(command.command, environ)
r.fin(success) r.fin(success)
removeFiles(command.tempFiles)
}
func (r *Reader) readChannel(inputChan chan string) bool {
for {
item, more := <-inputChan
if !more {
break
}
if r.pusher([]byte(item)) {
atomic.StoreInt32(&r.event, int32(EvtReadNew))
}
}
return true
} }
// ReadSource reads data from the default command or from standard input // ReadSource reads data from the default command or from standard input
func (r *Reader) ReadSource(root string, opts walkerOpts, ignores []string) { func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts, ignores []string) {
r.startEventPoller() r.startEventPoller()
var success bool var success bool
if util.IsTty() { if inputChan != nil {
success = r.readChannel(inputChan)
} else if util.IsTty() {
cmd := os.Getenv("FZF_DEFAULT_COMMAND") cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 { if len(cmd) == 0 {
success = r.readFiles(root, opts, ignores) success = r.readFiles(root, opts, ignores)

View File

@@ -73,28 +73,28 @@ func parseListenAddress(address string) (listenAddress, error) {
return listenAddress{parts[0], port}, nil return listenAddress{parts[0], port}, nil
} }
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (int, error) { func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (net.Listener, int, error) {
host := address.host host := address.host
port := address.port port := address.port
apiKey := os.Getenv("FZF_API_KEY") apiKey := os.Getenv("FZF_API_KEY")
if !address.IsLocal() && len(apiKey) == 0 { if !address.IsLocal() && len(apiKey) == 0 {
return port, errors.New("FZF_API_KEY is required to allow remote access") return nil, port, errors.New("FZF_API_KEY is required to allow remote access")
} }
addrStr := fmt.Sprintf("%s:%d", host, port) addrStr := fmt.Sprintf("%s:%d", host, port)
listener, err := net.Listen("tcp", addrStr) listener, err := net.Listen("tcp", addrStr)
if err != nil { if err != nil {
return port, fmt.Errorf("failed to listen on %s", addrStr) return nil, port, fmt.Errorf("failed to listen on %s", addrStr)
} }
if port == 0 { if port == 0 {
addr := listener.Addr().String() addr := listener.Addr().String()
parts := strings.Split(addr, ":") parts := strings.Split(addr, ":")
if len(parts) < 2 { if len(parts) < 2 {
return port, fmt.Errorf("cannot extract port: %s", addr) return nil, port, fmt.Errorf("cannot extract port: %s", addr)
} }
var err error var err error
port, err = strconv.Atoi(parts[len(parts)-1]) port, err = strconv.Atoi(parts[len(parts)-1])
if err != nil { if err != nil {
return port, err return nil, port, err
} }
} }
@@ -109,18 +109,16 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, respon
conn, err := listener.Accept() conn, err := listener.Accept()
if err != nil { if err != nil {
if errors.Is(err, net.ErrClosed) { if errors.Is(err, net.ErrClosed) {
break return
} else {
continue
} }
continue
} }
conn.Write([]byte(server.handleHttpRequest(conn))) conn.Write([]byte(server.handleHttpRequest(conn)))
conn.Close() conn.Close()
} }
listener.Close()
}() }()
return port, nil return listener, port, nil
} }
// Here we are writing a simplistic HTTP server without using net/http // Here we are writing a simplistic HTTP server without using net/http
@@ -217,12 +215,9 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
} }
body = body[:contentLength] body = body[:contentLength]
errorMessage := "" actions, err := parseSingleActionList(strings.Trim(string(body), "\r\n"))
actions := parseSingleActionList(strings.Trim(string(body), "\r\n"), func(message string) { if err != nil {
errorMessage = message return bad(err.Error())
})
if len(errorMessage) > 0 {
return bad(errorMessage)
} }
if len(actions) == 0 { if len(actions) == 0 {
return bad("no action specified") return bad("no action specified")

View File

@@ -2,10 +2,12 @@ package fzf
import ( import (
"bufio" "bufio"
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"math" "math"
"net"
"os" "os"
"os/signal" "os/signal"
"regexp" "regexp"
@@ -48,9 +50,7 @@ var placeholder *regexp.Regexp
var whiteSuffix *regexp.Regexp var whiteSuffix *regexp.Regexp
var offsetComponentRegex *regexp.Regexp var offsetComponentRegex *regexp.Regexp
var offsetTrimCharsRegex *regexp.Regexp var offsetTrimCharsRegex *regexp.Regexp
var activeTempFiles []string
var passThroughRegex *regexp.Regexp var passThroughRegex *regexp.Regexp
var actionTypeRegex *regexp.Regexp
const clearCode string = "\x1b[2J" const clearCode string = "\x1b[2J"
@@ -62,7 +62,6 @@ func init() {
whiteSuffix = regexp.MustCompile(`\s*$`) whiteSuffix = regexp.MustCompile(`\s*$`)
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`) offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`) offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
activeTempFiles = []string{}
// Parts of the preview output that should be passed through to the terminal // Parts of the preview output that should be passed through to the terminal
// * https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it // * https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it
@@ -112,6 +111,16 @@ func (s *resumableState) Set(flag bool) {
} }
} }
type commandSpec struct {
command string
tempFiles []string
}
type quitSignal struct {
code int
err error
}
type previewer struct { type previewer struct {
version int64 version int64
lines []string lines []string
@@ -226,6 +235,7 @@ type Terminal struct {
printQuery bool printQuery bool
history *History history *History
cycle bool cycle bool
highlightLine bool
headerVisible bool headerVisible bool
headerFirst bool headerFirst bool
headerLines int headerLines int
@@ -241,6 +251,7 @@ type Terminal struct {
unicode bool unicode bool
listenAddr *listenAddress listenAddr *listenAddress
listenPort *int listenPort *int
listener net.Listener
listenUnsafe bool listenUnsafe bool
borderShape tui.BorderShape borderShape tui.BorderShape
cleanExit bool cleanExit bool
@@ -259,7 +270,7 @@ type Terminal struct {
hasResizeActions bool hasResizeActions bool
triggerLoad bool triggerLoad bool
reading bool reading bool
running bool running *util.AtomicBool
failed *string failed *string
jumping jumpMode jumping jumpMode
jumpLabels string jumpLabels string
@@ -278,12 +289,12 @@ type Terminal struct {
previewBox *util.EventBox previewBox *util.EventBox
eventBox *util.EventBox eventBox *util.EventBox
mutex sync.Mutex mutex sync.Mutex
initFunc func() initFunc func() error
prevLines []itemLine prevLines []itemLine
suppress bool suppress bool
sigstop bool sigstop bool
startChan chan fitpad startChan chan fitpad
killChan chan int killChan chan bool
serverInputChan chan []*action serverInputChan chan []*action
serverOutputChan chan string serverOutputChan chan string
eventChan chan tui.Event eventChan chan tui.Event
@@ -298,6 +309,8 @@ type Terminal struct {
areaLines int areaLines int
areaColumns int areaColumns int
forcePreview bool forcePreview bool
clickHeaderLine int
clickHeaderColumn int
} }
type selectedItem struct { type selectedItem struct {
@@ -338,6 +351,7 @@ const (
reqPreviewRefresh reqPreviewRefresh
reqPreviewDelayed reqPreviewDelayed
reqQuit reqQuit
reqFatal
) )
type action struct { type action struct {
@@ -378,6 +392,7 @@ const (
actDeleteChar actDeleteChar
actDeleteCharEof actDeleteCharEof
actEndOfLine actEndOfLine
actFatal
actForwardChar actForwardChar
actForwardWord actForwardWord
actKillLine actKillLine
@@ -498,7 +513,7 @@ type placeholderFlags struct {
type searchRequest struct { type searchRequest struct {
sort bool sort bool
sync bool sync bool
command *string command *commandSpec
environ []string environ []string
changed bool changed bool
} }
@@ -509,6 +524,7 @@ type previewRequest struct {
pwindowSize tui.TermSize pwindowSize tui.TermSize
scrollOffset int scrollOffset int
list []*Item list []*Item
env []string
} }
type previewResult struct { type previewResult struct {
@@ -535,6 +551,7 @@ func defaultKeymap() map[tui.Event][]*action {
keymap[e] = toActions(a) keymap[e] = toActions(a)
} }
add(tui.Fatal, actFatal)
add(tui.Invalid, actInvalid) add(tui.Invalid, actInvalid)
add(tui.CtrlA, actBeginningOfLine) add(tui.CtrlA, actBeginningOfLine)
add(tui.CtrlB, actBackwardChar) add(tui.CtrlB, actBackwardChar)
@@ -640,7 +657,7 @@ func evaluateHeight(opts *Options, termHeight int) int {
} }
// NewTerminal returns new Terminal object // NewTerminal returns new Terminal object
func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor) *Terminal { func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor) (*Terminal, error) {
input := trimQuery(opts.Query) input := trimQuery(opts.Query)
var delay time.Duration var delay time.Duration
if opts.Tac { if opts.Tac {
@@ -658,11 +675,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
} }
var renderer tui.Renderer var renderer tui.Renderer
fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100) fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100)
var err error
if fullscreen { if fullscreen {
if tui.HasFullscreenRenderer() { if tui.HasFullscreenRenderer() {
renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
} else { } else {
renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, renderer, err = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
true, func(h int) int { return h }) true, func(h int) int { return h })
} }
} else { } else {
@@ -678,7 +696,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
effectiveMinHeight += borderLines(opts.BorderShape) effectiveMinHeight += borderLines(opts.BorderShape)
return util.Min(termHeight, util.Max(evaluateHeight(opts, termHeight), effectiveMinHeight)) return util.Min(termHeight, util.Max(evaluateHeight(opts, termHeight), effectiveMinHeight))
} }
renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc) renderer, err = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc)
}
if err != nil {
return nil, err
} }
wordRubout := "[^\\pL\\pN][\\pL\\pN]" wordRubout := "[^\\pL\\pN][\\pL\\pN]"
wordNext := "[\\pL\\pN][^\\pL\\pN]|(.$)" wordNext := "[\\pL\\pN][^\\pL\\pN]|(.$)"
@@ -691,6 +712,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
for key, action := range opts.Keymap { for key, action := range opts.Keymap {
keymapCopy[key] = action keymapCopy[key] = action
} }
t := Terminal{ t := Terminal{
initDelay: delay, initDelay: delay,
infoStyle: opts.InfoStyle, infoStyle: opts.InfoStyle,
@@ -739,6 +761,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
executor: executor, executor: executor,
paused: opts.Phony, paused: opts.Phony,
cycle: opts.Cycle, cycle: opts.Cycle,
highlightLine: opts.CursorLine,
headerVisible: true, headerVisible: true,
headerFirst: opts.HeaderFirst, headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines, headerLines: opts.HeaderLines,
@@ -752,7 +775,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
hasLoadActions: false, hasLoadActions: false,
triggerLoad: false, triggerLoad: false,
reading: true, reading: true,
running: true, running: util.NewAtomicBool(true),
failed: nil, failed: nil,
jumping: jumpDisabled, jumping: jumpDisabled,
jumpLabels: opts.JumpLabels, jumpLabels: opts.JumpLabels,
@@ -773,12 +796,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
slab: util.MakeSlab(slab16Size, slab32Size), slab: util.MakeSlab(slab16Size, slab32Size),
theme: opts.Theme, theme: opts.Theme,
startChan: make(chan fitpad, 1), startChan: make(chan fitpad, 1),
killChan: make(chan int), killChan: make(chan bool),
serverInputChan: make(chan []*action, 100), serverInputChan: make(chan []*action, 100),
serverOutputChan: make(chan string), serverOutputChan: make(chan string),
eventChan: make(chan tui.Event, 6), // (load + result + zero|one) | (focus) | (resize) | (GetChar) eventChan: make(chan tui.Event, 6), // (load + result + zero|one) | (focus) | (resize) | (GetChar)
tui: renderer, tui: renderer,
initFunc: func() { renderer.Init() }, initFunc: func() error { return renderer.Init() },
executing: util.NewAtomicBool(false), executing: util.NewAtomicBool(false),
lastAction: actStart, lastAction: actStart,
lastFocus: minItem.Index()} lastFocus: minItem.Index()}
@@ -830,14 +853,15 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()] _, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
if t.listenAddr != nil { if t.listenAddr != nil {
port, err := startHttpServer(*t.listenAddr, t.serverInputChan, t.serverOutputChan) listener, port, err := startHttpServer(*t.listenAddr, t.serverInputChan, t.serverOutputChan)
if err != nil { if err != nil {
errorExit(err.Error()) return nil, err
} }
t.listener = listener
t.listenPort = &port t.listenPort = &port
} }
return &t return &t, nil
} }
func (t *Terminal) environ() []string { func (t *Terminal) environ() []string {
@@ -857,6 +881,8 @@ func (t *Terminal) environ() []string {
env = append(env, fmt.Sprintf("FZF_LINES=%d", t.areaLines)) env = append(env, fmt.Sprintf("FZF_LINES=%d", t.areaLines))
env = append(env, fmt.Sprintf("FZF_COLUMNS=%d", t.areaColumns)) env = append(env, fmt.Sprintf("FZF_COLUMNS=%d", t.areaColumns))
env = append(env, fmt.Sprintf("FZF_POS=%d", util.Min(t.merger.Length(), t.cy+1))) env = append(env, fmt.Sprintf("FZF_POS=%d", util.Min(t.merger.Length(), t.cy+1)))
env = append(env, fmt.Sprintf("FZF_CLICK_HEADER_LINE=%d", t.clickHeaderLine))
env = append(env, fmt.Sprintf("FZF_CLICK_HEADER_COLUMN=%d", t.clickHeaderColumn))
return env return env
} }
@@ -1288,7 +1314,8 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
t.pborder.Close() t.pborder.Close()
t.pborder = nil t.pborder = nil
} }
if t.pwindow != nil { hadPreviewWindow := t.hasPreviewWindow()
if hadPreviewWindow {
t.pwindow.Close() t.pwindow.Close()
t.pwindow = nil t.pwindow = nil
} }
@@ -1387,6 +1414,9 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
pwidth = util.Max(0, pwidth) pwidth = util.Max(0, pwidth)
pheight = util.Max(0, pheight) pheight = util.Max(0, pheight)
t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, true, noBorder) t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, true, noBorder)
if !hadPreviewWindow {
t.pwindow.Erase()
}
} }
verticalPad := 2 verticalPad := 2
minPreviewHeight := 3 minPreviewHeight := 3
@@ -1452,6 +1482,14 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
// Add a 1-column margin between the preview window and the main window // Add a 1-column margin between the preview window and the main window
t.window = t.tui.NewWindow( t.window = t.tui.NewWindow(
marginInt[0], marginInt[3]+pwidth+1, width-pwidth-1, height, false, noBorder) marginInt[0], marginInt[3]+pwidth+1, width-pwidth-1, height, false, noBorder)
// Clear characters on the margin
// fzf --bind 'space:preview(seq 100)' --preview-window left,1
for y := 0; y < height; y++ {
t.window.Move(y, -1)
t.window.Print(" ")
}
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
} else { } else {
t.window = t.tui.NewWindow( t.window = t.tui.NewWindow(
@@ -1486,10 +1524,6 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
// Print border label // Print border label
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, false) t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, false)
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.previewOpts.border, false) t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.previewOpts.border, false)
for i := 0; i < t.window.Height(); i++ {
t.window.MoveAndClear(i, 0)
}
} }
func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape, redrawBorder bool) { func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape, redrawBorder bool) {
@@ -1645,7 +1679,7 @@ func (t *Terminal) printInfo() {
case infoDefault: case infoDefault:
t.move(line+1, 0, t.separatorLen == 0) t.move(line+1, 0, t.separatorLen == 0)
printSpinner() printSpinner()
t.move(line+1, 2, false) t.window.Print(" ") // Margin
pos = 2 pos = 2
case infoRight: case infoRight:
t.move(line+1, 0, false) t.move(line+1, 0, false)
@@ -1710,14 +1744,17 @@ func (t *Terminal) printInfo() {
printSeparator(fillLength, true) printSeparator(fillLength, true)
} }
t.window.CPrint(tui.ColInfo, output) t.window.CPrint(tui.ColInfo, output)
t.window.Print(" ") // Margin
return return
} }
if t.infoStyle == infoInlineRight { if t.infoStyle == infoInlineRight {
if len(t.infoPrefix) == 0 { if len(t.infoPrefix) == 0 {
pos = util.Max(pos, t.window.Width()-util.StringWidth(output)-3) t.move(line, pos, false)
newPos := util.Max(pos, t.window.Width()-util.StringWidth(output)-3)
t.window.Print(strings.Repeat(" ", newPos-pos))
pos = newPos
if pos < t.window.Width() { if pos < t.window.Width() {
t.move(line, pos, false)
printSpinner() printSpinner()
pos++ pos++
} }
@@ -1865,7 +1902,7 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool, bar b
t.window.CPrint(tui.ColCurrentCursor, label) t.window.CPrint(tui.ColCurrentCursor, label)
} }
if selected { if selected {
t.window.CPrint(tui.ColCurrentSelected, t.marker) t.window.CPrint(tui.ColCurrentMarker, t.marker)
} else { } else {
t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty) t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty)
} }
@@ -1877,15 +1914,36 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool, bar b
t.window.CPrint(tui.ColCursor, label) t.window.CPrint(tui.ColCursor, label)
} }
if selected { if selected {
t.window.CPrint(tui.ColSelected, t.marker) t.window.CPrint(tui.ColMarker, t.marker)
} else { } else {
t.window.Print(t.markerEmpty) t.window.Print(t.markerEmpty)
} }
newLine.width = t.printHighlighted(result, tui.ColNormal, tui.ColMatch, false, true) var base, match tui.ColorPair
if selected {
base = tui.ColSelected
match = tui.ColSelectedMatch
} else {
base = tui.ColNormal
match = tui.ColMatch
}
newLine.width = t.printHighlighted(result, base, match, false, true)
} }
fillSpaces := prevLine.width - newLine.width if (current || selected) && t.highlightLine {
if fillSpaces > 0 { color := tui.ColSelected
t.window.Print(strings.Repeat(" ", fillSpaces)) if current {
color = tui.ColCurrent
}
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
fillSpaces := maxWidth - newLine.width
newLine.width = maxWidth
if fillSpaces > 0 {
t.window.CPrint(color, strings.Repeat(" ", fillSpaces))
}
} else {
fillSpaces := prevLine.width - newLine.width
if fillSpaces > 0 {
t.window.Print(strings.Repeat(" ", fillSpaces))
}
} }
printBar() printBar()
t.prevLines[i] = newLine t.prevLines[i] = newLine
@@ -2088,12 +2146,11 @@ func (t *Terminal) renderPreviewArea(unchanged bool) {
} }
height := t.pwindow.Height() height := t.pwindow.Height()
header := []string{}
body := t.previewer.lines body := t.previewer.lines
headerLines := t.previewOpts.headerLines headerLines := t.previewOpts.headerLines
// Do not enable preview header lines if it's value is too large // Do not enable preview header lines if it's value is too large
if headerLines > 0 && headerLines < util.Min(len(body), height) { if headerLines > 0 && headerLines < util.Min(len(body), height) {
header = t.previewer.lines[0:headerLines] header := t.previewer.lines[0:headerLines]
body = t.previewer.lines[headerLines:] body = t.previewer.lines[headerLines:]
// Always redraw header // Always redraw header
t.renderPreviewText(height, header, 0, false) t.renderPreviewText(height, header, 0, false)
@@ -2217,9 +2274,8 @@ Loop:
t.pwindow.Move(height-1, maxWidth-1) t.pwindow.Move(height-1, maxWidth-1)
t.previewed.filled = true t.previewed.filled = true
break Loop break Loop
} else {
t.pwindow.MoveAndClear(y+requiredLines, 0)
} }
t.pwindow.MoveAndClear(y+requiredLines, 0)
} }
} }
@@ -2470,8 +2526,6 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
case 'q': case 'q':
flags.forceUpdate = true flags.forceUpdate = true
// query flag is not skipped // query flag is not skipped
default:
break
} }
} }
@@ -2494,26 +2548,6 @@ func hasPreviewFlags(template string) (slot bool, plus bool, forceUpdate bool) {
return return
} }
func writeTemporaryFile(data []string, printSep string) string {
f, err := os.CreateTemp("", "fzf-preview-*")
if err != nil {
errorExit("Unable to create temporary file")
}
defer f.Close()
f.WriteString(strings.Join(data, printSep))
f.WriteString(printSep)
activeTempFiles = append(activeTempFiles, f.Name())
return f.Name()
}
func cleanTemporaryFiles() {
for _, filename := range activeTempFiles {
os.Remove(filename)
}
activeTempFiles = []string{}
}
type replacePlaceholderParams struct { type replacePlaceholderParams struct {
template string template string
stripAnsi bool stripAnsi bool
@@ -2527,7 +2561,7 @@ type replacePlaceholderParams struct {
executor *util.Executor executor *util.Executor
} }
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string { func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) (string, []string) {
return replacePlaceholder(replacePlaceholderParams{ return replacePlaceholder(replacePlaceholderParams{
template: template, template: template,
stripAnsi: t.ansi, stripAnsi: t.ansi,
@@ -2548,8 +2582,9 @@ func (t *Terminal) evaluateScrollOffset() int {
} }
// We only need the current item to calculate the scroll offset // We only need the current item to calculate the scroll offset
offsetExpr := offsetTrimCharsRegex.ReplaceAllString( replaced, tempFiles := t.replacePlaceholder(t.previewOpts.scroll, false, "", []*Item{t.currentItem(), nil})
t.replacePlaceholder(t.previewOpts.scroll, false, "", []*Item{t.currentItem(), nil}), "") removeFiles(tempFiles)
offsetExpr := offsetTrimCharsRegex.ReplaceAllString(replaced, "")
atoi := func(s string) int { atoi := func(s string) int {
n, e := strconv.Atoi(s) n, e := strconv.Atoi(s)
@@ -2577,7 +2612,8 @@ func (t *Terminal) evaluateScrollOffset() int {
return util.Max(0, base) return util.Max(0, base)
} }
func replacePlaceholder(params replacePlaceholderParams) string { func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
tempFiles := []string{}
current := params.allItems[:1] current := params.allItems[:1]
selected := params.allItems[1:] selected := params.allItems[1:]
if current[0] == nil { if current[0] == nil {
@@ -2588,7 +2624,7 @@ func replacePlaceholder(params replacePlaceholderParams) string {
} }
// replace placeholders one by one // replace placeholders one by one
return placeholder.ReplaceAllStringFunc(params.template, func(match string) string { replaced := placeholder.ReplaceAllStringFunc(params.template, func(match string) string {
escaped, match, flags := parsePlaceholder(match) escaped, match, flags := parsePlaceholder(match)
// this function implements the effects a placeholder has on items // this function implements the effects a placeholder has on items
@@ -2671,10 +2707,14 @@ func replacePlaceholder(params replacePlaceholderParams) string {
} }
if flags.file { if flags.file {
return writeTemporaryFile(replacements, params.printsep) file := writeTemporaryFile(replacements, params.printsep)
tempFiles = append(tempFiles, file)
return file
} }
return strings.Join(replacements, " ") return strings.Join(replacements, " ")
}) })
return replaced, tempFiles
} }
func (t *Terminal) redraw() { func (t *Terminal) redraw() {
@@ -2691,7 +2731,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
if !valid && !capture { if !valid && !capture {
return line return line
} }
command := t.replacePlaceholder(template, forcePlus, string(t.input), list) command, tempFiles := t.replacePlaceholder(template, forcePlus, string(t.input), list)
cmd := t.executor.ExecCommand(command, false) cmd := t.executor.ExecCommand(command, false)
cmd.Env = t.environ() cmd.Env = t.environ()
t.executing.Set(true) t.executing.Set(true)
@@ -2722,7 +2762,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
} }
} }
t.executing.Set(false) t.executing.Set(false)
cleanTemporaryFiles() removeFiles(tempFiles)
return line return line
} }
@@ -2821,18 +2861,18 @@ func (t *Terminal) toggleItem(item *Item) bool {
return true return true
} }
func (t *Terminal) killPreview(code int) { func (t *Terminal) killPreview() {
select { select {
case t.killChan <- code: case t.killChan <- true:
default: default:
if code != exitCancel {
t.eventBox.Set(EvtQuit, code)
}
} }
} }
func (t *Terminal) cancelPreview() { func (t *Terminal) cancelPreview() {
t.killPreview(exitCancel) select {
case t.killChan <- false:
default:
}
} }
func (t *Terminal) pwindowSize() tui.TermSize { func (t *Terminal) pwindowSize() tui.TermSize {
@@ -2856,7 +2896,7 @@ func (t *Terminal) currentIndex() int32 {
} }
// Loop is called to start Terminal I/O // Loop is called to start Terminal I/O
func (t *Terminal) Loop() { func (t *Terminal) Loop() error {
// prof := profile.Start(profile.ProfilePath("/tmp/")) // prof := profile.Start(profile.ProfilePath("/tmp/"))
fitpad := <-t.startChan fitpad := <-t.startChan
fit := fitpad.fit fit := fitpad.fit
@@ -2880,14 +2920,23 @@ func (t *Terminal) Loop() {
return util.Min(termHeight, contentHeight+pad) return util.Min(termHeight, contentHeight+pad)
}) })
} }
// Context
ctx, cancel := context.WithCancel(context.Background())
{ // Late initialization { // Late initialization
intChan := make(chan os.Signal, 1) intChan := make(chan os.Signal, 1)
signal.Notify(intChan, os.Interrupt, syscall.SIGTERM) signal.Notify(intChan, os.Interrupt, syscall.SIGTERM)
go func() { go func() {
for s := range intChan { for {
// Don't quit by SIGINT while executing because it should be for the executing command and not for fzf itself select {
if !(s == os.Interrupt && t.executing.Get()) { case <-ctx.Done():
t.reqBox.Set(reqQuit, nil) return
case s := <-intChan:
// Don't quit by SIGINT while executing because it should be for the executing command and not for fzf itself
if !(s == os.Interrupt && t.executing.Get()) {
t.reqBox.Set(reqQuit, nil)
}
} }
} }
}() }()
@@ -2896,8 +2945,12 @@ func (t *Terminal) Loop() {
notifyOnCont(contChan) notifyOnCont(contChan)
go func() { go func() {
for { for {
<-contChan select {
t.reqBox.Set(reqReinit, nil) case <-ctx.Done():
return
case <-contChan:
t.reqBox.Set(reqReinit, nil)
}
} }
}() }()
@@ -2906,16 +2959,26 @@ func (t *Terminal) Loop() {
notifyOnResize(resizeChan) // Non-portable notifyOnResize(resizeChan) // Non-portable
go func() { go func() {
for { for {
<-resizeChan select {
t.reqBox.Set(reqResize, nil) case <-ctx.Done():
return
case <-resizeChan:
t.reqBox.Set(reqResize, nil)
}
} }
}() }()
} }
t.mutex.Lock() t.mutex.Lock()
t.initFunc() if err := t.initFunc(); err != nil {
t.mutex.Unlock()
cancel()
t.eventBox.Set(EvtQuit, quitSignal{ExitError, err})
return err
}
t.termSize = t.tui.Size() t.termSize = t.tui.Size()
t.resizeWindows(false) t.resizeWindows(false)
t.window.Erase()
t.printPrompt() t.printPrompt()
t.printInfo() t.printInfo()
t.printHeader() t.printHeader()
@@ -2929,7 +2992,7 @@ func (t *Terminal) Loop() {
// Keep the spinner spinning // Keep the spinner spinning
go func() { go func() {
for { for t.running.Get() {
t.mutex.Lock() t.mutex.Lock()
reading := t.reading reading := t.reading
t.mutex.Unlock() t.mutex.Unlock()
@@ -2944,15 +3007,20 @@ func (t *Terminal) Loop() {
if t.hasPreviewer() { if t.hasPreviewer() {
go func() { go func() {
var version int64 var version int64
stop := false
for { for {
var items []*Item var items []*Item
var commandTemplate string var commandTemplate string
var pwindow tui.Window var pwindow tui.Window
var pwindowSize tui.TermSize var pwindowSize tui.TermSize
var env []string
initialOffset := 0 initialOffset := 0
t.previewBox.Wait(func(events *util.Events) { t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events { for req, value := range *events {
switch req { switch req {
case reqQuit:
stop = true
return
case reqPreviewEnqueue: case reqPreviewEnqueue:
request := value.(previewRequest) request := value.(previewRequest)
commandTemplate = request.template commandTemplate = request.template
@@ -2960,17 +3028,20 @@ func (t *Terminal) Loop() {
items = request.list items = request.list
pwindow = request.pwindow pwindow = request.pwindow
pwindowSize = request.pwindowSize pwindowSize = request.pwindowSize
env = request.env
} }
} }
events.Clear() events.Clear()
}) })
if stop {
break
}
version++ version++
// We don't display preview window if no match // We don't display preview window if no match
if items[0] != nil { if items[0] != nil {
_, query := t.Input() _, query := t.Input()
command := t.replacePlaceholder(commandTemplate, false, string(query), items) command, tempFiles := t.replacePlaceholder(commandTemplate, false, string(query), items)
cmd := t.executor.ExecCommand(command, true) cmd := t.executor.ExecCommand(command, true)
env := t.environ()
if pwindowSize.Lines > 0 { if pwindowSize.Lines > 0 {
lines := fmt.Sprintf("LINES=%d", pwindowSize.Lines) lines := fmt.Sprintf("LINES=%d", pwindowSize.Lines)
columns := fmt.Sprintf("COLUMNS=%d", pwindowSize.Columns) columns := fmt.Sprintf("COLUMNS=%d", pwindowSize.Columns)
@@ -3055,12 +3126,13 @@ func (t *Terminal) Loop() {
Loop: Loop:
for { for {
select { select {
case <-ctx.Done():
break Loop
case <-timer.C: case <-timer.C:
t.reqBox.Set(reqPreviewDelayed, version) t.reqBox.Set(reqPreviewDelayed, version)
case code := <-t.killChan: case immediately := <-t.killChan:
if code != exitCancel { if immediately {
util.KillCommand(cmd) util.KillCommand(cmd)
t.eventBox.Set(EvtQuit, code)
} else { } else {
// We can immediately kill a long-running preview program // We can immediately kill a long-running preview program
// once we started rendering its partial output // once we started rendering its partial output
@@ -3090,12 +3162,11 @@ func (t *Terminal) Loop() {
finishChan <- true // Tell Goroutine 3 to stop finishChan <- true // Tell Goroutine 3 to stop
<-reapChan // Goroutine 2 and 3 finished <-reapChan // Goroutine 2 and 3 finished
<-reapChan <-reapChan
removeFiles(tempFiles)
} else { } else {
// Failed to start the command. Report the error immediately. // Failed to start the command. Report the error immediately.
t.reqBox.Set(reqPreviewDisplay, previewResult{version, []string{err.Error()}, 0, ""}) t.reqBox.Set(reqPreviewDisplay, previewResult{version, []string{err.Error()}, 0, ""})
} }
cleanTemporaryFiles()
} else { } else {
t.reqBox.Set(reqPreviewDisplay, previewResult{version, nil, 0, ""}) t.reqBox.Set(reqPreviewDisplay, previewResult{version, nil, 0, ""})
} }
@@ -3107,19 +3178,25 @@ func (t *Terminal) Loop() {
if len(command) > 0 && t.canPreview() { if len(command) > 0 && t.canPreview() {
_, list := t.buildPlusList(command, false) _, list := t.buildPlusList(command, false)
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.pwindowSize(), t.evaluateScrollOffset(), list}) t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.pwindowSize(), t.evaluateScrollOffset(), list, t.environ()})
} }
} }
go func() { go func() {
var focusedIndex int32 = minItem.Index() var focusedIndex = minItem.Index()
var version int64 = -1 var version int64 = -1
running := true running := true
code := exitError code := ExitError
exit := func(getCode func() int) { exit := func(getCode func() int) {
if t.hasPreviewer() {
t.previewBox.Set(reqQuit, nil)
}
if t.listener != nil {
t.listener.Close()
}
t.tui.Close() t.tui.Close()
code = getCode() code = getCode()
if code <= exitNoMatch && t.history != nil { if code <= ExitNoMatch && t.history != nil {
t.history.append(string(t.input)) t.history.append(string(t.input))
} }
running = false running = false
@@ -3187,9 +3264,9 @@ func (t *Terminal) Loop() {
case reqClose: case reqClose:
exit(func() int { exit(func() int {
if t.output() { if t.output() {
return exitOk return ExitOk
} }
return exitNoMatch return ExitNoMatch
}) })
return return
case reqPreviewDisplay: case reqPreviewDisplay:
@@ -3217,11 +3294,14 @@ func (t *Terminal) Loop() {
case reqPrintQuery: case reqPrintQuery:
exit(func() int { exit(func() int {
t.printer(string(t.input)) t.printer(string(t.input))
return exitOk return ExitOk
}) })
return return
case reqQuit: case reqQuit:
exit(func() int { return exitInterrupt }) exit(func() int { return ExitInterrupt })
return
case reqFatal:
exit(func() int { return ExitError })
return return
} }
} }
@@ -3229,8 +3309,11 @@ func (t *Terminal) Loop() {
t.mutex.Unlock() t.mutex.Unlock()
}) })
} }
// prof.Stop()
t.killPreview(code) t.eventBox.Set(EvtQuit, quitSignal{code, nil})
t.running.Set(false)
t.killPreview()
cancel()
}() }()
looping := true looping := true
@@ -3240,8 +3323,16 @@ func (t *Terminal) Loop() {
barrier := make(chan bool) barrier := make(chan bool)
go func() { go func() {
for { for {
<-barrier select {
t.eventChan <- t.tui.GetChar() case <-ctx.Done():
return
case <-barrier:
}
select {
case <-ctx.Done():
return
case t.eventChan <- t.tui.GetChar():
}
} }
}() }()
previewDraggingPos := -1 previewDraggingPos := -1
@@ -3249,7 +3340,7 @@ func (t *Terminal) Loop() {
pbarDragging := false pbarDragging := false
wasDown := false wasDown := false
for looping { for looping {
var newCommand *string var newCommand *commandSpec
var reloadSync bool var reloadSync bool
changed := false changed := false
beof := false beof := false
@@ -3337,7 +3428,7 @@ func (t *Terminal) Loop() {
t.pressed = ret t.pressed = ret
t.reqBox.Set(reqClose, nil) t.reqBox.Set(reqClose, nil)
t.mutex.Unlock() t.mutex.Unlock()
return return nil
} }
} }
@@ -3346,8 +3437,7 @@ func (t *Terminal) Loop() {
} }
var doAction func(*action) bool var doAction func(*action) bool
var doActions func(actions []*action) bool doActions := func(actions []*action) bool {
doActions = func(actions []*action) bool {
for iter := 0; iter <= maxFocusEvents; iter++ { for iter := 0; iter <= maxFocusEvents; iter++ {
currentIndex := t.currentIndex() currentIndex := t.currentIndex()
for _, action := range actions { for _, action := range actions {
@@ -3375,7 +3465,8 @@ func (t *Terminal) Loop() {
case actBecome: case actBecome:
valid, list := t.buildPlusList(a.a, false) valid, list := t.buildPlusList(a.a, false)
if valid { if valid {
command := t.replacePlaceholder(a.a, false, string(t.input), list) // We do not remove temp files in this case
command, _ := t.replacePlaceholder(a.a, false, string(t.input), list)
t.tui.Close() t.tui.Close()
if t.history != nil { if t.history != nil {
t.history.append(string(t.input)) t.history.append(string(t.input))
@@ -3417,7 +3508,7 @@ func (t *Terminal) Loop() {
if valid { if valid {
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, t.previewBox.Set(reqPreviewEnqueue,
previewRequest{t.previewOpts.command, t.pwindow, t.pwindowSize(), t.evaluateScrollOffset(), list}) previewRequest{t.previewOpts.command, t.pwindow, t.pwindowSize(), t.evaluateScrollOffset(), list, t.environ()})
} }
} else { } else {
// Discard the preview content so that it won't accidentally appear // Discard the preview content so that it won't accidentally appear
@@ -3531,8 +3622,9 @@ func (t *Terminal) Loop() {
} }
case actTransform: case actTransform:
body := t.executeCommand(a.a, false, true, true, false) body := t.executeCommand(a.a, false, true, true, false)
actions := parseSingleActionList(strings.Trim(body, "\r\n"), func(message string) {}) if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil {
return doActions(actions) return doActions(actions)
}
case actTransformBorderLabel: case actTransformBorderLabel:
label := t.executeCommand(a.a, false, true, true, true) label := t.executeCommand(a.a, false, true, true, true)
t.borderLabelOpts.label = label t.borderLabelOpts.label = label
@@ -3564,6 +3656,8 @@ func (t *Terminal) Loop() {
t.input = current.text.ToRunes() t.input = current.text.ToRunes()
t.cx = len(t.input) t.cx = len(t.input)
} }
case actFatal:
req(reqFatal)
case actAbort: case actAbort:
req(reqQuit) req(reqQuit)
case actDeleteChar: case actDeleteChar:
@@ -3989,10 +4083,10 @@ func (t *Terminal) Loop() {
} }
if me.Down { if me.Down {
mx = util.Constrain(mx-t.promptLen, 0, len(t.input)) mxCons := util.Constrain(mx-t.promptLen, 0, len(t.input))
if my == t.promptLine() && mx >= 0 { if my == t.promptLine() && mxCons >= 0 {
// Prompt // Prompt
t.cx = mx + t.xoffset t.cx = mxCons + t.xoffset
} else if my >= min { } else if my >= min {
t.vset(t.offset + my - min) t.vset(t.offset + my - min)
req(reqList) req(reqList)
@@ -4007,6 +4101,29 @@ func (t *Terminal) Loop() {
} }
} }
return doActions(actionsFor(evt)) return doActions(actionsFor(evt))
} else if t.headerVisible {
// Header
numLines := t.visibleHeaderLines()
lineOffset := 0
if !t.headerFirst {
// offset for info line
if t.noSeparatorLine() {
lineOffset = 1
} else {
lineOffset = 2
}
}
my -= lineOffset
mx -= 2 // offset gutter
if my >= 0 && my < numLines && mx >= 0 {
if t.layout == layoutReverse {
t.clickHeaderLine = my + 1
} else {
t.clickHeaderLine = numLines - my
}
t.clickHeaderColumn = mx + 1
return doActions(actionsFor(tui.ClickHeader))
}
} }
} }
case actReload, actReloadSync: case actReload, actReloadSync:
@@ -4021,21 +4138,23 @@ func (t *Terminal) Loop() {
valid = !slot || forceUpdate valid = !slot || forceUpdate
} }
if valid { if valid {
command := t.replacePlaceholder(a.a, false, string(t.input), list) command, tempFiles := t.replacePlaceholder(a.a, false, string(t.input), list)
newCommand = &command newCommand = &commandSpec{command, tempFiles}
reloadSync = a.t == actReloadSync reloadSync = a.t == actReloadSync
t.reading = true t.reading = true
} }
case actUnbind: case actUnbind:
keys := parseKeyChords(a.a, "PANIC") if keys, err := parseKeyChords(a.a, "PANIC"); err == nil {
for key := range keys { for key := range keys {
delete(t.keymap, key) delete(t.keymap, key)
}
} }
case actRebind: case actRebind:
keys := parseKeyChords(a.a, "PANIC") if keys, err := parseKeyChords(a.a, "PANIC"); err == nil {
for key := range keys { for key := range keys {
if originalAction, found := t.keymapOrg[key]; found { if originalAction, found := t.keymapOrg[key]; found {
t.keymap[key] = originalAction t.keymap[key] = originalAction
}
} }
} }
case actChangePreview: case actChangePreview:
@@ -4049,6 +4168,7 @@ func (t *Terminal) Loop() {
// Reset preview options and apply the additional options // Reset preview options and apply the additional options
t.previewOpts = t.initialPreviewOpts t.previewOpts = t.initialPreviewOpts
t.previewOpts.command = currentPreviewOpts.command
// Split window options // Split window options
tokens := strings.Split(a.a, "|") tokens := strings.Split(a.a, "|")
@@ -4181,6 +4301,7 @@ func (t *Terminal) Loop() {
t.reqBox.Set(event, nil) t.reqBox.Set(event, nil)
} }
} }
return nil
} }
func (t *Terminal) constrain() { func (t *Terminal) constrain() {

View File

@@ -13,7 +13,7 @@ import (
) )
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string { func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
return replacePlaceholder(replacePlaceholderParams{ replaced, _ := replacePlaceholder(replacePlaceholderParams{
template: template, template: template,
stripAnsi: stripAnsi, stripAnsi: stripAnsi,
delimiter: delimiter, delimiter: delimiter,
@@ -25,6 +25,7 @@ func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter
prompt: "prompt", prompt: "prompt",
executor: util.NewExecutor(""), executor: util.NewExecutor(""),
}) })
return replaced
} }
func TestReplacePlaceholder(t *testing.T) { func TestReplacePlaceholder(t *testing.T) {

View File

@@ -91,7 +91,7 @@ func withPrefixLengths(tokens []string, begin int) []Token {
prefixLength := begin prefixLength := begin
for idx := range tokens { for idx := range tokens {
chars := util.ToChars(sbytes(tokens[idx])) chars := util.ToChars(stringBytes(tokens[idx]))
ret[idx] = Token{&chars, int32(prefixLength)} ret[idx] = Token{&chars, int32(prefixLength)}
prefixLength += chars.Length() prefixLength += chars.Length()
} }
@@ -187,7 +187,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
if r.begin == r.end { if r.begin == r.end {
idx := r.begin idx := r.begin
if idx == rangeEllipsis { if idx == rangeEllipsis {
chars := util.ToChars(sbytes(joinTokens(tokens))) chars := util.ToChars(stringBytes(joinTokens(tokens)))
parts = append(parts, &chars) parts = append(parts, &chars)
} else { } else {
if idx < 0 { if idx < 0 {

View File

@@ -71,14 +71,14 @@ func TestTransform(t *testing.T) {
{ {
tokens := Tokenize(input, Delimiter{}) tokens := Tokenize(input, Delimiter{})
{ {
ranges := splitNth("1,2,3") ranges, _ := splitNth("1,2,3")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if joinTokens(tx) != "abc: def: ghi: " { if joinTokens(tx) != "abc: def: ghi: " {
t.Errorf("%s", tx) t.Errorf("%s", tx)
} }
} }
{ {
ranges := splitNth("1..2,3,2..,1") ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " || if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
len(tx) != 4 || len(tx) != 4 ||
@@ -93,7 +93,7 @@ func TestTransform(t *testing.T) {
{ {
tokens := Tokenize(input, delimiterRegexp(":")) tokens := Tokenize(input, delimiterRegexp(":"))
{ {
ranges := splitNth("1..2,3,2..,1") ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" || if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(tx) != 4 || len(tx) != 4 ||
@@ -108,5 +108,6 @@ func TestTransform(t *testing.T) {
} }
func TestTransformIndexOutOfBounds(t *testing.T) { func TestTransformIndexOutOfBounds(t *testing.T) {
Transform([]Token{}, splitNth("1")) s, _ := splitNth("1")
Transform([]Token{}, s)
} }

View File

@@ -8,7 +8,7 @@ func HasFullscreenRenderer() bool {
return false return false
} }
var DefaultBorderShape BorderShape = BorderRounded var DefaultBorderShape = BorderRounded
func (a Attr) Merge(b Attr) Attr { func (a Attr) Merge(b Attr) Attr {
return a | b return a | b
@@ -29,7 +29,7 @@ const (
StrikeThrough = Attr(1 << 7) StrikeThrough = Attr(1 << 7)
) )
func (r *FullscreenRenderer) Init() {} func (r *FullscreenRenderer) Init() error { return nil }
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {} func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
func (r *FullscreenRenderer) Pause(bool) {} func (r *FullscreenRenderer) Pause(bool) {}
func (r *FullscreenRenderer) Resume(bool, bool) {} func (r *FullscreenRenderer) Resume(bool, bool) {}

View File

@@ -83,34 +83,35 @@ func _() {
_ = x[Alt-72] _ = x[Alt-72]
_ = x[CtrlAlt-73] _ = x[CtrlAlt-73]
_ = x[Invalid-74] _ = x[Invalid-74]
_ = x[Mouse-75] _ = x[Fatal-75]
_ = x[DoubleClick-76] _ = x[Mouse-76]
_ = x[LeftClick-77] _ = x[DoubleClick-77]
_ = x[RightClick-78] _ = x[LeftClick-78]
_ = x[SLeftClick-79] _ = x[RightClick-79]
_ = x[SRightClick-80] _ = x[SLeftClick-80]
_ = x[ScrollUp-81] _ = x[SRightClick-81]
_ = x[ScrollDown-82] _ = x[ScrollUp-82]
_ = x[SScrollUp-83] _ = x[ScrollDown-83]
_ = x[SScrollDown-84] _ = x[SScrollUp-84]
_ = x[PreviewScrollUp-85] _ = x[SScrollDown-85]
_ = x[PreviewScrollDown-86] _ = x[PreviewScrollUp-86]
_ = x[Resize-87] _ = x[PreviewScrollDown-87]
_ = x[Change-88] _ = x[Resize-88]
_ = x[BackwardEOF-89] _ = x[Change-89]
_ = x[Start-90] _ = x[BackwardEOF-90]
_ = x[Load-91] _ = x[Start-91]
_ = x[Focus-92] _ = x[Load-92]
_ = x[One-93] _ = x[Focus-93]
_ = x[Zero-94] _ = x[One-94]
_ = x[Result-95] _ = x[Zero-95]
_ = x[Jump-96] _ = x[Result-96]
_ = x[JumpCancel-97] _ = x[Jump-97]
_ = x[JumpCancel-98]
} }
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancel" const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancel"
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} 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, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637}
func (i EventType) String() string { func (i EventType) String() string {
if i < 0 || i >= EventType(len(_EventType_index)-1) { if i < 0 || i >= EventType(len(_EventType_index)-1) {

View File

@@ -2,6 +2,7 @@ package tui
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
@@ -10,6 +11,7 @@ import (
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
"golang.org/x/term" "golang.org/x/term"
@@ -27,8 +29,8 @@ const (
const consoleDevice string = "/dev/tty" const consoleDevice string = "/dev/tty"
var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R") var offsetRegexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R") var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
func (r *LightRenderer) PassThrough(str string) { func (r *LightRenderer) PassThrough(str string) {
r.queued.WriteString("\x1b7" + str + "\x1b8") r.queued.WriteString("\x1b7" + str + "\x1b8")
@@ -78,6 +80,7 @@ func (r *LightRenderer) flush() {
// Light renderer // Light renderer
type LightRenderer struct { type LightRenderer struct {
closed *util.AtomicBool
theme *ColorTheme theme *ColorTheme
mouse bool mouse bool
forceBlack bool forceBlack bool
@@ -123,19 +126,24 @@ type LightWindow struct {
bg Color bg Color
} }
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) Renderer { func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
in, err := openTtyIn()
if err != nil {
return nil, err
}
r := LightRenderer{ r := LightRenderer{
closed: util.NewAtomicBool(false),
theme: theme, theme: theme,
forceBlack: forceBlack, forceBlack: forceBlack,
mouse: mouse, mouse: mouse,
clearOnExit: clearOnExit, clearOnExit: clearOnExit,
ttyin: openTtyIn(), ttyin: in,
yoffset: 0, yoffset: 0,
tabstop: tabstop, tabstop: tabstop,
fullscreen: fullscreen, fullscreen: fullscreen,
upOneLine: false, upOneLine: false,
maxHeightFunc: maxHeightFunc} maxHeightFunc: maxHeightFunc}
return &r return &r, nil
} }
func repeat(r rune, times int) string { func repeat(r rune, times int) string {
@@ -153,11 +161,11 @@ func atoi(s string, defaultValue int) int {
return value return value
} }
func (r *LightRenderer) Init() { func (r *LightRenderer) Init() error {
r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay) r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
if err := r.initPlatform(); err != nil { if err := r.initPlatform(); err != nil {
errorExit(err.Error()) return err
} }
r.updateTerminalSize() r.updateTerminalSize()
initTheme(r.theme, r.defaultTheme(), r.forceBlack) initTheme(r.theme, r.defaultTheme(), r.forceBlack)
@@ -195,6 +203,7 @@ func (r *LightRenderer) Init() {
if !r.fullscreen && r.mouse { if !r.fullscreen && r.mouse {
r.yoffset, _ = r.findOffset() r.yoffset, _ = r.findOffset()
} }
return nil
} }
func (r *LightRenderer) Resize(maxHeightFunc func(int) int) { func (r *LightRenderer) Resize(maxHeightFunc func(int) int) {
@@ -233,15 +242,16 @@ func getEnv(name string, defaultValue int) int {
return atoi(env, defaultValue) return atoi(env, defaultValue)
} }
func (r *LightRenderer) getBytes() []byte { func (r *LightRenderer) getBytes() ([]byte, error) {
return r.getBytesInternal(r.buffer, false) bytes, err := r.getBytesInternal(r.buffer, false)
return bytes, err
} }
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte { func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte, error) {
c, ok := r.getch(nonblock) c, ok := r.getch(nonblock)
if !nonblock && !ok { if !nonblock && !ok {
r.Close() r.Close()
errorExit("Failed to read " + consoleDevice) return nil, errors.New("failed to read " + consoleDevice)
} }
retries := 0 retries := 0
@@ -272,19 +282,23 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
// so terminate fzf immediately. // so terminate fzf immediately.
if len(buffer) > maxInputBuffer { if len(buffer) > maxInputBuffer {
r.Close() r.Close()
panic(fmt.Sprintf("Input buffer overflow (%d): %v", len(buffer), buffer)) return nil, fmt.Errorf("input buffer overflow (%d): %v", len(buffer), buffer)
} }
} }
return buffer return buffer, nil
} }
func (r *LightRenderer) GetChar() Event { func (r *LightRenderer) GetChar() Event {
var err error
if len(r.buffer) == 0 { if len(r.buffer) == 0 {
r.buffer = r.getBytes() r.buffer, err = r.getBytes()
if err != nil {
return Event{Fatal, 0, nil}
}
} }
if len(r.buffer) == 0 { if len(r.buffer) == 0 {
panic("Empty buffer") return Event{Fatal, 0, nil}
} }
sz := 1 sz := 1
@@ -315,7 +329,9 @@ func (r *LightRenderer) GetChar() Event {
ev := r.escSequence(&sz) ev := r.escSequence(&sz)
// Second chance // Second chance
if ev.Type == Invalid { if ev.Type == Invalid {
r.buffer = r.getBytes() if r.buffer, err = r.getBytes(); err != nil {
return Event{Fatal, 0, nil}
}
ev = r.escSequence(&sz) ev = r.escSequence(&sz)
} }
return ev return ev
@@ -738,6 +754,7 @@ func (r *LightRenderer) Close() {
r.flush() r.flush()
r.closePlatform() r.closePlatform()
r.restoreTerminal() r.restoreTerminal()
r.closed.Set(true)
} }
func (r *LightRenderer) Top() int { func (r *LightRenderer) Top() int {
@@ -821,44 +838,32 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
color = ColPreviewBorder color = ColPreviewBorder
} }
hw := runeWidth(w.border.top) hw := runeWidth(w.border.top)
pad := repeat(' ', w.width/hw)
w.Move(0, 0)
if top { if top {
w.Move(0, 0)
w.CPrint(color, repeat(w.border.top, w.width/hw)) w.CPrint(color, repeat(w.border.top, w.width/hw))
} else {
w.CPrint(color, pad)
} }
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(color, pad)
}
w.Move(w.height-1, 0)
if bottom { if bottom {
w.Move(w.height-1, 0)
w.CPrint(color, repeat(w.border.bottom, w.width/hw)) w.CPrint(color, repeat(w.border.bottom, w.width/hw))
} else {
w.CPrint(color, pad)
} }
} }
func (w *LightWindow) drawBorderVertical(left, right bool) { func (w *LightWindow) drawBorderVertical(left, right bool) {
width := w.width - 2 vw := runeWidth(w.border.left)
if !left || !right {
width++
}
color := ColBorder color := ColBorder
if w.preview { if w.preview {
color = ColPreviewBorder color = ColPreviewBorder
} }
for y := 0; y < w.height; y++ { for y := 0; y < w.height; y++ {
w.Move(y, 0)
if left { if left {
w.Move(y, 0)
w.CPrint(color, string(w.border.left)) w.CPrint(color, string(w.border.left))
w.CPrint(color, " ") // Margin
} }
w.CPrint(color, repeat(' ', width))
if right { if right {
w.Move(y, w.width-vw-1)
w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right)) w.CPrint(color, string(w.border.right))
} }
} }
@@ -880,7 +885,10 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
for y := 1; y < w.height-1; y++ { for y := 1; y < w.height-1; y++ {
w.Move(y, 0) w.Move(y, 0)
w.CPrint(color, string(w.border.left)) w.CPrint(color, string(w.border.left))
w.CPrint(color, repeat(' ', w.width-vw*2)) w.CPrint(color, " ") // Margin
w.Move(y, w.width-vw-1)
w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right)) w.CPrint(color, string(w.border.right))
} }
} }

View File

@@ -3,7 +3,7 @@
package tui package tui
import ( import (
"fmt" "errors"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
@@ -48,19 +48,18 @@ func (r *LightRenderer) closePlatform() {
// NOOP // NOOP
} }
func openTtyIn() *os.File { func openTtyIn() (*os.File, error) {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
if err != nil { if err != nil {
tty := ttyname() tty := ttyname()
if len(tty) > 0 { if len(tty) > 0 {
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil { if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
return in return in, nil
} }
} }
fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice) return nil, errors.New("failed to open " + consoleDevice)
util.Exit(2)
} }
return in return in, nil
} }
func (r *LightRenderer) setupTerminal() { func (r *LightRenderer) setupTerminal() {
@@ -86,9 +85,14 @@ func (r *LightRenderer) updateTerminalSize() {
func (r *LightRenderer) findOffset() (row int, col int) { func (r *LightRenderer) findOffset() (row int, col int) {
r.csi("6n") r.csi("6n")
r.flush() r.flush()
var err error
bytes := []byte{} bytes := []byte{}
for tries := 0; tries < offsetPollTries; tries++ { for tries := 0; tries < offsetPollTries; tries++ {
bytes = r.getBytesInternal(bytes, tries > 0) bytes, err = r.getBytesInternal(bytes, tries > 0)
if err != nil {
return -1, -1
}
offsets := offsetRegexp.FindSubmatch(bytes) offsets := offsetRegexp.FindSubmatch(bytes)
if len(offsets) > 3 { if len(offsets) > 3 {
// Add anything we skipped over to the input buffer // Add anything we skipped over to the input buffer

View File

@@ -72,7 +72,7 @@ func (r *LightRenderer) initPlatform() error {
go func() { go func() {
fd := int(r.inHandle) fd := int(r.inHandle)
b := make([]byte, 1) b := make([]byte, 1)
for { for !r.closed.Get() {
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT. // HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput) _ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
@@ -91,9 +91,9 @@ func (r *LightRenderer) closePlatform() {
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput) windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
} }
func openTtyIn() *os.File { func openTtyIn() (*os.File, error) {
// not used // not used
return nil return nil, nil
} }
func (r *LightRenderer) setupTerminal() error { func (r *LightRenderer) setupTerminal() error {

View File

@@ -7,7 +7,6 @@ import (
"time" "time"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/encoding"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
@@ -146,13 +145,13 @@ var (
_initialResize bool = true _initialResize bool = true
) )
func (r *FullscreenRenderer) initScreen() { func (r *FullscreenRenderer) initScreen() error {
s, e := tcell.NewScreen() s, e := tcell.NewScreen()
if e != nil { if e != nil {
errorExit(e.Error()) return e
} }
if e = s.Init(); e != nil { if e = s.Init(); e != nil {
errorExit(e.Error()) return e
} }
if r.mouse { if r.mouse {
s.EnableMouse() s.EnableMouse()
@@ -160,16 +159,21 @@ func (r *FullscreenRenderer) initScreen() {
s.DisableMouse() s.DisableMouse()
} }
_screen = s _screen = s
return nil
} }
func (r *FullscreenRenderer) Init() { func (r *FullscreenRenderer) Init() error {
if os.Getenv("TERM") == "cygwin" { if os.Getenv("TERM") == "cygwin" {
os.Setenv("TERM", "") os.Setenv("TERM", "")
} }
encoding.Register()
r.initScreen() if err := r.initScreen(); err != nil {
return err
}
initTheme(r.theme, r.defaultTheme(), r.forceBlack) initTheme(r.theme, r.defaultTheme(), r.forceBlack)
return nil
} }
func (r *FullscreenRenderer) Top() int { func (r *FullscreenRenderer) Top() int {
@@ -561,7 +565,11 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
} }
func (w *TcellWindow) Erase() { func (w *TcellWindow) Erase() {
fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ') if w.borderStyle.shape.HasLeft() {
fill(w.left-1, w.top, w.width, w.height-1, w.normal, ' ')
} else {
fill(w.left, w.top, w.width-1, w.height-1, w.normal, ' ')
}
w.drawBorder(false) w.drawBorder(false)
} }

View File

@@ -1,8 +1,6 @@
package tui package tui
import ( import (
"fmt"
"os"
"strconv" "strconv"
"time" "time"
@@ -104,6 +102,7 @@ const (
CtrlAlt CtrlAlt
Invalid Invalid
Fatal
Mouse Mouse
DoubleClick DoubleClick
@@ -130,6 +129,7 @@ const (
Result Result
Jump Jump
JumpCancel JumpCancel
ClickHeader
) )
func (t EventType) AsEvent() Event { func (t EventType) AsEvent() Event {
@@ -303,6 +303,9 @@ type ColorTheme struct {
Disabled ColorAttr Disabled ColorAttr
Fg ColorAttr Fg ColorAttr
Bg ColorAttr Bg ColorAttr
SelectedFg ColorAttr
SelectedBg ColorAttr
SelectedMatch ColorAttr
PreviewFg ColorAttr PreviewFg ColorAttr
PreviewBg ColorAttr PreviewBg ColorAttr
DarkBg ColorAttr DarkBg ColorAttr
@@ -314,7 +317,7 @@ type ColorTheme struct {
Spinner ColorAttr Spinner ColorAttr
Info ColorAttr Info ColorAttr
Cursor ColorAttr Cursor ColorAttr
Selected ColorAttr Marker ColorAttr
Header ColorAttr Header ColorAttr
Separator ColorAttr Separator ColorAttr
Scrollbar ColorAttr Scrollbar ColorAttr
@@ -368,6 +371,14 @@ const (
BorderRight BorderRight
) )
func (s BorderShape) HasLeft() bool {
switch s {
case BorderNone, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
return false
}
return true
}
func (s BorderShape) HasRight() bool { func (s BorderShape) HasRight() bool {
switch s { switch s {
case BorderNone, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right case BorderNone, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
@@ -516,7 +527,7 @@ type TermSize struct {
} }
type Renderer interface { type Renderer interface {
Init() Init() error
Resize(maxHeightFunc func(int) int) Resize(maxHeightFunc func(int) int)
Pause(clear bool) Pause(clear bool)
Resume(clear bool, sigcont bool) Resume(clear bool, sigcont bool)
@@ -595,12 +606,14 @@ var (
ColMatch ColorPair ColMatch ColorPair
ColCursor ColorPair ColCursor ColorPair
ColCursorEmpty ColorPair ColCursorEmpty ColorPair
ColMarker ColorPair
ColSelected ColorPair ColSelected ColorPair
ColSelectedMatch ColorPair
ColCurrent ColorPair ColCurrent ColorPair
ColCurrentMatch ColorPair ColCurrentMatch ColorPair
ColCurrentCursor ColorPair ColCurrentCursor ColorPair
ColCurrentCursorEmpty ColorPair ColCurrentCursorEmpty ColorPair
ColCurrentSelected ColorPair ColCurrentMarker ColorPair
ColCurrentSelectedEmpty ColorPair ColCurrentSelectedEmpty ColorPair
ColSpinner ColorPair ColSpinner ColorPair
ColInfo ColorPair ColInfo ColorPair
@@ -622,6 +635,9 @@ func EmptyTheme() *ColorTheme {
Input: ColorAttr{colUndefined, AttrUndefined}, Input: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colUndefined, AttrUndefined}, Fg: ColorAttr{colUndefined, AttrUndefined},
Bg: ColorAttr{colUndefined, AttrUndefined}, Bg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colUndefined, AttrUndefined}, DarkBg: ColorAttr{colUndefined, AttrUndefined},
Prompt: ColorAttr{colUndefined, AttrUndefined}, Prompt: ColorAttr{colUndefined, AttrUndefined},
Match: ColorAttr{colUndefined, AttrUndefined}, Match: ColorAttr{colUndefined, AttrUndefined},
@@ -630,7 +646,7 @@ func EmptyTheme() *ColorTheme {
Spinner: ColorAttr{colUndefined, AttrUndefined}, Spinner: ColorAttr{colUndefined, AttrUndefined},
Info: ColorAttr{colUndefined, AttrUndefined}, Info: ColorAttr{colUndefined, AttrUndefined},
Cursor: ColorAttr{colUndefined, AttrUndefined}, Cursor: ColorAttr{colUndefined, AttrUndefined},
Selected: ColorAttr{colUndefined, AttrUndefined}, Marker: ColorAttr{colUndefined, AttrUndefined},
Header: ColorAttr{colUndefined, AttrUndefined}, Header: ColorAttr{colUndefined, AttrUndefined},
Border: ColorAttr{colUndefined, AttrUndefined}, Border: ColorAttr{colUndefined, AttrUndefined},
BorderLabel: ColorAttr{colUndefined, AttrUndefined}, BorderLabel: ColorAttr{colUndefined, AttrUndefined},
@@ -652,6 +668,9 @@ func NoColorTheme() *ColorTheme {
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{colDefault, AttrUndefined}, DarkBg: ColorAttr{colDefault, AttrUndefined},
Prompt: ColorAttr{colDefault, AttrUndefined}, Prompt: ColorAttr{colDefault, AttrUndefined},
Match: ColorAttr{colDefault, Underline}, Match: ColorAttr{colDefault, Underline},
@@ -660,7 +679,7 @@ func NoColorTheme() *ColorTheme {
Spinner: ColorAttr{colDefault, AttrUndefined}, Spinner: ColorAttr{colDefault, AttrUndefined},
Info: ColorAttr{colDefault, AttrUndefined}, Info: ColorAttr{colDefault, AttrUndefined},
Cursor: ColorAttr{colDefault, AttrUndefined}, Cursor: ColorAttr{colDefault, AttrUndefined},
Selected: ColorAttr{colDefault, AttrUndefined}, Marker: ColorAttr{colDefault, AttrUndefined},
Header: ColorAttr{colDefault, AttrUndefined}, Header: ColorAttr{colDefault, AttrUndefined},
Border: ColorAttr{colDefault, AttrUndefined}, Border: ColorAttr{colDefault, AttrUndefined},
BorderLabel: ColorAttr{colDefault, AttrUndefined}, BorderLabel: ColorAttr{colDefault, AttrUndefined},
@@ -676,17 +695,15 @@ func NoColorTheme() *ColorTheme {
} }
} }
func errorExit(message string) {
fmt.Fprintln(os.Stderr, message)
util.Exit(2)
}
func init() { func init() {
Default16 = &ColorTheme{ Default16 = &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{colBlack, AttrUndefined}, DarkBg: ColorAttr{colBlack, AttrUndefined},
Prompt: ColorAttr{colBlue, AttrUndefined}, Prompt: ColorAttr{colBlue, AttrUndefined},
Match: ColorAttr{colGreen, AttrUndefined}, Match: ColorAttr{colGreen, AttrUndefined},
@@ -695,7 +712,7 @@ func init() {
Spinner: ColorAttr{colGreen, AttrUndefined}, Spinner: ColorAttr{colGreen, AttrUndefined},
Info: ColorAttr{colWhite, AttrUndefined}, Info: ColorAttr{colWhite, AttrUndefined},
Cursor: ColorAttr{colRed, AttrUndefined}, Cursor: ColorAttr{colRed, AttrUndefined},
Selected: ColorAttr{colMagenta, AttrUndefined}, Marker: ColorAttr{colMagenta, AttrUndefined},
Header: ColorAttr{colCyan, AttrUndefined}, Header: ColorAttr{colCyan, AttrUndefined},
Border: ColorAttr{colBlack, AttrUndefined}, Border: ColorAttr{colBlack, AttrUndefined},
BorderLabel: ColorAttr{colWhite, AttrUndefined}, BorderLabel: ColorAttr{colWhite, AttrUndefined},
@@ -714,6 +731,9 @@ func init() {
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{236, AttrUndefined}, DarkBg: ColorAttr{236, AttrUndefined},
Prompt: ColorAttr{110, AttrUndefined}, Prompt: ColorAttr{110, AttrUndefined},
Match: ColorAttr{108, AttrUndefined}, Match: ColorAttr{108, AttrUndefined},
@@ -722,7 +742,7 @@ func init() {
Spinner: ColorAttr{148, AttrUndefined}, Spinner: ColorAttr{148, AttrUndefined},
Info: ColorAttr{144, AttrUndefined}, Info: ColorAttr{144, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined}, Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{109, AttrUndefined}, Header: ColorAttr{109, AttrUndefined},
Border: ColorAttr{59, AttrUndefined}, Border: ColorAttr{59, AttrUndefined},
BorderLabel: ColorAttr{145, AttrUndefined}, BorderLabel: ColorAttr{145, AttrUndefined},
@@ -741,6 +761,9 @@ func init() {
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{251, AttrUndefined}, DarkBg: ColorAttr{251, AttrUndefined},
Prompt: ColorAttr{25, AttrUndefined}, Prompt: ColorAttr{25, AttrUndefined},
Match: ColorAttr{66, AttrUndefined}, Match: ColorAttr{66, AttrUndefined},
@@ -749,7 +772,7 @@ func init() {
Spinner: ColorAttr{65, AttrUndefined}, Spinner: ColorAttr{65, AttrUndefined},
Info: ColorAttr{101, AttrUndefined}, Info: ColorAttr{101, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined}, Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{31, AttrUndefined}, Header: ColorAttr{31, AttrUndefined},
Border: ColorAttr{145, AttrUndefined}, Border: ColorAttr{145, AttrUndefined},
BorderLabel: ColorAttr{59, AttrUndefined}, BorderLabel: ColorAttr{59, AttrUndefined},
@@ -791,12 +814,15 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
theme.Spinner = o(baseTheme.Spinner, theme.Spinner) theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
theme.Info = o(baseTheme.Info, theme.Info) theme.Info = o(baseTheme.Info, theme.Info)
theme.Cursor = o(baseTheme.Cursor, theme.Cursor) theme.Cursor = o(baseTheme.Cursor, theme.Cursor)
theme.Selected = o(baseTheme.Selected, theme.Selected) theme.Marker = o(baseTheme.Marker, theme.Marker)
theme.Header = o(baseTheme.Header, theme.Header) theme.Header = o(baseTheme.Header, theme.Header)
theme.Border = o(baseTheme.Border, theme.Border) theme.Border = o(baseTheme.Border, theme.Border)
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel) theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel)
// These colors are not defined in the base themes // These colors are not defined in the base themes
theme.SelectedFg = o(theme.Fg, theme.SelectedFg)
theme.SelectedBg = o(theme.Bg, theme.SelectedBg)
theme.SelectedMatch = o(theme.CurrentMatch, theme.SelectedMatch)
theme.Disabled = o(theme.Input, theme.Disabled) theme.Disabled = o(theme.Input, theme.Disabled)
theme.Gutter = o(theme.DarkBg, theme.Gutter) theme.Gutter = o(theme.DarkBg, theme.Gutter)
theme.PreviewFg = o(theme.Fg, theme.PreviewFg) theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
@@ -822,17 +848,23 @@ func initPalette(theme *ColorTheme) {
ColPrompt = pair(theme.Prompt, theme.Bg) ColPrompt = pair(theme.Prompt, theme.Bg)
ColNormal = pair(theme.Fg, theme.Bg) ColNormal = pair(theme.Fg, theme.Bg)
ColSelected = pair(theme.SelectedFg, theme.SelectedBg)
ColInput = pair(theme.Input, theme.Bg) ColInput = pair(theme.Input, theme.Bg)
ColDisabled = pair(theme.Disabled, theme.Bg) ColDisabled = pair(theme.Disabled, theme.Bg)
ColMatch = pair(theme.Match, theme.Bg) ColMatch = pair(theme.Match, theme.Bg)
ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg)
ColCursor = pair(theme.Cursor, theme.Gutter) ColCursor = pair(theme.Cursor, theme.Gutter)
ColCursorEmpty = pair(blank, theme.Gutter) ColCursorEmpty = pair(blank, theme.Gutter)
ColSelected = pair(theme.Selected, theme.Gutter) if theme.SelectedBg.Color != theme.Bg.Color {
ColMarker = pair(theme.Marker, theme.SelectedBg)
} else {
ColMarker = pair(theme.Marker, theme.Gutter)
}
ColCurrent = pair(theme.Current, theme.DarkBg) ColCurrent = pair(theme.Current, theme.DarkBg)
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg) ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
ColCurrentCursor = pair(theme.Cursor, theme.DarkBg) ColCurrentCursor = pair(theme.Cursor, theme.DarkBg)
ColCurrentCursorEmpty = pair(blank, theme.DarkBg) ColCurrentCursorEmpty = pair(blank, theme.DarkBg)
ColCurrentSelected = pair(theme.Selected, theme.DarkBg) ColCurrentMarker = pair(theme.Marker, theme.DarkBg)
ColCurrentSelectedEmpty = pair(blank, theme.DarkBg) ColCurrentSelectedEmpty = pair(blank, theme.DarkBg)
ColSpinner = pair(theme.Spinner, theme.Bg) ColSpinner = pair(theme.Spinner, theme.Bg)
ColInfo = pair(theme.Info, theme.Bg) ColInfo = pair(theme.Info, theme.Bg)

View File

@@ -1,7 +1,6 @@
package util package util
import ( import (
"os"
"sync" "sync"
) )
@@ -25,14 +24,5 @@ func RunAtExitFuncs() {
for i := len(fns) - 1; i >= 0; i-- { for i := len(fns) - 1; i >= 0; i-- {
fns[i]() fns[i]()
} }
} atExitFuncs = nil
// 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()
} }

View File

@@ -61,7 +61,7 @@ func (x *Executor) Become(stdin *os.File, environ []string, command string) {
shellPath, err := exec.LookPath(x.shell) shellPath, err := exec.LookPath(x.shell)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error()) fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error())
Exit(127) os.Exit(127)
} }
args := append([]string{shellPath}, append(x.args, command)...) args := append([]string{shellPath}, append(x.args, command)...)
SetStdin(stdin) SetStdin(stdin)

View File

@@ -42,7 +42,7 @@ func NewExecutor(withShell string) *Executor {
args = args[1:] args = args[1:]
} else if strings.HasPrefix(basename, "cmd") { } else if strings.HasPrefix(basename, "cmd") {
shellType = shellTypeCmd shellType = shellTypeCmd
args = []string{"/v:on/s/c"} args = []string{"/s/c"}
} else if strings.HasPrefix(basename, "pwsh") || strings.HasPrefix(basename, "powershell") { } else if strings.HasPrefix(basename, "pwsh") || strings.HasPrefix(basename, "powershell") {
shellType = shellTypePowerShell shellType = shellTypePowerShell
args = []string{"-NoProfile", "-Command"} args = []string{"-NoProfile", "-Command"}
@@ -97,15 +97,15 @@ func (x *Executor) Become(stdin *os.File, environ []string, command string) {
err := cmd.Start() err := cmd.Start()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error()) fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error())
Exit(127) os.Exit(127)
} }
err = cmd.Wait() err = cmd.Wait()
if err != nil { if err != nil {
if exitError, ok := err.(*exec.ExitError); ok { if exitError, ok := err.(*exec.ExitError); ok {
Exit(exitError.ExitCode()) os.Exit(exitError.ExitCode())
} }
} }
Exit(0) os.Exit(0)
} }
func escapeArg(s string) string { func escapeArg(s string) string {
@@ -119,8 +119,6 @@ func escapeArg(s string) string {
slashes = 0 slashes = 0
case '\\': case '\\':
slashes++ slashes++
case '&', '|', '<', '>', '(', ')', '@', '^', '%', '!':
b = append(b, '^')
case '"': case '"':
for ; slashes > 0; slashes-- { for ; slashes > 0; slashes-- {
b = append(b, '\\') b = append(b, '\\')

View File

@@ -557,7 +557,7 @@ class TestGoFZF < TestBase
def test_expect def test_expect
test = lambda do |key, feed, expected = key| test = lambda do |key, feed, expected = key|
tmux.send_keys "seq 1 100 | #{fzf(:expect, key)}", :Enter tmux.send_keys "seq 1 100 | #{fzf(:expect, key, :prompt, "[#{key}]")}", :Enter
tmux.until { |lines| assert_equal ' 100/100', lines[-2] } tmux.until { |lines| assert_equal ' 100/100', lines[-2] }
tmux.send_keys '55' tmux.send_keys '55'
tmux.until { |lines| assert_equal ' 1/100', lines[-2] } tmux.until { |lines| assert_equal ' 1/100', lines[-2] }
@@ -2693,6 +2693,13 @@ class TestGoFZF < TestBase
end end
end end
def test_change_preview_window_should_not_reset_change_preview
tmux.send_keys "#{FZF} --preview-window up,border-none --bind 'start:change-preview(echo hello)' --bind 'enter:change-preview-window(border-left)'", :Enter
tmux.until { |lines| assert_includes lines, 'hello' }
tmux.send_keys :Enter
tmux.until { |lines| assert_includes lines, '│ hello' }
end
def test_change_preview_window_rotate def test_change_preview_window_rotate
tmux.send_keys "seq 100 | #{FZF} --preview-window left,border-none --preview 'echo hello' --bind '" \ tmux.send_keys "seq 100 | #{FZF} --preview-window left,border-none --preview 'echo hello' --bind '" \
"a:change-preview-window(right|down|up|hidden|)'", :Enter "a:change-preview-window(right|down|up|hidden|)'", :Enter
@@ -2970,6 +2977,13 @@ class TestGoFZF < TestBase
tmux.until { assert_match(%r{[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] 100/100}, _1[-1]) } tmux.until { assert_match(%r{[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] 100/100}, _1[-1]) }
end end
def test_info_inline_right_clearance
tmux.send_keys "seq 100000 | #{FZF} --info inline-right", :Enter
tmux.until { assert_match(%r{100000/100000}, _1[-1]) }
tmux.send_keys 'x'
tmux.until { assert_match(%r{ 0/100000}, _1[-1]) }
end
def test_prev_next_selected def test_prev_next_selected
tmux.send_keys 'seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected', :Enter 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.until { |lines| assert_equal 10, lines.item_count }