mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-12 13:23:48 -05:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2024010119 | ||
|
|
412040f77e | ||
|
|
d210660ce8 | ||
|
|
863a12562b | ||
|
|
5da606a9ac | ||
|
|
8d20f3d5c4 | ||
|
|
5d360180af | ||
|
|
f0fbed6007 | ||
|
|
519de7c833 | ||
|
|
97ccef1a04 | ||
|
|
c4df0dd06e | ||
|
|
cd114c6818 | ||
|
|
1707b8cdba | ||
|
|
41d4d70b98 | ||
|
|
0e999482cb | ||
|
|
65b2c06027 | ||
|
|
d7b61ede07 | ||
|
|
87fc1c84b8 | ||
|
|
d4b5f12383 | ||
|
|
eb62b0d665 | ||
|
|
91387a741b | ||
|
|
e8b34cb00d | ||
|
|
82954258c1 | ||
|
|
50f092551b | ||
|
|
c36a64be68 | ||
|
|
a343b20775 | ||
|
|
a714e76ae1 | ||
|
|
d21d5c9510 | ||
|
|
cd6788a2bb | ||
|
|
6b99399c41 | ||
|
|
952b6af445 | ||
|
|
7c674ad7fa | ||
|
|
d7d2ac3951 | ||
|
|
29e67d307a | ||
|
|
7320b7df62 | ||
|
|
11fb4233f7 | ||
|
|
84bb350b14 | ||
|
|
38e3694d1c | ||
|
|
1084935241 | ||
|
|
f5f0b9ecaa | ||
|
|
230fc49ae2 | ||
|
|
250d507bdf | ||
|
|
a818653174 | ||
|
|
5c3b044740 | ||
|
|
c5aa8729a1 | ||
|
|
3f78d76da1 | ||
|
|
70c19ccf16 | ||
|
|
68db9cb499 | ||
|
|
d0466fa777 | ||
|
|
21ab64e962 | ||
|
|
a0145cebf2 | ||
|
|
69176fc5f4 | ||
|
|
278dce9ba6 | ||
|
|
1cfa3ee4c7 | ||
|
|
9a95cd5794 | ||
|
|
a62fe3df6f | ||
|
|
7701244a08 | ||
|
|
96e31e4b78 | ||
|
|
ec208af474 | ||
|
|
242641264d | ||
|
|
d3a9a0615b | ||
|
|
3277e8c89c | ||
|
|
d02b9442a5 | ||
|
|
bac385b59c | ||
|
|
b1a0ab8086 | ||
|
|
a33749eb71 | ||
|
|
f5e4ee90e4 | ||
|
|
690d5e6dbd | ||
|
|
a76c055b63 | ||
|
|
70c461c60b | ||
|
|
d51b71ee80 | ||
|
|
3666448ca6 |
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -33,12 +33,12 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.19
|
||||
|
||||
|
||||
2
.github/workflows/macos.yml
vendored
2
.github/workflows/macos.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.18
|
||||
|
||||
|
||||
29
ADVANCED.md
29
ADVANCED.md
@@ -1,8 +1,8 @@
|
||||
Advanced fzf examples
|
||||
======================
|
||||
|
||||
* *Last update: 2023/05/26*
|
||||
* *Requires fzf 0.41.0 or above*
|
||||
* *Last update: 2023/12/29*
|
||||
* *Requires fzf 0.45.0 or above*
|
||||
|
||||
---
|
||||
|
||||
@@ -16,6 +16,7 @@ Advanced fzf examples
|
||||
* [Dynamic reloading of the list](#dynamic-reloading-of-the-list)
|
||||
* [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r)
|
||||
* [Toggling between data sources](#toggling-between-data-sources)
|
||||
* [Toggling with a single key binding](#toggling-with-a-single-key-binding)
|
||||
* [Ripgrep integration](#ripgrep-integration)
|
||||
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
|
||||
* [Using fzf as interactive Ripgrep launcher](#using-fzf-as-interactive-ripgrep-launcher)
|
||||
@@ -208,6 +209,30 @@ find * | fzf --prompt 'All> ' \
|
||||
|
||||

|
||||
|
||||
### Toggling with a single key binding
|
||||
|
||||
The above example uses two different key bindings to toggle between two modes,
|
||||
but can we just use a single key binding?
|
||||
|
||||
To make a key binding behave differently each time it is pressed, we need:
|
||||
|
||||
1. a way to store the current state. i.e. "which mode are we in?"
|
||||
2. and a way to dynamically perform different actions depending on the state.
|
||||
|
||||
The following example shows how to 1. store the current mode in the prompt
|
||||
string, 2. and use this information (`{fzf:prompt}`) to determine which
|
||||
actions to perform using the `transform` action.
|
||||
|
||||
```sh
|
||||
fd --type file |
|
||||
fzf --prompt 'Files> ' \
|
||||
--header 'CTRL-T: Switch between Files/Directories' \
|
||||
--bind 'ctrl-t:transform:[[ ! {fzf:prompt} =~ Files ]] &&
|
||||
echo "change-prompt(Files> )+reload(fd --type file)" ||
|
||||
echo "change-prompt(Directories> )+reload(fd --type directory)"' \
|
||||
--preview '[[ {fzf:prompt} =~ Files ]] && bat --color=always {} || tree -C {}'
|
||||
```
|
||||
|
||||
Ripgrep integration
|
||||
-------------------
|
||||
|
||||
|
||||
2
BUILD.md
2
BUILD.md
@@ -6,7 +6,7 @@ Build instructions
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.17 or above
|
||||
- Go 1.18 or above
|
||||
|
||||
### Using Makefile
|
||||
|
||||
|
||||
82
CHANGELOG.md
82
CHANGELOG.md
@@ -1,9 +1,91 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.45.0
|
||||
------
|
||||
- Added `transform` action to conditionally perform a series of actions
|
||||
```sh
|
||||
# Disallow selecting an empty line
|
||||
echo -e "1. Hello\n2. Goodbye\n\n3. Exit" |
|
||||
fzf --height '~100%' --reverse --header 'Select one' \
|
||||
--bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"'
|
||||
|
||||
# Move cursor past the empty line
|
||||
echo -e "1. Hello\n2. Goodbye\n\n3. Exit" |
|
||||
fzf --height '~100%' --reverse --header 'Select one' \
|
||||
--bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"' \
|
||||
--bind 'focus:transform:[[ -n {} ]] && exit; [[ {fzf:action} =~ up$ ]] && echo up || echo down'
|
||||
|
||||
# A single key binding to toggle between modes
|
||||
fd --type file |
|
||||
fzf --prompt 'Files> ' \
|
||||
--header 'CTRL-T: Switch between Files/Directories' \
|
||||
--bind 'ctrl-t:transform:[[ ! {fzf:prompt} =~ Files ]] &&
|
||||
echo "change-prompt(Files> )+reload(fd --type file)" ||
|
||||
echo "change-prompt(Directories> )+reload(fd --type directory)"'
|
||||
```
|
||||
- Added placeholder expressions
|
||||
- `{fzf:action}` - The name of the last action performed
|
||||
- `{fzf:prompt}` - Prompt string (including ANSI color codes)
|
||||
- `{fzf:query}` - Synonym for `{q}`
|
||||
- Added support for negative height
|
||||
```sh
|
||||
# Terminal height minus 1, so you can still see the command line
|
||||
fzf --height=-1
|
||||
```
|
||||
- This handles a terminal resize better than `--height=$(($(tput lines) - 1))`
|
||||
- Added `accept-or-print-query` action that acts like `accept` but prints the
|
||||
current query when there's no match for the query
|
||||
```sh
|
||||
# You can make CTRL-R paste the current query when there's no match
|
||||
export FZF_CTRL_R_OPTS='--bind enter:accept-or-print-query'
|
||||
```
|
||||
- Note that there are alternative ways to implement the same strategy
|
||||
```sh
|
||||
# 'become' is apparently more versatile but it's not available on Windows.
|
||||
export FZF_CTRL_R_OPTS='--bind "enter:become:if [ -z {} ]; then echo {q}; else echo {}; fi"'
|
||||
|
||||
# Using the new 'transform' action
|
||||
export FZF_CTRL_R_OPTS='--bind "enter:transform:[ -z {} ] && echo print-query || echo accept"'
|
||||
```
|
||||
- Added `show-header` and `hide-header` actions
|
||||
- Bug fixes
|
||||
|
||||
0.44.1
|
||||
------
|
||||
- Fixed crash when preview window is hidden on `focus` event
|
||||
|
||||
0.44.0
|
||||
------
|
||||
- (Experimental) Sixel image support in preview window (not available on Windows)
|
||||
- [bin/fzf-preview.sh](bin/fzf-preview.sh) is added to demonstrate how to
|
||||
display an image using Kitty image protocol or Sixel. You can use it
|
||||
like so:
|
||||
```sh
|
||||
fzf --preview='fzf-preview.sh {}'
|
||||
```
|
||||
- (Experimental) iTerm2 inline image protocol support in preview window (not available on Windows)
|
||||
```sh
|
||||
# Using https://iterm2.com/utilities/imgcat
|
||||
fzf --preview 'imgcat -W $FZF_PREVIEW_COLUMNS -H $FZF_PREVIEW_LINES {}'
|
||||
```
|
||||
- HTTP server can be configured to accept remote connections
|
||||
```sh
|
||||
# FZF_API_KEY is required for a non-localhost listen address
|
||||
export FZF_API_KEY="$(head -c 32 /dev/urandom | base64)"
|
||||
fzf --listen 0.0.0.0:6266
|
||||
```
|
||||
- To allow remote process execution, use `--listen-unsafe` instead
|
||||
(`execute*`, `reload*`, `become`, `preview`, `change-preview`, `transform-*`)
|
||||
```sh
|
||||
fzf --listen-unsafe 0.0.0.0:6266
|
||||
```
|
||||
- Bug fixes
|
||||
|
||||
0.43.0
|
||||
------
|
||||
- (Experimental) Added support for Kitty image protocol in the preview window
|
||||
(not available on Windows)
|
||||
```sh
|
||||
fzf --preview='
|
||||
if file --mime-type {} | grep -qF image/; then
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2023 Junegunn Choi
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
9
Makefile
9
Makefile
@@ -89,10 +89,17 @@ bench:
|
||||
|
||||
install: bin/fzf
|
||||
|
||||
generate:
|
||||
PATH=$(PATH):$(GOPATH)/bin $(GO) generate ./...
|
||||
|
||||
build:
|
||||
goreleaser build --rm-dist --snapshot --skip-post-hooks
|
||||
|
||||
release:
|
||||
# Make sure that the tests pass and the build works
|
||||
TAGS=tcell make test
|
||||
make test build clean
|
||||
|
||||
ifndef GITHUB_TOKEN
|
||||
$(error GITHUB_TOKEN is not defined)
|
||||
endif
|
||||
@@ -177,4 +184,4 @@ update:
|
||||
$(GO) get -u
|
||||
$(GO) mod tidy
|
||||
|
||||
.PHONY: all build release test bench install clean docker docker-test update
|
||||
.PHONY: all generate build release test bench install clean docker docker-test update
|
||||
|
||||
@@ -489,4 +489,4 @@ autocmd FileType fzf set laststatus=0 noshowmode noruler
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2023 Junegunn Choi
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
|
||||
74
bin/fzf-preview.sh
Executable file
74
bin/fzf-preview.sh
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# The purpose of this script is to demonstrate how to preview a file or an
|
||||
# image in the preview window of fzf.
|
||||
#
|
||||
# Dependencies:
|
||||
# - https://github.com/sharkdp/bat
|
||||
# - https://github.com/hpjansson/chafa
|
||||
# - https://iterm2.com/utilities/imgcat
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
>&2 echo "usage: $0 FILENAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
file=${1/#\~\//$HOME/}
|
||||
type=$(file --dereference --mime -- "$file")
|
||||
|
||||
if [[ ! $type =~ image/ ]]; then
|
||||
if [[ $type =~ =binary ]]; then
|
||||
file "$1"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Sometimes bat is installed as batcat.
|
||||
if command -v batcat > /dev/null; then
|
||||
batname="batcat"
|
||||
elif command -v bat > /dev/null; then
|
||||
batname="bat"
|
||||
else
|
||||
cat "$1"
|
||||
exit
|
||||
fi
|
||||
|
||||
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never -- "$file"
|
||||
exit
|
||||
fi
|
||||
|
||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||
if [[ $dim = x ]]; then
|
||||
dim=$(stty size < /dev/tty | awk '{print $2 "x" $1}')
|
||||
elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}') )); then
|
||||
# Avoid scrolling issue when the Sixel image touches the bottom of the screen
|
||||
# * https://github.com/junegunn/fzf/issues/2544
|
||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||
fi
|
||||
|
||||
# 1. Use kitty icat on kitty terminal
|
||||
if [[ $KITTY_WINDOW_ID ]]; then
|
||||
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
||||
# you have to use 'stream'.
|
||||
#
|
||||
# 2. The last line of the output is the ANSI reset code without newline.
|
||||
# This confuses fzf and makes it render scroll offset indicator.
|
||||
# So we remove the last line and append the reset code to its previous line.
|
||||
kitty icat --clear --transfer-mode=memory --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||
|
||||
# 2. Use chafa with Sixel output
|
||||
elif command -v chafa > /dev/null; then
|
||||
chafa -f sixel -s "$dim" "$file"
|
||||
# Add a new line character so that fzf can display multiple images in the preview window
|
||||
echo
|
||||
|
||||
# 3. If chafa is not found but imgcat is available, use it on iTerm2
|
||||
elif command -v imgcat > /dev/null; then
|
||||
# NOTE: We should use https://iterm2.com/utilities/it2check to check if the
|
||||
# user is running iTerm2. But for the sake of simplicity, we just assume
|
||||
# that's the case here.
|
||||
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
|
||||
|
||||
# 4. Cannot find any suitable method to preview the image
|
||||
else
|
||||
file "$file"
|
||||
fi
|
||||
@@ -1,4 +1,4 @@
|
||||
fzf.txt fzf Last change: September 17 2023
|
||||
fzf.txt fzf Last change: January 1 2024
|
||||
FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
|
||||
==============================================================================
|
||||
|
||||
@@ -512,7 +512,7 @@ LICENSE *fzf-license*
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2023 Junegunn Choi
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
|
||||
==============================================================================
|
||||
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:
|
||||
|
||||
8
go.mod
8
go.mod
@@ -7,15 +7,17 @@ require (
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/rivo/uniseg v0.4.4
|
||||
github.com/saracen/walker v0.1.3
|
||||
golang.org/x/sys v0.13.0
|
||||
golang.org/x/term v0.13.0
|
||||
golang.org/x/sys v0.15.0
|
||||
golang.org/x/term v0.15.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
golang.org/x/tools v0.16.1 // indirect
|
||||
)
|
||||
|
||||
go 1.17
|
||||
|
||||
14
go.sum
14
go.sum
@@ -19,6 +19,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
@@ -26,18 +28,20 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
@@ -46,4 +50,6 @@ golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
|
||||
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
2
install
2
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.43.0
|
||||
version=0.45.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$version="0.43.0"
|
||||
$version="0.45.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
||||
2
main.go
2
main.go
@@ -5,7 +5,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version string = "0.43"
|
||||
var version string = "0.45"
|
||||
var revision string = "devel"
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.ig
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2023 Junegunn Choi
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf-tmux 1 "Oct 2023" "fzf 0.43.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
.TH fzf-tmux 1 "Jan 2024" "fzf 0.45.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf-tmux - open fzf in tmux split pane
|
||||
|
||||
106
man/man1/fzf.1
106
man/man1/fzf.1
@@ -1,7 +1,7 @@
|
||||
.ig
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2023 Junegunn Choi
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf 1 "Oct 2023" "fzf 0.43.0" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "Jan 2024" "fzf 0.45.0" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -192,9 +192,21 @@ Label characters for \fBjump\fR and \fBjump-accept\fR
|
||||
.TP
|
||||
.BI "--height=" "[~]HEIGHT[%]"
|
||||
Display fzf window below the cursor with the given height instead of using
|
||||
the full screen. When prefixed with \fB~\fR, fzf will automatically determine
|
||||
the height in the range according to the input size. Note that adaptive height
|
||||
is not compatible with top/bottom margin and padding given in percent size.
|
||||
the full screen.
|
||||
|
||||
If a negative value is specified, the height is calculated as the terminal
|
||||
height minus the given value.
|
||||
|
||||
fzf --height=-1
|
||||
|
||||
When prefixed with \fB~\fR, fzf will automatically determine the height in the
|
||||
range according to the input size. Note that adaptive height is not compatible
|
||||
with top/bottom margin and padding given in percent size. It is also not
|
||||
compatible with a negative height value.
|
||||
|
||||
# Will not take up 100% of the screen
|
||||
seq 5 | fzf --height=~100%
|
||||
|
||||
.TP
|
||||
.BI "--min-height=" "HEIGHT"
|
||||
Minimum height when \fB--height\fR is given in percent (default: 10).
|
||||
@@ -548,6 +560,9 @@ they represent the exact size of the preview window. (It also overrides
|
||||
by the default shell, so prefer to refer to the ones with \fBFZF_PREVIEW_\fR
|
||||
prefix.)
|
||||
|
||||
fzf also exports \fB$FZF_PREVIEW_TOP\fR and \fB$FZF_PREVIEW_LEFT\fR so that
|
||||
the preview command can determine the position of the preview window.
|
||||
|
||||
A placeholder expression starting with \fB+\fR flag will be replaced to the
|
||||
space-separated list of the selected lines (or the current line if no selection
|
||||
was made) individually quoted.
|
||||
@@ -559,10 +574,6 @@ e.g.
|
||||
When using a field index expression, leading and trailing whitespace is stripped
|
||||
from the replacement string. To preserve the whitespace, use the \fBs\fR flag.
|
||||
|
||||
Also, \fB{q}\fR is replaced to the current query string, and \fB{n}\fR is
|
||||
replaced to zero-based ordinal index of the line. Use \fB{+n}\fR if you want
|
||||
all index numbers when multiple lines are selected.
|
||||
|
||||
A placeholder expression with \fBf\fR flag is replaced to the path of
|
||||
a temporary file that holds the evaluated list. This is useful when you
|
||||
multi-select a large number of items and the length of the evaluated string may
|
||||
@@ -574,6 +585,16 @@ e.g.
|
||||
seq 100000 | fzf --multi --bind ctrl-a:select-all \\
|
||||
--preview "awk '{sum+=\\$1} END {print sum}' {+f}"\fR
|
||||
|
||||
Also,
|
||||
|
||||
* \fB{q}\fR (or \fB{fzf:query}\fR) is replaced to the current query string
|
||||
.br
|
||||
* \fB{n}\fR is replaced to the zero-based ordinal index of the current item.
|
||||
Use \fB{+n}\fR if you want all index numbers when multiple lines are selected.
|
||||
.br
|
||||
* \fB{fzf:action}\fR is replaced to to the name of the last action performed
|
||||
* \fB{fzf:prompt}\fR is replaced to to the prompt string
|
||||
|
||||
Note that you can escape a placeholder pattern by prepending a backslash.
|
||||
|
||||
Preview window will be updated even when there is no match for the current
|
||||
@@ -592,17 +613,13 @@ e.g.
|
||||
sleep 0.01
|
||||
done'\fR
|
||||
|
||||
Since 0.43.0, fzf has experimental support for Kitty graphics protocol,
|
||||
so if you use Kitty, you can make fzf display an image in the preview window.
|
||||
fzf has experimental support for Kitty graphics protocol and Sixel graphics.
|
||||
The following example uses https://github.com/junegunn/fzf/blob/master/bin/fzf-preview.sh
|
||||
script to render an image using either of the protocols inside the preview window.
|
||||
|
||||
e.g.
|
||||
\fBfzf --preview='
|
||||
if file --mime-type {} | grep -qF "image/"; then
|
||||
kitty icat --clear --transfer-mode=memory --stdin=no --place=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0 {} | sed \\$d
|
||||
else
|
||||
bat --color=always {}
|
||||
fi
|
||||
'\fR
|
||||
\fBfzf --preview='fzf-preview.sh {}'
|
||||
|
||||
.RE
|
||||
|
||||
.TP
|
||||
@@ -794,14 +811,19 @@ ncurses finder only after the input stream is complete.
|
||||
e.g. \fBfzf --multi | fzf --sync\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "--listen[=HTTP_PORT]"
|
||||
Start HTTP server on the given port. It allows external processes to send
|
||||
actions to perform via POST method. If the port number is omitted or given as
|
||||
0, fzf will choose the port automatically and export it as \fBFZF_PORT\fR
|
||||
environment variable to the child processes started via \fBexecute\fR and
|
||||
\fBexecute-silent\fR actions. If \fBFZF_API_KEY\fR environment variable is
|
||||
set, the server would require sending an API key with the same value in the
|
||||
\fBx-api-key\fR HTTP header.
|
||||
.B "--listen[=[ADDR:]PORT]" "--listen-unsafe[=[ADDR:]PORT]"
|
||||
Start HTTP server and listen on the given address. It allows external processes
|
||||
to send actions to perform via POST method.
|
||||
|
||||
- If the port number is omitted or given as 0, fzf will automatically choose
|
||||
a port and export it as \fBFZF_PORT\fR environment variable to the child processes
|
||||
|
||||
- If \fBFZF_API_KEY\fR environment variable is set, the server would require
|
||||
sending an API key with the same value in the \fBx-api-key\fR HTTP header
|
||||
|
||||
- \fBFZF_API_KEY\fR is required for a non-localhost listen address
|
||||
|
||||
- To allow remote process execution, use \fB--listen-unsafe\fR
|
||||
|
||||
e.g.
|
||||
\fB# Start HTTP server on port 6266
|
||||
@@ -813,8 +835,12 @@ e.g.
|
||||
# Send action to the server
|
||||
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
|
||||
|
||||
# Start HTTP server on port 6266 and send an authenticated action
|
||||
# Start HTTP server on port 6266 with remote connections allowed
|
||||
# * Listening on non-localhost address requires using an API key
|
||||
export FZF_API_KEY="$(head -c 32 /dev/urandom | base64)"
|
||||
fzf --listen 0.0.0.0:6266
|
||||
|
||||
# Send an authenticated action
|
||||
curl -XPOST localhost:6266 -H "x-api-key: $FZF_API_KEY" -d 'change-query(yo)'
|
||||
|
||||
# Choose port automatically and export it as $FZF_PORT to the child process
|
||||
@@ -1112,6 +1138,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
|
||||
\fBaccept\fR \fIenter double-click\fR
|
||||
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
|
||||
\fBaccept-or-print-query\fR (same as \fBaccept\fR except that it prints the query when there's no match)
|
||||
\fBbackward-char\fR \fIctrl-b left\fR
|
||||
\fBbackward-delete-char\fR \fIctrl-h bspace\fR
|
||||
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
|
||||
@@ -1156,6 +1183,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBpage-up\fR \fIpgup\fR
|
||||
\fBhalf-page-down\fR
|
||||
\fBhalf-page-up\fR
|
||||
\fBhide-header\fR
|
||||
\fBhide-preview\fR
|
||||
\fBoffset-down\fR (similar to CTRL-E of Vim)
|
||||
\fBoffset-up\fR (similar to CTRL-Y of Vim)
|
||||
@@ -1181,6 +1209,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBreplace-query\fR (replace query string with the current selection)
|
||||
\fBselect\fR
|
||||
\fBselect-all\fR (select all matches)
|
||||
\fBshow-header\fR
|
||||
\fBshow-preview\fR
|
||||
\fBtoggle\fR (\fIright-click\fR)
|
||||
\fBtoggle-all\fR (toggle all matches)
|
||||
@@ -1195,6 +1224,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBtoggle-track\fR
|
||||
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
|
||||
\fBtrack\fR (track the current item; automatically disabled if focus changes)
|
||||
\fBtransform(...)\fR (transform states using the output of an external command)
|
||||
\fBtransform-border-label(...)\fR (transform border label using an external command)
|
||||
\fBtransform-header(...)\fR (transform header using an external command)
|
||||
\fBtransform-preview-label(...)\fR (transform preview label using an external command)
|
||||
@@ -1307,6 +1337,28 @@ e.g.
|
||||
\fB# You can still filter and select entries from the initial list for 3 seconds
|
||||
seq 100 | fzf --bind 'load:reload-sync(sleep 3; seq 1000)+unbind(load)'\fR
|
||||
|
||||
.SS TRANSFORM ACTIONS
|
||||
|
||||
Actions with \fBtransform-\fR prefix are used to transform the states of fzf
|
||||
using the output of an external command. The output of these commands are
|
||||
expected to be a single line of text.
|
||||
|
||||
e.g.
|
||||
\fBfzf --bind 'focus:transform-header:file --brief {}'\fR
|
||||
|
||||
\fBtransform(...)\fR action runs an external command that should print a series
|
||||
of actions to be performed. The output should be in the same format as the
|
||||
payload of HTTP POST request to the \fB--listen\fR server.
|
||||
|
||||
e.g.
|
||||
\fB# Disallow selecting an empty line
|
||||
echo -e "1. Hello\\n2. Goodbye\\n\\n3. Exit" |
|
||||
fzf --height '~100%' --reverse --header 'Select one' \\
|
||||
--bind 'enter:transform:[[ -n {} ]] &&
|
||||
echo accept ||
|
||||
echo "change-header:Invalid selection"'
|
||||
\fR
|
||||
|
||||
.SS PREVIEW BINDING
|
||||
|
||||
With \fBpreview(...)\fR action, you can specify multiple different preview
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
" Copyright (c) 2013-2023 Junegunn Choi
|
||||
" Copyright (c) 2013-2024 Junegunn Choi
|
||||
"
|
||||
" MIT License
|
||||
"
|
||||
|
||||
@@ -423,8 +423,8 @@ if ! declare -F __fzf_list_hosts > /dev/null; then
|
||||
__fzf_list_hosts() {
|
||||
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | command awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \
|
||||
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts 2> /dev/null | command tr ',' '\n' | command tr -d '[' | command awk '{ print $1 " " $1 }') \
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0') |
|
||||
command awk '{if (length($2) > 0) {print $2}}' | command sort -u
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0' | command sed 's/#.*//') |
|
||||
command awk '{for (i = 2; i <= NF; i++) print $i}' | command sort -u
|
||||
}
|
||||
fi
|
||||
|
||||
@@ -481,7 +481,7 @@ a_cmds="
|
||||
svn tar unzip zip"
|
||||
|
||||
# Preserve existing completion
|
||||
__fzf_orig_completion < <(complete -p $d_cmds $a_cmds 2> /dev/null)
|
||||
__fzf_orig_completion < <(complete -p $d_cmds $a_cmds ssh 2> /dev/null)
|
||||
|
||||
if type _completion_loader > /dev/null 2>&1; then
|
||||
_fzf_completion_loader=1
|
||||
|
||||
@@ -226,8 +226,8 @@ if ! declare -f __fzf_list_hosts > /dev/null; then
|
||||
setopt localoptions nonomatch
|
||||
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \
|
||||
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts 2> /dev/null | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0') |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0' | command sed 's/#.*//') |
|
||||
awk '{for (i = 2; i <= NF; i++) print $i}' | sort -u
|
||||
}
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2023 Junegunn Choi
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
129
src/actiontype_string.go
Normal file
129
src/actiontype_string.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// Code generated by "stringer -type=actionType"; DO NOT EDIT.
|
||||
|
||||
package fzf
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[actIgnore-0]
|
||||
_ = x[actStart-1]
|
||||
_ = x[actClick-2]
|
||||
_ = x[actInvalid-3]
|
||||
_ = x[actChar-4]
|
||||
_ = x[actMouse-5]
|
||||
_ = x[actBeginningOfLine-6]
|
||||
_ = x[actAbort-7]
|
||||
_ = x[actAccept-8]
|
||||
_ = x[actAcceptNonEmpty-9]
|
||||
_ = x[actAcceptOrPrintQuery-10]
|
||||
_ = x[actBackwardChar-11]
|
||||
_ = x[actBackwardDeleteChar-12]
|
||||
_ = x[actBackwardDeleteCharEof-13]
|
||||
_ = x[actBackwardWord-14]
|
||||
_ = x[actCancel-15]
|
||||
_ = x[actChangeBorderLabel-16]
|
||||
_ = x[actChangeHeader-17]
|
||||
_ = x[actChangePreviewLabel-18]
|
||||
_ = x[actChangePrompt-19]
|
||||
_ = x[actChangeQuery-20]
|
||||
_ = x[actClearScreen-21]
|
||||
_ = x[actClearQuery-22]
|
||||
_ = x[actClearSelection-23]
|
||||
_ = x[actClose-24]
|
||||
_ = x[actDeleteChar-25]
|
||||
_ = x[actDeleteCharEof-26]
|
||||
_ = x[actEndOfLine-27]
|
||||
_ = x[actForwardChar-28]
|
||||
_ = x[actForwardWord-29]
|
||||
_ = x[actKillLine-30]
|
||||
_ = x[actKillWord-31]
|
||||
_ = x[actUnixLineDiscard-32]
|
||||
_ = x[actUnixWordRubout-33]
|
||||
_ = x[actYank-34]
|
||||
_ = x[actBackwardKillWord-35]
|
||||
_ = x[actSelectAll-36]
|
||||
_ = x[actDeselectAll-37]
|
||||
_ = x[actToggle-38]
|
||||
_ = x[actToggleSearch-39]
|
||||
_ = x[actToggleAll-40]
|
||||
_ = x[actToggleDown-41]
|
||||
_ = x[actToggleUp-42]
|
||||
_ = x[actToggleIn-43]
|
||||
_ = x[actToggleOut-44]
|
||||
_ = x[actToggleTrack-45]
|
||||
_ = x[actToggleHeader-46]
|
||||
_ = x[actTrack-47]
|
||||
_ = x[actDown-48]
|
||||
_ = x[actUp-49]
|
||||
_ = x[actPageUp-50]
|
||||
_ = x[actPageDown-51]
|
||||
_ = x[actPosition-52]
|
||||
_ = x[actHalfPageUp-53]
|
||||
_ = x[actHalfPageDown-54]
|
||||
_ = x[actOffsetUp-55]
|
||||
_ = x[actOffsetDown-56]
|
||||
_ = x[actJump-57]
|
||||
_ = x[actJumpAccept-58]
|
||||
_ = x[actPrintQuery-59]
|
||||
_ = x[actRefreshPreview-60]
|
||||
_ = x[actReplaceQuery-61]
|
||||
_ = x[actToggleSort-62]
|
||||
_ = x[actShowPreview-63]
|
||||
_ = x[actHidePreview-64]
|
||||
_ = x[actTogglePreview-65]
|
||||
_ = x[actTogglePreviewWrap-66]
|
||||
_ = x[actTransform-67]
|
||||
_ = x[actTransformBorderLabel-68]
|
||||
_ = x[actTransformHeader-69]
|
||||
_ = x[actTransformPreviewLabel-70]
|
||||
_ = x[actTransformPrompt-71]
|
||||
_ = x[actTransformQuery-72]
|
||||
_ = x[actPreview-73]
|
||||
_ = x[actChangePreview-74]
|
||||
_ = x[actChangePreviewWindow-75]
|
||||
_ = x[actPreviewTop-76]
|
||||
_ = x[actPreviewBottom-77]
|
||||
_ = x[actPreviewUp-78]
|
||||
_ = x[actPreviewDown-79]
|
||||
_ = x[actPreviewPageUp-80]
|
||||
_ = x[actPreviewPageDown-81]
|
||||
_ = x[actPreviewHalfPageUp-82]
|
||||
_ = x[actPreviewHalfPageDown-83]
|
||||
_ = x[actPrevHistory-84]
|
||||
_ = x[actPrevSelected-85]
|
||||
_ = x[actPut-86]
|
||||
_ = x[actNextHistory-87]
|
||||
_ = x[actNextSelected-88]
|
||||
_ = x[actExecute-89]
|
||||
_ = x[actExecuteSilent-90]
|
||||
_ = x[actExecuteMulti-91]
|
||||
_ = x[actSigStop-92]
|
||||
_ = x[actFirst-93]
|
||||
_ = x[actLast-94]
|
||||
_ = x[actReload-95]
|
||||
_ = x[actReloadSync-96]
|
||||
_ = x[actDisableSearch-97]
|
||||
_ = x[actEnableSearch-98]
|
||||
_ = x[actSelect-99]
|
||||
_ = x[actDeselect-100]
|
||||
_ = x[actUnbind-101]
|
||||
_ = x[actRebind-102]
|
||||
_ = x[actBecome-103]
|
||||
_ = x[actResponse-104]
|
||||
_ = x[actShowHeader-105]
|
||||
_ = x[actHideHeader-106]
|
||||
}
|
||||
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleHeaderactTrackactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
|
||||
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 263, 278, 292, 306, 319, 336, 344, 357, 373, 385, 399, 413, 424, 435, 453, 470, 477, 496, 508, 522, 531, 546, 558, 571, 582, 593, 605, 619, 634, 642, 649, 654, 663, 674, 685, 698, 713, 724, 737, 744, 757, 770, 787, 802, 815, 829, 843, 859, 879, 891, 914, 932, 956, 974, 991, 1001, 1017, 1039, 1052, 1068, 1080, 1094, 1110, 1128, 1148, 1170, 1184, 1199, 1205, 1219, 1234, 1244, 1260, 1275, 1285, 1293, 1300, 1309, 1322, 1338, 1353, 1362, 1373, 1382, 1391, 1400, 1411, 1424, 1437}
|
||||
|
||||
func (i actionType) String() string {
|
||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||
return "actionType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _actionType_name[_actionType_index[i]:_actionType_index[i+1]]
|
||||
}
|
||||
@@ -351,9 +351,11 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
ptr := &state.fg
|
||||
|
||||
var delimiter byte = 0
|
||||
count := 0
|
||||
for len(ansiCode) != 0 {
|
||||
var num int
|
||||
if num, delimiter, ansiCode = parseAnsiCode(ansiCode, delimiter); num != -1 {
|
||||
count++
|
||||
switch state256 {
|
||||
case 0:
|
||||
switch num {
|
||||
@@ -435,6 +437,13 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
}
|
||||
}
|
||||
|
||||
// Empty sequence: reset
|
||||
if count == 0 {
|
||||
state.fg = -1
|
||||
state.bg = -1
|
||||
state.attr = 0
|
||||
}
|
||||
|
||||
if state256 > 0 {
|
||||
*ptr = -1
|
||||
}
|
||||
|
||||
@@ -348,6 +348,9 @@ func TestAnsiCodeStringConversion(t *testing.T) {
|
||||
}
|
||||
assert("\x1b[m", nil, "")
|
||||
assert("\x1b[m", &ansiState{attr: tui.Blink, lbg: -1}, "")
|
||||
assert("\x1b[0m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
|
||||
assert("\x1b[;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
|
||||
assert("\x1b[;;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
|
||||
|
||||
assert("\x1b[31m", nil, "\x1b[31;49m")
|
||||
assert("\x1b[41m", nil, "\x1b[39;41m")
|
||||
|
||||
@@ -200,7 +200,7 @@ func Run(opts *Options, version string, revision string) {
|
||||
padHeight := 0
|
||||
heightUnknown := opts.Height.auto
|
||||
if heightUnknown {
|
||||
maxFit, padHeight = terminal.MaxFitAndPad(opts)
|
||||
maxFit, padHeight = terminal.MaxFitAndPad()
|
||||
}
|
||||
deferred := opts.Select1 || opts.Exit0
|
||||
go terminal.Loop()
|
||||
|
||||
@@ -57,6 +57,8 @@ const usage = `usage: fzf [options]
|
||||
Layout
|
||||
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given
|
||||
height instead of using fullscreen.
|
||||
A negative value is calcalated as the terminal height
|
||||
minus the given value.
|
||||
If prefixed with '~', fzf will determine the height
|
||||
according to the input size.
|
||||
--min-height=HEIGHT Minimum height when --height is given in percent
|
||||
@@ -118,7 +120,8 @@ const usage = `usage: fzf [options]
|
||||
--read0 Read input delimited by ASCII NUL characters
|
||||
--print0 Print output delimited by ASCII NUL characters
|
||||
--sync Synchronous search for multi-staged filtering
|
||||
--listen[=HTTP_PORT] Start HTTP server to receive actions (POST /)
|
||||
--listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
|
||||
(To allow remote process execution, use --listen-unsafe)
|
||||
--version Display version information and exit
|
||||
|
||||
Environment variables
|
||||
@@ -156,6 +159,7 @@ type heightSpec struct {
|
||||
size float64
|
||||
percent bool
|
||||
auto bool
|
||||
inverse bool
|
||||
}
|
||||
|
||||
type sizeSpec struct {
|
||||
@@ -334,7 +338,8 @@ type Options struct {
|
||||
PreviewLabel labelOpts
|
||||
Unicode bool
|
||||
Tabstop int
|
||||
ListenPort *int
|
||||
ListenAddr *listenAddress
|
||||
Unsafe bool
|
||||
ClearOnExit bool
|
||||
Version bool
|
||||
}
|
||||
@@ -404,6 +409,7 @@ func defaultOptions() *Options {
|
||||
Tabstop: 8,
|
||||
BorderLabel: labelOpts{},
|
||||
PreviewLabel: labelOpts{},
|
||||
Unsafe: false,
|
||||
ClearOnExit: true,
|
||||
Version: false}
|
||||
}
|
||||
@@ -973,7 +979,7 @@ const (
|
||||
|
||||
func init() {
|
||||
executeRegexp = regexp.MustCompile(
|
||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
|
||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|transform|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
|
||||
splitRegexp = regexp.MustCompile("[,:]+")
|
||||
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
||||
}
|
||||
@@ -1067,6 +1073,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actAccept)
|
||||
case "accept-non-empty":
|
||||
appendAction(actAcceptNonEmpty)
|
||||
case "accept-or-print-query":
|
||||
appendAction(actAcceptOrPrintQuery)
|
||||
case "print-query":
|
||||
appendAction(actPrintQuery)
|
||||
case "refresh-preview":
|
||||
@@ -1078,7 +1086,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
case "backward-delete-char":
|
||||
appendAction(actBackwardDeleteChar)
|
||||
case "backward-delete-char/eof":
|
||||
appendAction(actBackwardDeleteCharEOF)
|
||||
appendAction(actBackwardDeleteCharEof)
|
||||
case "backward-word":
|
||||
appendAction(actBackwardWord)
|
||||
case "clear-screen":
|
||||
@@ -1086,7 +1094,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
case "delete-char":
|
||||
appendAction(actDeleteChar)
|
||||
case "delete-char/eof":
|
||||
appendAction(actDeleteCharEOF)
|
||||
appendAction(actDeleteCharEof)
|
||||
case "deselect":
|
||||
appendAction(actDeselect)
|
||||
case "end-of-line":
|
||||
@@ -1133,6 +1141,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actToggleTrack)
|
||||
case "toggle-header":
|
||||
appendAction(actToggleHeader)
|
||||
case "show-header":
|
||||
appendAction(actShowHeader)
|
||||
case "hide-header":
|
||||
appendAction(actHideHeader)
|
||||
case "track":
|
||||
appendAction(actTrack)
|
||||
case "select":
|
||||
@@ -1205,7 +1217,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actDisableSearch)
|
||||
case "put":
|
||||
if putAllowed {
|
||||
appendAction(actRune)
|
||||
appendAction(actChar)
|
||||
} else {
|
||||
exit("unable to put non-printable character")
|
||||
}
|
||||
@@ -1325,6 +1337,8 @@ func isExecuteAction(str string) actionType {
|
||||
return actExecuteMulti
|
||||
case "put":
|
||||
return actPut
|
||||
case "transform":
|
||||
return actTransform
|
||||
case "transform-border-label":
|
||||
return actTransformBorderLabel
|
||||
case "transform-preview-label":
|
||||
@@ -1381,6 +1395,13 @@ func parseHeight(str string) heightSpec {
|
||||
heightSpec.auto = true
|
||||
str = str[1:]
|
||||
}
|
||||
if strings.HasPrefix(str, "-") {
|
||||
if heightSpec.auto {
|
||||
errorExit("negative(-) height is not compatible with adaptive(~) height")
|
||||
}
|
||||
heightSpec.inverse = true
|
||||
str = str[1:]
|
||||
}
|
||||
|
||||
size := parseSize(str, 100, "height")
|
||||
heightSpec.size = size.size
|
||||
@@ -1832,11 +1853,21 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
|
||||
case "--tabstop":
|
||||
opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
|
||||
case "--listen":
|
||||
port := optionalNumeric(allArgs, &i, 0)
|
||||
opts.ListenPort = &port
|
||||
case "--no-listen":
|
||||
opts.ListenPort = nil
|
||||
case "--listen", "--listen-unsafe":
|
||||
given, str := optionalNextString(allArgs, &i)
|
||||
addr := defaultListenAddr
|
||||
if given {
|
||||
var err error
|
||||
err, addr = parseListenAddress(str)
|
||||
if err != nil {
|
||||
errorExit(err.Error())
|
||||
}
|
||||
}
|
||||
opts.ListenAddr = &addr
|
||||
opts.Unsafe = arg == "--listen-unsafe"
|
||||
case "--no-listen", "--no-listen-unsafe":
|
||||
opts.ListenAddr = nil
|
||||
opts.Unsafe = false
|
||||
case "--clear":
|
||||
opts.ClearOnExit = true
|
||||
case "--no-clear":
|
||||
@@ -1927,8 +1958,19 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
} else if match, value := optString(arg, "--tabstop="); match {
|
||||
opts.Tabstop = atoi(value)
|
||||
} else if match, value := optString(arg, "--listen="); match {
|
||||
port := atoi(value)
|
||||
opts.ListenPort = &port
|
||||
err, addr := parseListenAddress(value)
|
||||
if err != nil {
|
||||
errorExit(err.Error())
|
||||
}
|
||||
opts.ListenAddr = &addr
|
||||
opts.Unsafe = false
|
||||
} else if match, value := optString(arg, "--listen-unsafe="); match {
|
||||
err, addr := parseListenAddress(value)
|
||||
if err != nil {
|
||||
errorExit(err.Error())
|
||||
}
|
||||
opts.ListenAddr = &addr
|
||||
opts.Unsafe = true
|
||||
} else if match, value := optString(arg, "--hscroll-off="); match {
|
||||
opts.HscrollOff = atoi(value)
|
||||
} else if match, value := optString(arg, "--scroll-off="); match {
|
||||
@@ -1958,10 +2000,6 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
errorExit("tab stop must be a positive integer")
|
||||
}
|
||||
|
||||
if opts.ListenPort != nil && (*opts.ListenPort < 0 || *opts.ListenPort > 65535) {
|
||||
errorExit("invalid listen port")
|
||||
}
|
||||
|
||||
if len(opts.JumpLabels) == 0 {
|
||||
errorExit("empty jump labels")
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ import "golang.org/x/sys/unix"
|
||||
|
||||
// Protect calls OS specific protections like pledge on OpenBSD
|
||||
func Protect() {
|
||||
unix.PledgePromises("stdio rpath tty proc exec")
|
||||
unix.PledgePromises("stdio rpath tty proc exec inet tmppath")
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||
if criterion == byBegin {
|
||||
val = util.AsUint16(minEnd - whitePrefixLen)
|
||||
} else {
|
||||
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()))
|
||||
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()+1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,30 +40,63 @@ type httpServer struct {
|
||||
responseChannel chan string
|
||||
}
|
||||
|
||||
func startHttpServer(port int, actionChannel chan []*action, responseChannel chan string) (error, int) {
|
||||
if port < 0 {
|
||||
return nil, port
|
||||
}
|
||||
type listenAddress struct {
|
||||
host string
|
||||
port int
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
|
||||
func (addr listenAddress) IsLocal() bool {
|
||||
return addr.host == "localhost" || addr.host == "127.0.0.1"
|
||||
}
|
||||
|
||||
var defaultListenAddr = listenAddress{"localhost", 0}
|
||||
|
||||
func parseListenAddress(address string) (error, listenAddress) {
|
||||
parts := strings.SplitN(address, ":", 3)
|
||||
if len(parts) == 1 {
|
||||
parts = []string{"localhost", parts[0]}
|
||||
}
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid listen address: %s", address), defaultListenAddr
|
||||
}
|
||||
portStr := parts[len(parts)-1]
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil || port < 0 || port > 65535 {
|
||||
return fmt.Errorf("invalid listen port: %s", portStr), defaultListenAddr
|
||||
}
|
||||
if len(parts[0]) == 0 {
|
||||
parts[0] = "localhost"
|
||||
}
|
||||
return nil, listenAddress{parts[0], port}
|
||||
}
|
||||
|
||||
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (error, int) {
|
||||
host := address.host
|
||||
port := address.port
|
||||
apiKey := os.Getenv("FZF_API_KEY")
|
||||
if !address.IsLocal() && len(apiKey) == 0 {
|
||||
return fmt.Errorf("FZF_API_KEY is required to allow remote access"), port
|
||||
}
|
||||
addrStr := fmt.Sprintf("%s:%d", host, port)
|
||||
listener, err := net.Listen("tcp", addrStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("port not available: %d", port), port
|
||||
return fmt.Errorf("failed to listen on %s", addrStr), port
|
||||
}
|
||||
if port == 0 {
|
||||
addr := listener.Addr().String()
|
||||
parts := strings.SplitN(addr, ":", 2)
|
||||
parts := strings.Split(addr, ":")
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("cannot extract port: %s", addr), port
|
||||
}
|
||||
var err error
|
||||
port, err = strconv.Atoi(parts[1])
|
||||
port, err = strconv.Atoi(parts[len(parts)-1])
|
||||
if err != nil {
|
||||
return err, port
|
||||
}
|
||||
}
|
||||
|
||||
server := httpServer{
|
||||
apiKey: []byte(os.Getenv("FZF_API_KEY")),
|
||||
apiKey: []byte(apiKey),
|
||||
actionChannel: actionChannel,
|
||||
responseChannel: responseChannel,
|
||||
}
|
||||
@@ -185,7 +218,7 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
||||
}
|
||||
|
||||
server.actionChannel <- actions
|
||||
return httpOk
|
||||
return httpOk + crlf
|
||||
}
|
||||
|
||||
func parseGetParams(query string) getParams {
|
||||
|
||||
425
src/terminal.go
425
src/terminal.go
@@ -52,11 +52,12 @@ var offsetComponentRegex *regexp.Regexp
|
||||
var offsetTrimCharsRegex *regexp.Regexp
|
||||
var activeTempFiles []string
|
||||
var passThroughRegex *regexp.Regexp
|
||||
var actionTypeRegex *regexp.Regexp
|
||||
|
||||
const clearCode string = "\x1b[2J"
|
||||
|
||||
func init() {
|
||||
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`)
|
||||
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`)
|
||||
whiteSuffix = regexp.MustCompile(`\s*$`)
|
||||
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
|
||||
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
|
||||
@@ -65,7 +66,9 @@ func init() {
|
||||
// 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://sw.kovidgoyal.net/kitty/graphics-protocol
|
||||
passThroughRegex = regexp.MustCompile(`\x1bPtmux;\x1b\x1b.*?[^\x1b]\x1b\\|\x1b_G.*?\x1b\\`)
|
||||
// * https://en.wikipedia.org/wiki/Sixel
|
||||
// * https://iterm2.com/documentation-images.html
|
||||
passThroughRegex = regexp.MustCompile(`\x1bPtmux;\x1b\x1b.*?[^\x1b]\x1b\\|\x1b(_G|P[0-9;]*q).*?\x1b\\\r?|\x1b]1337;.*?\a`)
|
||||
}
|
||||
|
||||
type jumpMode int
|
||||
@@ -120,10 +123,13 @@ type previewer struct {
|
||||
}
|
||||
|
||||
type previewed struct {
|
||||
version int64
|
||||
numLines int
|
||||
offset int
|
||||
filled bool
|
||||
version int64
|
||||
numLines int
|
||||
offset int
|
||||
filled bool
|
||||
image bool
|
||||
wipe bool
|
||||
wireframe bool
|
||||
}
|
||||
|
||||
type eachLine struct {
|
||||
@@ -177,6 +183,7 @@ type Terminal struct {
|
||||
separator labelPrinter
|
||||
separatorLen int
|
||||
spinner []string
|
||||
promptString string
|
||||
prompt func()
|
||||
promptLen int
|
||||
borderLabel labelPrinter
|
||||
@@ -231,7 +238,9 @@ type Terminal struct {
|
||||
margin [4]sizeSpec
|
||||
padding [4]sizeSpec
|
||||
unicode bool
|
||||
listenAddr *listenAddress
|
||||
listenPort *int
|
||||
listenUnsafe bool
|
||||
borderShape tui.BorderShape
|
||||
cleanExit bool
|
||||
paused bool
|
||||
@@ -277,6 +286,9 @@ type Terminal struct {
|
||||
theme *tui.ColorTheme
|
||||
tui tui.Renderer
|
||||
executing *util.AtomicBool
|
||||
termSize tui.TermSize
|
||||
lastAction actionType
|
||||
lastFocus int32
|
||||
}
|
||||
|
||||
type selectedItem struct {
|
||||
@@ -307,6 +319,7 @@ const (
|
||||
reqRefresh
|
||||
reqReinit
|
||||
reqFullRedraw
|
||||
reqResize
|
||||
reqRedrawBorderLabel
|
||||
reqRedrawPreviewLabel
|
||||
reqClose
|
||||
@@ -323,20 +336,24 @@ type action struct {
|
||||
a string
|
||||
}
|
||||
|
||||
//go:generate stringer -type=actionType
|
||||
type actionType int
|
||||
|
||||
const (
|
||||
actIgnore actionType = iota
|
||||
actStart
|
||||
actClick
|
||||
actInvalid
|
||||
actRune
|
||||
actChar
|
||||
actMouse
|
||||
actBeginningOfLine
|
||||
actAbort
|
||||
actAccept
|
||||
actAcceptNonEmpty
|
||||
actAcceptOrPrintQuery
|
||||
actBackwardChar
|
||||
actBackwardDeleteChar
|
||||
actBackwardDeleteCharEOF
|
||||
actBackwardDeleteCharEof
|
||||
actBackwardWord
|
||||
actCancel
|
||||
actChangeBorderLabel
|
||||
@@ -349,7 +366,7 @@ const (
|
||||
actClearSelection
|
||||
actClose
|
||||
actDeleteChar
|
||||
actDeleteCharEOF
|
||||
actDeleteCharEof
|
||||
actEndOfLine
|
||||
actForwardChar
|
||||
actForwardWord
|
||||
@@ -390,6 +407,7 @@ const (
|
||||
actHidePreview
|
||||
actTogglePreview
|
||||
actTogglePreviewWrap
|
||||
actTransform
|
||||
actTransformBorderLabel
|
||||
actTransformHeader
|
||||
actTransformPreviewLabel
|
||||
@@ -427,13 +445,37 @@ const (
|
||||
actRebind
|
||||
actBecome
|
||||
actResponse
|
||||
actShowHeader
|
||||
actHideHeader
|
||||
)
|
||||
|
||||
func processExecution(action actionType) bool {
|
||||
switch action {
|
||||
case actTransform,
|
||||
actTransformBorderLabel,
|
||||
actTransformHeader,
|
||||
actTransformPreviewLabel,
|
||||
actTransformPrompt,
|
||||
actTransformQuery,
|
||||
actPreview,
|
||||
actChangePreview,
|
||||
actRefreshPreview,
|
||||
actExecute,
|
||||
actExecuteSilent,
|
||||
actExecuteMulti,
|
||||
actReload,
|
||||
actReloadSync,
|
||||
actBecome:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type placeholderFlags struct {
|
||||
plus bool
|
||||
preserveSpace bool
|
||||
number bool
|
||||
query bool
|
||||
forceUpdate bool
|
||||
file bool
|
||||
}
|
||||
|
||||
@@ -447,6 +489,7 @@ type searchRequest struct {
|
||||
type previewRequest struct {
|
||||
template string
|
||||
pwindow tui.Window
|
||||
pwindowSize tui.TermSize
|
||||
scrollOffset int
|
||||
list []*Item
|
||||
}
|
||||
@@ -483,7 +526,7 @@ func defaultKeymap() map[tui.Event][]*action {
|
||||
add(tui.CtrlG, actAbort)
|
||||
add(tui.CtrlQ, actAbort)
|
||||
add(tui.ESC, actAbort)
|
||||
add(tui.CtrlD, actDeleteCharEOF)
|
||||
add(tui.CtrlD, actDeleteCharEof)
|
||||
add(tui.CtrlE, actEndOfLine)
|
||||
add(tui.CtrlF, actForwardChar)
|
||||
add(tui.CtrlH, actBackwardDeleteChar)
|
||||
@@ -525,7 +568,7 @@ func defaultKeymap() map[tui.Event][]*action {
|
||||
add(tui.SDown, actPreviewDown)
|
||||
|
||||
add(tui.Mouse, actMouse)
|
||||
add(tui.LeftClick, actIgnore)
|
||||
add(tui.LeftClick, actClick)
|
||||
add(tui.RightClick, actToggle)
|
||||
add(tui.SLeftClick, actToggle)
|
||||
add(tui.SRightClick, actToggle)
|
||||
@@ -544,10 +587,14 @@ func trimQuery(query string) []rune {
|
||||
return []rune(strings.Replace(query, "\t", " ", -1))
|
||||
}
|
||||
|
||||
func hasPreviewAction(opts *Options) bool {
|
||||
func mayTriggerPreview(opts *Options) bool {
|
||||
if opts.ListenAddr != nil {
|
||||
return true
|
||||
}
|
||||
for _, actions := range opts.Keymap {
|
||||
for _, action := range actions {
|
||||
if action.t == actPreview || action.t == actChangePreview {
|
||||
switch action.t {
|
||||
case actPreview, actChangePreview, actTransform:
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -563,10 +610,17 @@ func makeSpinner(unicode bool) []string {
|
||||
}
|
||||
|
||||
func evaluateHeight(opts *Options, termHeight int) int {
|
||||
size := opts.Height.size
|
||||
if opts.Height.percent {
|
||||
return util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight)
|
||||
if opts.Height.inverse {
|
||||
size = 100 - size
|
||||
}
|
||||
return util.Max(int(size*float64(termHeight)/100.0), opts.MinHeight)
|
||||
}
|
||||
return int(opts.Height.size)
|
||||
if opts.Height.inverse {
|
||||
size = float64(termHeight) - size
|
||||
}
|
||||
return int(size)
|
||||
}
|
||||
|
||||
// NewTerminal returns new Terminal object
|
||||
@@ -579,8 +633,11 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
delay = initialDelay
|
||||
}
|
||||
var previewBox *util.EventBox
|
||||
// We need to start previewer if HTTP server is enabled even when --preview option is not specified
|
||||
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) || opts.ListenPort != nil {
|
||||
// We need to start the previewer even when --preview option is not specified
|
||||
// * if HTTP server is enabled
|
||||
// * if 'preview' or 'change-preview' action is bound to a key
|
||||
// * if 'transform' action is bound to a key
|
||||
if len(opts.Preview.command) > 0 || mayTriggerPreview(opts) {
|
||||
previewBox = util.NewEventBox()
|
||||
}
|
||||
var renderer tui.Renderer
|
||||
@@ -624,6 +681,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
infoSep: opts.InfoSep,
|
||||
separator: nil,
|
||||
spinner: makeSpinner(opts.Unicode),
|
||||
promptString: opts.Prompt,
|
||||
queryLen: [2]int{0, 0},
|
||||
layout: opts.Layout,
|
||||
fullscreen: fullscreen,
|
||||
@@ -653,7 +711,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
margin: opts.Margin,
|
||||
padding: opts.Padding,
|
||||
unicode: opts.Unicode,
|
||||
listenPort: opts.ListenPort,
|
||||
listenAddr: opts.ListenAddr,
|
||||
listenUnsafe: opts.Unsafe,
|
||||
borderShape: opts.BorderShape,
|
||||
borderWidth: 1,
|
||||
borderLabel: nil,
|
||||
@@ -686,7 +745,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
initialPreviewOpts: opts.Preview,
|
||||
previewOpts: opts.Preview,
|
||||
previewer: previewer{0, []string{}, 0, false, true, disabledState, "", []bool{}},
|
||||
previewed: previewed{0, 0, 0, false},
|
||||
previewed: previewed{0, 0, 0, false, false, false, false},
|
||||
previewBox: previewBox,
|
||||
eventBox: eventBox,
|
||||
mutex: sync.Mutex{},
|
||||
@@ -698,10 +757,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
killChan: make(chan int),
|
||||
serverInputChan: make(chan []*action, 10),
|
||||
serverOutputChan: make(chan string),
|
||||
eventChan: make(chan tui.Event, 1),
|
||||
eventChan: make(chan tui.Event, 3), // load / zero|one | GetChar
|
||||
tui: renderer,
|
||||
initFunc: func() { renderer.Init() },
|
||||
executing: util.NewAtomicBool(false)}
|
||||
executing: util.NewAtomicBool(false),
|
||||
lastAction: actStart,
|
||||
lastFocus: minItem.Index()}
|
||||
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
|
||||
t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
|
||||
t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
|
||||
@@ -742,8 +803,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
|
||||
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
|
||||
|
||||
if t.listenPort != nil {
|
||||
err, port := startHttpServer(*t.listenPort, t.serverInputChan, t.serverOutputChan)
|
||||
if t.listenAddr != nil {
|
||||
err, port := startHttpServer(*t.listenAddr, t.serverInputChan, t.serverOutputChan)
|
||||
if err != nil {
|
||||
errorExit(err.Error())
|
||||
}
|
||||
@@ -787,7 +848,7 @@ func (t *Terminal) extraLines() int {
|
||||
return extra
|
||||
}
|
||||
|
||||
func (t *Terminal) MaxFitAndPad(opts *Options) (int, int) {
|
||||
func (t *Terminal) MaxFitAndPad() (int, int) {
|
||||
_, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding()
|
||||
padHeight := marginInt[0] + marginInt[2] + paddingInt[0] + paddingInt[2]
|
||||
fit := screenHeight - padHeight - t.extraLines()
|
||||
@@ -1301,10 +1362,6 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
|
||||
if previewOpts.hidden {
|
||||
return
|
||||
}
|
||||
// Put scrollbar closer to the right border for consistent look
|
||||
if t.borderShape.HasRight() {
|
||||
width++
|
||||
}
|
||||
if previewOpts.position == posUp {
|
||||
t.window = t.tui.NewWindow(
|
||||
marginInt[0]+pheight, marginInt[3], width, height-pheight, false, noBorder)
|
||||
@@ -1933,11 +1990,21 @@ func (t *Terminal) renderPreviewSpinner() {
|
||||
}
|
||||
|
||||
func (t *Terminal) renderPreviewArea(unchanged bool) {
|
||||
if unchanged {
|
||||
if t.previewed.wipe && t.previewed.version != t.previewer.version {
|
||||
t.previewed.wipe = false
|
||||
t.pwindow.Erase()
|
||||
} else if unchanged {
|
||||
t.pwindow.MoveAndClear(0, 0) // Clear scroll offset display
|
||||
} else {
|
||||
t.previewed.filled = false
|
||||
t.pwindow.Erase()
|
||||
// We don't erase the window here to avoid flickering during scroll.
|
||||
// However, tcell renderer uses double-buffering technique and there's no
|
||||
// flickering. So we just erase the window and make the rest of the code
|
||||
// simpler.
|
||||
if !t.pwindow.EraseMaybe() {
|
||||
t.pwindow.DrawBorder()
|
||||
t.pwindow.Move(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
height := t.pwindow.Height()
|
||||
@@ -1967,10 +2034,32 @@ func (t *Terminal) renderPreviewArea(unchanged bool) {
|
||||
t.renderPreviewScrollbar(headerLines, barLength, barStart)
|
||||
}
|
||||
|
||||
func (t *Terminal) makeImageBorder(width int, top bool) string {
|
||||
tl := "┌"
|
||||
tr := "┐"
|
||||
v := "╎"
|
||||
h := "╌"
|
||||
if !t.unicode {
|
||||
tl = "+"
|
||||
tr = "+"
|
||||
h = "-"
|
||||
v = "|"
|
||||
}
|
||||
repeat := util.Max(0, width-2)
|
||||
if top {
|
||||
return tl + strings.Repeat(h, repeat) + tr
|
||||
}
|
||||
return v + strings.Repeat(" ", repeat) + v
|
||||
}
|
||||
|
||||
func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unchanged bool) {
|
||||
maxWidth := t.pwindow.Width()
|
||||
var ansi *ansiState
|
||||
spinnerRedraw := t.pwindow.Y() == 0
|
||||
wiped := false
|
||||
image := false
|
||||
wireframe := false
|
||||
Loop:
|
||||
for _, line := range lines {
|
||||
var lbg tui.Color = -1
|
||||
if ansi != nil {
|
||||
@@ -1988,16 +2077,76 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
|
||||
t.previewer.scrollable = true
|
||||
break
|
||||
} else if lineNo >= 0 {
|
||||
x := t.pwindow.X()
|
||||
y := t.pwindow.Y()
|
||||
if spinnerRedraw && lineNo > 0 {
|
||||
spinnerRedraw = false
|
||||
y := t.pwindow.Y()
|
||||
x := t.pwindow.X()
|
||||
t.renderPreviewSpinner()
|
||||
t.pwindow.Move(y, x)
|
||||
}
|
||||
for _, passThrough := range passThroughs {
|
||||
for idx, passThrough := range passThroughs {
|
||||
// Handling Sixel/iTerm image
|
||||
requiredLines := 0
|
||||
isSixel := strings.HasPrefix(passThrough, "\x1bP")
|
||||
isItermImage := strings.HasPrefix(passThrough, "\x1b]1337;")
|
||||
isImage := isSixel || isItermImage
|
||||
if isImage {
|
||||
t.previewed.wipe = true
|
||||
// NOTE: We don't have a good way to get the height of an iTerm image,
|
||||
// so we assume that it requires the full height of the preview
|
||||
// window.
|
||||
requiredLines = height
|
||||
|
||||
if isSixel && t.termSize.PxHeight > 0 {
|
||||
rows := strings.Count(passThrough, "-")
|
||||
requiredLines = int(math.Ceil(float64(rows*6*t.termSize.Lines) / float64(t.termSize.PxHeight)))
|
||||
}
|
||||
}
|
||||
|
||||
// Render wireframe when the image cannot be displayed entirely
|
||||
if requiredLines > 0 && y+requiredLines > height {
|
||||
top := true
|
||||
for ; y < height; y++ {
|
||||
t.pwindow.MoveAndClear(y, 0)
|
||||
t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, t.makeImageBorder(maxWidth, top))
|
||||
top = false
|
||||
}
|
||||
wireframe = true
|
||||
t.previewed.filled = true
|
||||
t.previewer.scrollable = true
|
||||
break Loop
|
||||
}
|
||||
|
||||
// Clear previous wireframe or any other text
|
||||
if (t.previewed.wireframe || isImage && !t.previewed.image) && !wiped {
|
||||
wiped = true
|
||||
for i := y + 1; i < height; i++ {
|
||||
t.pwindow.MoveAndClear(i, 0)
|
||||
}
|
||||
}
|
||||
image = image || isImage
|
||||
if idx == 0 {
|
||||
t.pwindow.MoveAndClear(y, x)
|
||||
} else {
|
||||
t.pwindow.Move(y, x)
|
||||
}
|
||||
t.tui.PassThrough(passThrough)
|
||||
|
||||
if requiredLines > 0 {
|
||||
if y+requiredLines == height {
|
||||
t.pwindow.Move(height-1, maxWidth-1)
|
||||
t.previewed.filled = true
|
||||
break Loop
|
||||
} else {
|
||||
t.pwindow.MoveAndClear(y+requiredLines, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(passThroughs) > 0 && len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var fillRet tui.FillReturn
|
||||
prefixWidth := 0
|
||||
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
|
||||
@@ -2038,6 +2187,8 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
|
||||
}
|
||||
lineNo++
|
||||
}
|
||||
t.previewed.image = image
|
||||
t.previewed.wireframe = wireframe
|
||||
}
|
||||
|
||||
func (t *Terminal) renderPreviewScrollbar(yoff int, barLength int, barStart int) {
|
||||
@@ -2085,7 +2236,7 @@ func (t *Terminal) printPreview() {
|
||||
unchanged := (t.previewed.filled || numLines == t.previewed.numLines) &&
|
||||
t.previewer.version == t.previewed.version &&
|
||||
t.previewer.offset == t.previewed.offset
|
||||
t.previewer.scrollable = t.previewer.offset > 0 || numLines > height
|
||||
t.previewer.scrollable = t.previewer.offset > t.previewOpts.headerLines || numLines > height
|
||||
t.renderPreviewArea(unchanged)
|
||||
t.renderPreviewSpinner()
|
||||
t.previewed.numLines = numLines
|
||||
@@ -2215,6 +2366,12 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
|
||||
return true, match[1:], flags
|
||||
}
|
||||
|
||||
if strings.HasPrefix(match, "{fzf:") {
|
||||
// {fzf:*} are not determined by the current item
|
||||
flags.forceUpdate = true
|
||||
return false, match, flags
|
||||
}
|
||||
|
||||
skipChars := 1
|
||||
for _, char := range match[1:] {
|
||||
switch char {
|
||||
@@ -2231,7 +2388,7 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
|
||||
flags.file = true
|
||||
skipChars++
|
||||
case 'q':
|
||||
flags.query = true
|
||||
flags.forceUpdate = true
|
||||
// query flag is not skipped
|
||||
default:
|
||||
break
|
||||
@@ -2243,14 +2400,14 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
|
||||
return false, matchWithoutFlags, flags
|
||||
}
|
||||
|
||||
func hasPreviewFlags(template string) (slot bool, plus bool, query bool) {
|
||||
func hasPreviewFlags(template string) (slot bool, plus bool, forceUpdate bool) {
|
||||
for _, match := range placeholder.FindAllString(template, -1) {
|
||||
_, _, flags := parsePlaceholder(match)
|
||||
if flags.plus {
|
||||
plus = true
|
||||
}
|
||||
if flags.query {
|
||||
query = true
|
||||
if flags.forceUpdate {
|
||||
forceUpdate = true
|
||||
}
|
||||
slot = true
|
||||
}
|
||||
@@ -2277,9 +2434,30 @@ func cleanTemporaryFiles() {
|
||||
activeTempFiles = []string{}
|
||||
}
|
||||
|
||||
type replacePlaceholderParams struct {
|
||||
template string
|
||||
stripAnsi bool
|
||||
delimiter Delimiter
|
||||
printsep string
|
||||
forcePlus bool
|
||||
query string
|
||||
allItems []*Item
|
||||
lastAction actionType
|
||||
prompt string
|
||||
}
|
||||
|
||||
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string {
|
||||
return replacePlaceholder(
|
||||
template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list)
|
||||
return replacePlaceholder(replacePlaceholderParams{
|
||||
template: template,
|
||||
stripAnsi: t.ansi,
|
||||
delimiter: t.delimiter,
|
||||
printsep: t.printsep,
|
||||
forcePlus: forcePlus,
|
||||
query: input,
|
||||
allItems: list,
|
||||
lastAction: t.lastAction,
|
||||
prompt: t.promptString,
|
||||
})
|
||||
}
|
||||
|
||||
func (t *Terminal) evaluateScrollOffset() int {
|
||||
@@ -2317,9 +2495,9 @@ func (t *Terminal) evaluateScrollOffset() int {
|
||||
return util.Max(0, base)
|
||||
}
|
||||
|
||||
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
|
||||
current := allItems[:1]
|
||||
selected := allItems[1:]
|
||||
func replacePlaceholder(params replacePlaceholderParams) string {
|
||||
current := params.allItems[:1]
|
||||
selected := params.allItems[1:]
|
||||
if current[0] == nil {
|
||||
current = []*Item{}
|
||||
}
|
||||
@@ -2328,7 +2506,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
|
||||
}
|
||||
|
||||
// replace placeholders one by one
|
||||
return placeholder.ReplaceAllStringFunc(template, func(match string) string {
|
||||
return placeholder.ReplaceAllStringFunc(params.template, func(match string) string {
|
||||
escaped, match, flags := parsePlaceholder(match)
|
||||
|
||||
// this function implements the effects a placeholder has on items
|
||||
@@ -2338,8 +2516,8 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
|
||||
switch {
|
||||
case escaped:
|
||||
return match
|
||||
case match == "{q}":
|
||||
return quoteEntry(query)
|
||||
case match == "{q}" || match == "{fzf:query}":
|
||||
return quoteEntry(params.query)
|
||||
case match == "{}":
|
||||
replace = func(item *Item) string {
|
||||
switch {
|
||||
@@ -2350,11 +2528,22 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
|
||||
}
|
||||
return strconv.Itoa(n)
|
||||
case flags.file:
|
||||
return item.AsString(stripAnsi)
|
||||
return item.AsString(params.stripAnsi)
|
||||
default:
|
||||
return quoteEntry(item.AsString(stripAnsi))
|
||||
return quoteEntry(item.AsString(params.stripAnsi))
|
||||
}
|
||||
}
|
||||
case match == "{fzf:action}":
|
||||
name := ""
|
||||
for i, r := range params.lastAction.String()[3:] {
|
||||
if i > 0 && r >= 'A' && r <= 'Z' {
|
||||
name += "-"
|
||||
}
|
||||
name += string(r)
|
||||
}
|
||||
return strings.ToLower(name)
|
||||
case match == "{fzf:prompt}":
|
||||
return quoteEntry(params.prompt)
|
||||
default:
|
||||
// token type and also failover (below)
|
||||
rangeExpressions := strings.Split(match[1:len(match)-1], ",")
|
||||
@@ -2369,15 +2558,15 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
|
||||
}
|
||||
|
||||
replace = func(item *Item) string {
|
||||
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
|
||||
tokens := Tokenize(item.AsString(params.stripAnsi), params.delimiter)
|
||||
trans := Transform(tokens, ranges)
|
||||
str := joinTokens(trans)
|
||||
|
||||
// trim the last delimiter
|
||||
if delimiter.str != nil {
|
||||
str = strings.TrimSuffix(str, *delimiter.str)
|
||||
} else if delimiter.regex != nil {
|
||||
delims := delimiter.regex.FindAllStringIndex(str, -1)
|
||||
if params.delimiter.str != nil {
|
||||
str = strings.TrimSuffix(str, *params.delimiter.str)
|
||||
} else if params.delimiter.regex != nil {
|
||||
delims := params.delimiter.regex.FindAllStringIndex(str, -1)
|
||||
// make sure the delimiter is at the very end of the string
|
||||
if len(delims) > 0 && delims[len(delims)-1][1] == len(str) {
|
||||
str = str[:delims[len(delims)-1][0]]
|
||||
@@ -2397,7 +2586,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
|
||||
// apply 'replace' function over proper set of items and return result
|
||||
|
||||
items := current
|
||||
if flags.plus || forcePlus {
|
||||
if flags.plus || params.forcePlus {
|
||||
items = selected
|
||||
}
|
||||
replacements := make([]string, len(items))
|
||||
@@ -2407,7 +2596,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
|
||||
}
|
||||
|
||||
if flags.file {
|
||||
return writeTemporaryFile(replacements, printsep)
|
||||
return writeTemporaryFile(replacements, params.printsep)
|
||||
}
|
||||
return strings.Join(replacements, " ")
|
||||
})
|
||||
@@ -2489,8 +2678,8 @@ func (t *Terminal) currentItem() *Item {
|
||||
|
||||
func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) {
|
||||
current := t.currentItem()
|
||||
slot, plus, query := hasPreviewFlags(template)
|
||||
if !(!slot || query || (forcePlus || plus) && len(t.selected) > 0) {
|
||||
slot, plus, forceUpdate := hasPreviewFlags(template)
|
||||
if !(!slot || forceUpdate || (forcePlus || plus) && len(t.selected) > 0) {
|
||||
return current != nil, []*Item{current, current}
|
||||
}
|
||||
|
||||
@@ -2571,6 +2760,26 @@ func (t *Terminal) cancelPreview() {
|
||||
t.killPreview(exitCancel)
|
||||
}
|
||||
|
||||
func (t *Terminal) pwindowSize() tui.TermSize {
|
||||
if t.pwindow == nil {
|
||||
return tui.TermSize{}
|
||||
}
|
||||
size := tui.TermSize{Lines: t.pwindow.Height(), Columns: t.pwindow.Width()}
|
||||
|
||||
if t.termSize.PxWidth > 0 {
|
||||
size.PxWidth = size.Columns * t.termSize.PxWidth / t.termSize.Columns
|
||||
size.PxHeight = size.Lines * t.termSize.PxHeight / t.termSize.Lines
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func (t *Terminal) currentIndex() int32 {
|
||||
if currentItem := t.currentItem(); currentItem != nil {
|
||||
return currentItem.Index()
|
||||
}
|
||||
return minItem.Index()
|
||||
}
|
||||
|
||||
// Loop is called to start Terminal I/O
|
||||
func (t *Terminal) Loop() {
|
||||
// prof := profile.Start(profile.ProfilePath("/tmp/"))
|
||||
@@ -2622,12 +2831,13 @@ func (t *Terminal) Loop() {
|
||||
go func() {
|
||||
for {
|
||||
<-resizeChan
|
||||
t.reqBox.Set(reqFullRedraw, nil)
|
||||
t.reqBox.Set(reqResize, nil)
|
||||
}
|
||||
}()
|
||||
|
||||
t.mutex.Lock()
|
||||
t.initFunc()
|
||||
t.termSize = t.tui.Size()
|
||||
t.resizeWindows(false)
|
||||
t.printPrompt()
|
||||
t.printInfo()
|
||||
@@ -2661,6 +2871,7 @@ func (t *Terminal) Loop() {
|
||||
var items []*Item
|
||||
var commandTemplate string
|
||||
var pwindow tui.Window
|
||||
var pwindowSize tui.TermSize
|
||||
initialOffset := 0
|
||||
t.previewBox.Wait(func(events *util.Events) {
|
||||
for req, value := range *events {
|
||||
@@ -2671,6 +2882,7 @@ func (t *Terminal) Loop() {
|
||||
initialOffset = request.scrollOffset
|
||||
items = request.list
|
||||
pwindow = request.pwindow
|
||||
pwindowSize = request.pwindowSize
|
||||
}
|
||||
}
|
||||
events.Clear()
|
||||
@@ -2682,14 +2894,15 @@ func (t *Terminal) Loop() {
|
||||
command := t.replacePlaceholder(commandTemplate, false, string(query), items)
|
||||
cmd := util.ExecCommand(command, true)
|
||||
env := t.environ()
|
||||
if pwindow != nil {
|
||||
height := pwindow.Height()
|
||||
lines := fmt.Sprintf("LINES=%d", height)
|
||||
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
|
||||
if pwindowSize.Lines > 0 {
|
||||
lines := fmt.Sprintf("LINES=%d", pwindowSize.Lines)
|
||||
columns := fmt.Sprintf("COLUMNS=%d", pwindowSize.Columns)
|
||||
env = append(env, lines)
|
||||
env = append(env, "FZF_PREVIEW_"+lines)
|
||||
env = append(env, columns)
|
||||
env = append(env, "FZF_PREVIEW_"+columns)
|
||||
env = append(env, fmt.Sprintf("FZF_PREVIEW_TOP=%d", t.tui.Top()+pwindow.Top()))
|
||||
env = append(env, fmt.Sprintf("FZF_PREVIEW_LEFT=%d", pwindow.Left()))
|
||||
}
|
||||
cmd.Env = env
|
||||
|
||||
@@ -2817,7 +3030,7 @@ func (t *Terminal) Loop() {
|
||||
if len(command) > 0 && t.canPreview() {
|
||||
_, list := t.buildPlusList(command, false)
|
||||
t.cancelPreview()
|
||||
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list})
|
||||
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.pwindowSize(), t.evaluateScrollOffset(), list})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2851,17 +3064,14 @@ func (t *Terminal) Loop() {
|
||||
t.printInfo()
|
||||
case reqList:
|
||||
t.printList()
|
||||
var currentIndex int32 = minItem.Index()
|
||||
currentItem := t.currentItem()
|
||||
if currentItem != nil {
|
||||
currentIndex = currentItem.Index()
|
||||
}
|
||||
currentIndex := t.currentIndex()
|
||||
focusChanged := focusedIndex != currentIndex
|
||||
if focusChanged && t.track == trackCurrent {
|
||||
t.track = trackDisabled
|
||||
t.printInfo()
|
||||
}
|
||||
if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs && focusChanged {
|
||||
if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs && focusChanged && currentIndex != t.lastFocus {
|
||||
t.lastFocus = focusedIndex
|
||||
t.serverInputChan <- onFocus
|
||||
}
|
||||
if focusChanged || version != t.version {
|
||||
@@ -2885,7 +3095,10 @@ func (t *Terminal) Loop() {
|
||||
case reqReinit:
|
||||
t.tui.Resume(t.fullscreen, t.sigstop)
|
||||
t.redraw()
|
||||
case reqFullRedraw:
|
||||
case reqResize, reqFullRedraw:
|
||||
if req == reqResize {
|
||||
t.termSize = t.tui.Size()
|
||||
}
|
||||
wasHidden := t.pwindow == nil
|
||||
t.redraw()
|
||||
if wasHidden && t.hasPreviewWindow() {
|
||||
@@ -2974,8 +3187,18 @@ func (t *Terminal) Loop() {
|
||||
select {
|
||||
case event = <-t.eventChan:
|
||||
needBarrier = !event.Is(tui.Load, tui.One, tui.Zero)
|
||||
case actions = <-t.serverInputChan:
|
||||
case serverActions := <-t.serverInputChan:
|
||||
event = tui.Invalid.AsEvent()
|
||||
if t.listenAddr == nil || t.listenAddr.IsLocal() || t.listenUnsafe {
|
||||
actions = serverActions
|
||||
} else {
|
||||
for _, action := range serverActions {
|
||||
if !processExecution(action.t) {
|
||||
actions = append(actions, action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
needBarrier = false
|
||||
}
|
||||
}
|
||||
@@ -3038,17 +3261,26 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
|
||||
var doAction func(*action) bool
|
||||
doActions := func(actions []*action) bool {
|
||||
var doActions func(actions []*action) bool
|
||||
doActions = func(actions []*action) bool {
|
||||
currentIndex := t.currentIndex()
|
||||
for _, action := range actions {
|
||||
if !doAction(action) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs {
|
||||
if newIndex := t.currentIndex(); newIndex != currentIndex {
|
||||
t.lastFocus = newIndex
|
||||
return doActions(onFocus)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
doAction = func(a *action) bool {
|
||||
switch a.t {
|
||||
case actIgnore:
|
||||
case actIgnore, actStart, actClick:
|
||||
case actResponse:
|
||||
t.serverOutputChan <- t.dumpStatus(parseGetParams(a.a))
|
||||
case actBecome:
|
||||
@@ -3102,8 +3334,12 @@ func (t *Terminal) Loop() {
|
||||
if valid {
|
||||
t.cancelPreview()
|
||||
t.previewBox.Set(reqPreviewEnqueue,
|
||||
previewRequest{t.previewOpts.command, t.pwindow, t.evaluateScrollOffset(), list})
|
||||
previewRequest{t.previewOpts.command, t.pwindow, t.pwindowSize(), t.evaluateScrollOffset(), list})
|
||||
}
|
||||
} else {
|
||||
// Discard the preview content so that it won't accidentally appear
|
||||
// when preview window is re-enabled and previewDelay is triggered
|
||||
t.previewer.lines = nil
|
||||
}
|
||||
}
|
||||
case actTogglePreviewWrap:
|
||||
@@ -3115,6 +3351,7 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
case actTransformPrompt:
|
||||
prompt := t.executeCommand(a.a, false, true, true, true)
|
||||
t.promptString = prompt
|
||||
t.prompt, t.promptLen = t.parsePrompt(prompt)
|
||||
req(reqPrompt)
|
||||
case actTransformQuery:
|
||||
@@ -3158,6 +3395,7 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
case actBeginningOfLine:
|
||||
t.cx = 0
|
||||
t.xoffset = 0
|
||||
case actBackwardChar:
|
||||
if t.cx > 0 {
|
||||
t.cx--
|
||||
@@ -3190,6 +3428,10 @@ func (t *Terminal) Loop() {
|
||||
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(a.a, &tui.ColPreviewLabel, false)
|
||||
req(reqRedrawPreviewLabel)
|
||||
}
|
||||
case actTransform:
|
||||
body := t.executeCommand(a.a, false, true, true, false)
|
||||
actions := parseSingleActionList(strings.Trim(body, "\r\n"), func(message string) {})
|
||||
return doActions(actions)
|
||||
case actTransformBorderLabel:
|
||||
if t.border != nil {
|
||||
label := t.executeCommand(a.a, false, true, true, true)
|
||||
@@ -3203,6 +3445,7 @@ func (t *Terminal) Loop() {
|
||||
req(reqRedrawPreviewLabel)
|
||||
}
|
||||
case actChangePrompt:
|
||||
t.promptString = a.a
|
||||
t.prompt, t.promptLen = t.parsePrompt(a.a)
|
||||
req(reqPrompt)
|
||||
case actPreview:
|
||||
@@ -3220,7 +3463,7 @@ func (t *Terminal) Loop() {
|
||||
req(reqQuit)
|
||||
case actDeleteChar:
|
||||
t.delChar()
|
||||
case actDeleteCharEOF:
|
||||
case actDeleteCharEof:
|
||||
if !t.delChar() && t.cx == 0 {
|
||||
req(reqQuit)
|
||||
}
|
||||
@@ -3234,7 +3477,7 @@ func (t *Terminal) Loop() {
|
||||
t.input = []rune{}
|
||||
t.cx = 0
|
||||
}
|
||||
case actBackwardDeleteCharEOF:
|
||||
case actBackwardDeleteCharEof:
|
||||
if len(t.input) == 0 {
|
||||
req(reqQuit)
|
||||
} else if t.cx > 0 {
|
||||
@@ -3341,6 +3584,12 @@ func (t *Terminal) Loop() {
|
||||
if len(t.selected) > 0 || t.merger.Length() > 0 || !t.reading && t.count == 0 {
|
||||
req(reqClose)
|
||||
}
|
||||
case actAcceptOrPrintQuery:
|
||||
if len(t.selected) > 0 || t.merger.Length() > 0 {
|
||||
req(reqClose)
|
||||
} else {
|
||||
req(reqPrintQuery)
|
||||
}
|
||||
case actClearScreen:
|
||||
req(reqFullRedraw)
|
||||
case actClearQuery:
|
||||
@@ -3447,7 +3696,7 @@ func (t *Terminal) Loop() {
|
||||
t.yanked = copySlice(t.input[t.cx:])
|
||||
t.input = t.input[:t.cx]
|
||||
}
|
||||
case actRune:
|
||||
case actChar:
|
||||
prefix := copySlice(t.input[:t.cx])
|
||||
t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
|
||||
t.cx++
|
||||
@@ -3475,6 +3724,12 @@ func (t *Terminal) Loop() {
|
||||
t.track = trackEnabled
|
||||
}
|
||||
req(reqInfo)
|
||||
case actShowHeader:
|
||||
t.headerVisible = true
|
||||
req(reqList, reqInfo, reqPrompt, reqHeader)
|
||||
case actHideHeader:
|
||||
t.headerVisible = false
|
||||
req(reqList, reqInfo, reqPrompt, reqHeader)
|
||||
case actToggleHeader:
|
||||
t.headerVisible = !t.headerVisible
|
||||
req(reqList, reqInfo, reqPrompt, reqHeader)
|
||||
@@ -3644,8 +3899,8 @@ func (t *Terminal) Loop() {
|
||||
// We run the command even when there's no match
|
||||
// 1. If the template doesn't have any slots
|
||||
// 2. If the template has {q}
|
||||
slot, _, query := hasPreviewFlags(a.a)
|
||||
valid = !slot || query
|
||||
slot, _, forceUpdate := hasPreviewFlags(a.a)
|
||||
valid = !slot || forceUpdate
|
||||
}
|
||||
if valid {
|
||||
command := t.replacePlaceholder(a.a, false, string(t.input), list)
|
||||
@@ -3725,6 +3980,10 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !processExecution(a.t) {
|
||||
t.lastAction = a.t
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -3738,7 +3997,7 @@ func (t *Terminal) Loop() {
|
||||
actions = t.keymap[event.Comparable()]
|
||||
}
|
||||
if len(actions) == 0 && event.Type == tui.Rune {
|
||||
doAction(&action{t: actRune})
|
||||
doAction(&action{t: actChar})
|
||||
} else if !doActions(actions) {
|
||||
continue
|
||||
}
|
||||
@@ -3769,8 +4028,8 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
|
||||
if queryChanged && t.canPreview() && len(t.previewOpts.command) > 0 {
|
||||
_, _, q := hasPreviewFlags(t.previewOpts.command)
|
||||
if q {
|
||||
_, _, forceUpdate := hasPreviewFlags(t.previewOpts.command)
|
||||
if forceUpdate {
|
||||
t.version++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,20 @@ import (
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
|
||||
return replacePlaceholder(replacePlaceholderParams{
|
||||
template: template,
|
||||
stripAnsi: stripAnsi,
|
||||
delimiter: delimiter,
|
||||
printsep: printsep,
|
||||
forcePlus: forcePlus,
|
||||
query: query,
|
||||
allItems: allItems,
|
||||
lastAction: actBackwardDeleteCharEof,
|
||||
prompt: "prompt",
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplacePlaceholder(t *testing.T) {
|
||||
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
|
||||
items1 := []*Item{item1, item1}
|
||||
@@ -52,90 +66,90 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
*/
|
||||
|
||||
// {}, preserve ansi
|
||||
result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {}", false, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
|
||||
|
||||
// {}, strip ansi
|
||||
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// {}, with multiple items
|
||||
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// {..}, strip leading whitespaces, preserve ansi
|
||||
result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
|
||||
|
||||
// {..}, strip leading whitespaces, strip ansi
|
||||
result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// {q}
|
||||
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}")
|
||||
|
||||
// {q}, multiple items
|
||||
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||
result = replacePlaceholderTest("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||
result = replacePlaceholderTest("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
result = replacePlaceholderTest("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
|
||||
|
||||
// forcePlus
|
||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
|
||||
result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
|
||||
|
||||
// Whitespace preserving flag with "'" delimiter
|
||||
result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}}bar baz{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// Whitespace preserving flag with regex delimiter
|
||||
regex = regexp.MustCompile(`\w+`)
|
||||
|
||||
result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} {{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}}{{.I}}{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} {{.O}}")
|
||||
|
||||
// No match
|
||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
|
||||
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
|
||||
check("echo /")
|
||||
|
||||
// No match, but with selections
|
||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
|
||||
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
|
||||
checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// String delimiter
|
||||
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}")
|
||||
|
||||
// Regex delimiter
|
||||
regex = regexp.MustCompile("[oa]+")
|
||||
// foo'bar baz
|
||||
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}")
|
||||
|
||||
/*
|
||||
@@ -155,7 +169,6 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
newItem("7a 7b 7c 7d 7e 7f"),
|
||||
}
|
||||
stripAnsi := false
|
||||
printsep = "\n"
|
||||
forcePlus := false
|
||||
query := "sample query"
|
||||
|
||||
@@ -198,18 +211,23 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
// query flag is not removed after parsing, so it gets doubled
|
||||
// while the double q is invalid, it is useful here for testing purposes
|
||||
templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}"
|
||||
templateToOutput[`{fzf:query}`] = "{{.O}}" + query + "{{.O}}"
|
||||
templateToOutput[`{fzf:action} {fzf:prompt}`] = "backward-delete-char-eof 'prompt'"
|
||||
|
||||
// IV. escaping placeholder
|
||||
templateToOutput[`\{}`] = `{}`
|
||||
templateToOutput[`\{q}`] = `{q}`
|
||||
templateToOutput[`\{fzf:query}`] = `{fzf:query}`
|
||||
templateToOutput[`\{fzf:action}`] = `{fzf:action}`
|
||||
templateToOutput[`\{++}`] = `{++}`
|
||||
templateToOutput[`{++}`] = templateToOutput[`{+}`]
|
||||
|
||||
for giveTemplate, wantOutput := range templateToOutput {
|
||||
result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||
result = replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||
checkFormat(wantOutput)
|
||||
}
|
||||
for giveTemplate, wantOutput := range templateToFile {
|
||||
path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||
path := replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||
|
||||
data, err := readFile(path)
|
||||
if err != nil {
|
||||
@@ -563,7 +581,7 @@ func testCommands(t *testing.T, tests []testCase) {
|
||||
|
||||
// evaluate the test cases
|
||||
for idx, test := range tests {
|
||||
gotOutput := replacePlaceholder(
|
||||
gotOutput := replacePlaceholderTest(
|
||||
test.give.template, stripAnsi, delimiter, printsep, forcePlus,
|
||||
test.give.query,
|
||||
test.give.allItems)
|
||||
@@ -605,7 +623,7 @@ func (flags placeholderFlags) encodePlaceholder() string {
|
||||
if flags.file {
|
||||
encoded += "f"
|
||||
}
|
||||
if flags.query {
|
||||
if flags.forceUpdate { // FIXME
|
||||
encoded += "q"
|
||||
}
|
||||
return encoded
|
||||
|
||||
@@ -7,14 +7,35 @@ import (
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var escaper *strings.Replacer
|
||||
|
||||
func init() {
|
||||
tokens := strings.Split(os.Getenv("SHELL"), "/")
|
||||
if tokens[len(tokens)-1] == "fish" {
|
||||
// https://fishshell.com/docs/current/language.html#quotes
|
||||
// > The only meaningful escape sequences in single quotes are \', which
|
||||
// > escapes a single quote and \\, which escapes the backslash symbol.
|
||||
escaper = strings.NewReplacer("\\", "\\\\", "'", "\\'")
|
||||
} else {
|
||||
escaper = strings.NewReplacer("'", "'\\''")
|
||||
}
|
||||
}
|
||||
|
||||
func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||
signal.Notify(resizeChan, syscall.SIGWINCH)
|
||||
}
|
||||
|
||||
func notifyStop(p *os.Process) {
|
||||
p.Signal(syscall.SIGSTOP)
|
||||
pid := p.Pid
|
||||
pgid, err := unix.Getpgid(pid)
|
||||
if err == nil {
|
||||
pid = pgid * -1
|
||||
}
|
||||
unix.Kill(pid, syscall.SIGSTOP)
|
||||
}
|
||||
|
||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||
@@ -22,5 +43,5 @@ func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||
}
|
||||
|
||||
func quoteEntry(entry string) string {
|
||||
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
||||
return "'" + escaper.Replace(entry) + "'"
|
||||
}
|
||||
|
||||
@@ -38,8 +38,10 @@ func (r *FullscreenRenderer) Clear() {}
|
||||
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
|
||||
func (r *FullscreenRenderer) Refresh() {}
|
||||
func (r *FullscreenRenderer) Close() {}
|
||||
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
|
||||
|
||||
func (r *FullscreenRenderer) GetChar() Event { return Event{} }
|
||||
func (r *FullscreenRenderer) Top() int { return 0 }
|
||||
func (r *FullscreenRenderer) MaxX() int { return 0 }
|
||||
func (r *FullscreenRenderer) MaxY() int { return 0 }
|
||||
|
||||
|
||||
@@ -32,8 +32,7 @@ var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]
|
||||
var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
|
||||
|
||||
func (r *LightRenderer) PassThrough(str string) {
|
||||
r.queued.WriteString(str)
|
||||
r.flush()
|
||||
r.queued.WriteString("\x1b7" + str + "\x1b8")
|
||||
}
|
||||
|
||||
func (r *LightRenderer) stderr(str string) {
|
||||
@@ -403,7 +402,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
return Event{F3, 0, nil}
|
||||
case 'S':
|
||||
return Event{F4, 0, nil}
|
||||
case '1', '2', '3', '4', '5', '6':
|
||||
case '1', '2', '3', '4', '5', '6', '7', '8':
|
||||
if len(r.buffer) < 4 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
@@ -454,6 +453,10 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
return Event{PgUp, 0, nil}
|
||||
case '6':
|
||||
return Event{PgDn, 0, nil}
|
||||
case '7':
|
||||
return Event{Home, 0, nil}
|
||||
case '8':
|
||||
return Event{End, 0, nil}
|
||||
case '1':
|
||||
switch r.buffer[3] {
|
||||
case '~':
|
||||
@@ -721,6 +724,10 @@ func (r *LightRenderer) Close() {
|
||||
r.restoreTerminal()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Top() int {
|
||||
return r.yoffset
|
||||
}
|
||||
|
||||
func (r *LightRenderer) MaxX() int {
|
||||
return r.width
|
||||
}
|
||||
@@ -756,6 +763,10 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *LightWindow) DrawBorder() {
|
||||
w.drawBorder(false)
|
||||
}
|
||||
|
||||
func (w *LightWindow) DrawHBorder() {
|
||||
w.drawBorder(true)
|
||||
}
|
||||
@@ -1088,14 +1099,21 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu
|
||||
}
|
||||
|
||||
func (w *LightWindow) FinishFill() {
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
if w.posy < w.height {
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
}
|
||||
for y := w.posy + 1; y < w.height; y++ {
|
||||
w.MoveAndClear(y, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) Erase() {
|
||||
w.drawBorder(false)
|
||||
// We don't erase the window here to avoid flickering during scroll
|
||||
w.DrawBorder()
|
||||
w.Move(0, 0)
|
||||
w.FinishFill()
|
||||
w.Move(0, 0)
|
||||
}
|
||||
|
||||
func (w *LightWindow) EraseMaybe() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
"golang.org/x/sys/unix"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
@@ -108,3 +109,11 @@ func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
||||
}
|
||||
return int(b[0]), true
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Size() TermSize {
|
||||
ws, err := unix.IoctlGetWinsize(int(r.ttyin.Fd()), unix.TIOCGWINSZ)
|
||||
if err != nil {
|
||||
return TermSize{}
|
||||
}
|
||||
return TermSize{int(ws.Row), int(ws.Col), int(ws.Xpixel), int(ws.Ypixel)}
|
||||
}
|
||||
|
||||
@@ -110,16 +110,24 @@ func (r *LightRenderer) restoreTerminal() error {
|
||||
return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) updateTerminalSize() {
|
||||
func (r *LightRenderer) Size() TermSize {
|
||||
var w, h int
|
||||
var bufferInfo windows.ConsoleScreenBufferInfo
|
||||
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
|
||||
r.width = getEnv("COLUMNS", defaultWidth)
|
||||
r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight))
|
||||
w = getEnv("COLUMNS", defaultWidth)
|
||||
h = r.maxHeightFunc(getEnv("LINES", defaultHeight))
|
||||
|
||||
} else {
|
||||
r.width = int(bufferInfo.Window.Right - bufferInfo.Window.Left)
|
||||
r.height = r.maxHeightFunc(int(bufferInfo.Window.Bottom - bufferInfo.Window.Top))
|
||||
w = int(bufferInfo.Window.Right - bufferInfo.Window.Left)
|
||||
h = r.maxHeightFunc(int(bufferInfo.Window.Bottom - bufferInfo.Window.Top))
|
||||
}
|
||||
return TermSize{h, w, 0, 0}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) updateTerminalSize() {
|
||||
size := r.Size()
|
||||
r.width = size.Columns
|
||||
r.height = size.Lines
|
||||
}
|
||||
|
||||
func (r *LightRenderer) findOffset() (row int, col int) {
|
||||
|
||||
@@ -100,7 +100,7 @@ const (
|
||||
|
||||
func (r *FullscreenRenderer) PassThrough(str string) {
|
||||
// No-op
|
||||
// https://github.com/gdamore/tcell/issues/363#issuecomment-680665073
|
||||
// https://github.com/gdamore/tcell/pull/650#issuecomment-1806442846
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
|
||||
@@ -172,6 +172,10 @@ func (r *FullscreenRenderer) Init() {
|
||||
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Top() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) MaxX() int {
|
||||
ncols, _ := _screen.Size()
|
||||
return int(ncols)
|
||||
@@ -203,6 +207,12 @@ func (r *FullscreenRenderer) Refresh() {
|
||||
// noop
|
||||
}
|
||||
|
||||
// TODO: Pixel width and height not implemented
|
||||
func (r *FullscreenRenderer) Size() TermSize {
|
||||
cols, lines := _screen.Size()
|
||||
return TermSize{lines, cols, 0, 0}
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) GetChar() Event {
|
||||
ev := _screen.PollEvent()
|
||||
switch ev := ev.(type) {
|
||||
@@ -541,9 +551,15 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Erase() {
|
||||
w.drawBorder(false)
|
||||
fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ')
|
||||
}
|
||||
|
||||
func (w *TcellWindow) EraseMaybe() bool {
|
||||
w.Erase()
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Enclose(y int, x int) bool {
|
||||
return x >= w.left && x < (w.left+w.width) &&
|
||||
y >= w.top && y < (w.top+w.height)
|
||||
@@ -692,6 +708,10 @@ func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
|
||||
return w.fillString(str, NewColorPair(fg, bg, a))
|
||||
}
|
||||
|
||||
func (w *TcellWindow) DrawBorder() {
|
||||
w.drawBorder(false)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) DrawHBorder() {
|
||||
w.drawBorder(true)
|
||||
}
|
||||
|
||||
@@ -473,6 +473,13 @@ func MakeTransparentBorder() BorderStyle {
|
||||
bottomRight: ' '}
|
||||
}
|
||||
|
||||
type TermSize struct {
|
||||
Lines int
|
||||
Columns int
|
||||
PxWidth int
|
||||
PxHeight int
|
||||
}
|
||||
|
||||
type Renderer interface {
|
||||
Init()
|
||||
Resize(maxHeightFunc func(int) int)
|
||||
@@ -487,9 +494,12 @@ type Renderer interface {
|
||||
|
||||
GetChar() Event
|
||||
|
||||
Top() int
|
||||
MaxX() int
|
||||
MaxY() int
|
||||
|
||||
Size() TermSize
|
||||
|
||||
NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window
|
||||
}
|
||||
|
||||
@@ -499,6 +509,7 @@ type Window interface {
|
||||
Width() int
|
||||
Height() int
|
||||
|
||||
DrawBorder()
|
||||
DrawHBorder()
|
||||
Refresh()
|
||||
FinishFill()
|
||||
@@ -515,6 +526,7 @@ type Window interface {
|
||||
Fill(text string) FillReturn
|
||||
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
|
||||
Erase()
|
||||
EraseMaybe() bool
|
||||
}
|
||||
|
||||
type FullscreenRenderer struct {
|
||||
|
||||
@@ -741,6 +741,12 @@ class TestGoFZF < TestBase
|
||||
'xxoxxxxxxx',
|
||||
'xoxxxxxxxx'
|
||||
], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.lines(chomp: true)
|
||||
|
||||
writelines(tempname, ['/bar/baz', '/foo/bar/baz'])
|
||||
assert_equal [
|
||||
'/foo/bar/baz',
|
||||
'/bar/baz'
|
||||
], `#{FZF} -fbaz --tiebreak=end < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_tiebreak_length_with_nth
|
||||
@@ -1776,6 +1782,35 @@ class TestGoFZF < TestBase
|
||||
assert_equal %w[foo], readonce.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_accept_or_print_query_without_match
|
||||
tmux.send_keys %(seq 1000 | #{fzf('--bind enter:accept-or-print-query')}), :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.send_keys 99_999
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
assert_equal %w[99999], readonce.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_accept_or_print_query_with_match
|
||||
tmux.send_keys %(seq 1000 | #{fzf('--bind enter:accept-or-print-query')}), :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.send_keys '^99$'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
assert_equal %w[99], readonce.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_accept_or_print_query_with_multi_selection
|
||||
tmux.send_keys %(seq 1000 | #{fzf('--bind enter:accept-or-print-query --multi')}), :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.send_keys :BTab, :BTab, :BTab
|
||||
tmux.until { |lines| assert_equal 3, lines.select_count }
|
||||
tmux.send_keys 99_999
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
assert_equal %w[1 2 3], readonce.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_preview_update_on_select
|
||||
tmux.send_keys %(seq 10 | fzf -m --preview 'echo {+}' --bind a:toggle-all),
|
||||
:Enter
|
||||
@@ -1987,6 +2022,13 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_equal '> RAB', lines[-1] }
|
||||
end
|
||||
|
||||
def test_transform
|
||||
tmux.send_keys %{#{FZF} --bind 'focus:transform:echo "change-prompt({fzf:action})"'}, :Enter
|
||||
tmux.until { |lines| assert_equal 'start', lines[-1] }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_equal 'up', lines[-1] }
|
||||
end
|
||||
|
||||
def test_clear_selection
|
||||
tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
@@ -3020,6 +3062,13 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys :x
|
||||
tmux.until { |lines| assert(lines.any? { |line| line.include?('[x-foo]') }) }
|
||||
end
|
||||
|
||||
def test_preview_window_hidden_on_focus
|
||||
tmux.send_keys "seq 3 | #{FZF} --preview 'echo {}' --bind focus:hide-preview", :Enter
|
||||
tmux.until { |lines| assert_includes lines, '> 1' }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines, '> 2' }
|
||||
end
|
||||
end
|
||||
|
||||
module TestShell
|
||||
|
||||
@@ -4,6 +4,7 @@ ba = "ba"
|
||||
fo = "fo"
|
||||
enew = "enew"
|
||||
tabe = "tabe"
|
||||
Iterm = "Iterm"
|
||||
|
||||
[files]
|
||||
extend-exclude = ["README.md"]
|
||||
|
||||
Reference in New Issue
Block a user