mirror of
https://github.com/junegunn/fzf.git
synced 2025-12-21 20:01:16 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4c3ecc57e | ||
|
|
673c5d886d | ||
|
|
f799b568d1 | ||
|
|
7bff4661f6 | ||
|
|
ffd8bef808 | ||
|
|
02cee2234d | ||
|
|
e0dd2be3fb | ||
|
|
a33c011c21 | ||
|
|
7c3f42bbba | ||
|
|
edac9820b5 | ||
|
|
84a47f7102 | ||
|
|
97ae8afb6f |
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,6 +1,24 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
0.28.0
|
||||||
|
------
|
||||||
|
- Added `--header-first` option to print header before the prompt line
|
||||||
|
```sh
|
||||||
|
fzf --header $'Welcome to fzf\n▔▔▔▔▔▔▔▔▔▔▔▔▔▔' --reverse --height 30% --border --header-first
|
||||||
|
```
|
||||||
|
- Added `--scroll-off=LINES` option (similar to `scrolloff` option of Vim)
|
||||||
|
- You can set it to a very large number so that the cursor stays in the
|
||||||
|
middle of the screen while scrolling
|
||||||
|
```sh
|
||||||
|
fzf --scroll-off=5
|
||||||
|
fzf --scroll-off=999
|
||||||
|
```
|
||||||
|
- Fixed bug where preview window is not updated on `reload` (#2644)
|
||||||
|
- fzf on Windows will also use `$SHELL` to execute external programs
|
||||||
|
- See #2638 and #2647
|
||||||
|
- Thanks to @rashil2000, @vovcacik, and @janlazo
|
||||||
|
|
||||||
0.27.3
|
0.27.3
|
||||||
------
|
------
|
||||||
- Preview window is `hidden` by default when there are `preview` bindings but
|
- Preview window is `hidden` by default when there are `preview` bindings but
|
||||||
|
|||||||
1
Makefile
1
Makefile
@@ -102,6 +102,7 @@ endif
|
|||||||
grep -qF $(VERSION) install.ps1
|
grep -qF $(VERSION) install.ps1
|
||||||
|
|
||||||
# Make release note out of CHANGELOG.md
|
# Make release note out of CHANGELOG.md
|
||||||
|
mkdir -p tmp
|
||||||
sed -n '/^$(VERSION_REGEX)$$/,/^[0-9]/p' CHANGELOG.md | tail -r | \
|
sed -n '/^$(VERSION_REGEX)$$/,/^[0-9]/p' CHANGELOG.md | tail -r | \
|
||||||
sed '1,/^ *$$/d' | tail -r | sed 1,2d | tee tmp/release-note
|
sed '1,/^ *$$/d' | tail -r | sed 1,2d | tee tmp/release-note
|
||||||
|
|
||||||
|
|||||||
2
install
2
install
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
version=0.27.3
|
version=0.28.0
|
||||||
auto_completion=
|
auto_completion=
|
||||||
key_bindings=
|
key_bindings=
|
||||||
update_config=2
|
update_config=2
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
$version="0.27.3"
|
$version="0.28.0"
|
||||||
|
|
||||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
|
||||||
|
|||||||
4
main.go
4
main.go
@@ -1,11 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/junegunn/fzf/src"
|
fzf "github.com/junegunn/fzf/src"
|
||||||
"github.com/junegunn/fzf/src/protector"
|
"github.com/junegunn/fzf/src/protector"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version string = "0.27"
|
var version string = "0.28"
|
||||||
var revision string = "devel"
|
var revision string = "devel"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
..
|
..
|
||||||
.TH fzf-tmux 1 "Oct 2021" "fzf 0.27.3" "fzf-tmux - open fzf in tmux split pane"
|
.TH fzf-tmux 1 "Nov 2021" "fzf 0.28.0" "fzf-tmux - open fzf in tmux split pane"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf-tmux - open fzf in tmux split pane
|
fzf-tmux - open fzf in tmux split pane
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
..
|
..
|
||||||
.TH fzf 1 "Oct 2021" "fzf 0.27.3" "fzf - a command-line fuzzy finder"
|
.TH fzf 1 "Nov 2021" "fzf 0.28.0" "fzf - a command-line fuzzy finder"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf - a command-line fuzzy finder
|
fzf - a command-line fuzzy finder
|
||||||
@@ -135,10 +135,14 @@ Enable cyclic scroll
|
|||||||
Keep the right end of the line visible when it's too long. Effective only when
|
Keep the right end of the line visible when it's too long. Effective only when
|
||||||
the query string is empty.
|
the query string is empty.
|
||||||
.TP
|
.TP
|
||||||
|
.BI "--scroll-off=" "LINES"
|
||||||
|
Number of screen lines to keep above or below when scrolling to the top or to
|
||||||
|
the bottom (default: 0).
|
||||||
|
.TP
|
||||||
.B "--no-hscroll"
|
.B "--no-hscroll"
|
||||||
Disable horizontal scroll
|
Disable horizontal scroll
|
||||||
.TP
|
.TP
|
||||||
.BI "--hscroll-off=" "COL"
|
.BI "--hscroll-off=" "COLS"
|
||||||
Number of screen columns to keep to the right of the highlighted substring
|
Number of screen columns to keep to the right of the highlighted substring
|
||||||
(default: 10). Setting it to a large value will cause the text to be positioned
|
(default: 10). Setting it to a large value will cause the text to be positioned
|
||||||
on the center of the screen.
|
on the center of the screen.
|
||||||
@@ -295,6 +299,9 @@ are not affected by \fB--with-nth\fR. ANSI color codes are processed even when
|
|||||||
The first N lines of the input are treated as the sticky header. When
|
The first N lines of the input are treated as the sticky header. When
|
||||||
\fB--with-nth\fR is set, the lines are transformed just like the other
|
\fB--with-nth\fR is set, the lines are transformed just like the other
|
||||||
lines that follow.
|
lines that follow.
|
||||||
|
.TP
|
||||||
|
.B "--header-first"
|
||||||
|
Print header before the prompt line
|
||||||
.SS Display
|
.SS Display
|
||||||
.TP
|
.TP
|
||||||
.B "--ansi"
|
.B "--ansi"
|
||||||
@@ -853,6 +860,7 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBpreview-top\fR
|
\fBpreview-top\fR
|
||||||
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
|
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
|
||||||
\fBprint-query\fR (print query and exit)
|
\fBprint-query\fR (print query and exit)
|
||||||
|
\fBput\fR (put the character to the prompt)
|
||||||
\fBrefresh-preview\fR
|
\fBrefresh-preview\fR
|
||||||
\fBreload(...)\fR (see below for the details)
|
\fBreload(...)\fR (see below for the details)
|
||||||
\fBreplace-query\fR (replace query string with the current selection)
|
\fBreplace-query\fR (replace query string with the current selection)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ fi
|
|||||||
###########################################################
|
###########################################################
|
||||||
|
|
||||||
# To redraw line after fzf closes (printf '\e[5n')
|
# To redraw line after fzf closes (printf '\e[5n')
|
||||||
bind '"\e[0n": redraw-current-line'
|
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||||
|
|
||||||
__fzf_comprun() {
|
__fzf_comprun() {
|
||||||
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
|
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
|
||||||
|
|||||||
@@ -44,8 +44,10 @@ const usage = `usage: fzf [options]
|
|||||||
--bind=KEYBINDS Custom key bindings. Refer to the man page.
|
--bind=KEYBINDS Custom key bindings. Refer to the man page.
|
||||||
--cycle Enable cyclic scroll
|
--cycle Enable cyclic scroll
|
||||||
--keep-right Keep the right end of the line visible on overflow
|
--keep-right Keep the right end of the line visible on overflow
|
||||||
|
--scroll-off=LINES Number of screen lines to keep above or below when
|
||||||
|
scrolling to the top or to the bottom (default: 0)
|
||||||
--no-hscroll Disable horizontal scroll
|
--no-hscroll Disable horizontal scroll
|
||||||
--hscroll-off=COL Number of screen columns to keep to the right of the
|
--hscroll-off=COLS Number of screen columns to keep to the right of the
|
||||||
highlighted substring (default: 10)
|
highlighted substring (default: 10)
|
||||||
--filepath-word Make word-wise movements respect path separators
|
--filepath-word Make word-wise movements respect path separators
|
||||||
--jump-labels=CHARS Label characters for jump and jump-accept
|
--jump-labels=CHARS Label characters for jump and jump-accept
|
||||||
@@ -67,6 +69,7 @@ const usage = `usage: fzf [options]
|
|||||||
--marker=STR Multi-select marker (default: '>')
|
--marker=STR Multi-select marker (default: '>')
|
||||||
--header=STR String to print as header
|
--header=STR String to print as header
|
||||||
--header-lines=N The first N lines of the input are treated as header
|
--header-lines=N The first N lines of the input are treated as header
|
||||||
|
--header-first Print header before the prompt line
|
||||||
|
|
||||||
Display
|
Display
|
||||||
--ansi Enable processing of ANSI color codes
|
--ansi Enable processing of ANSI color codes
|
||||||
@@ -200,6 +203,7 @@ type Options struct {
|
|||||||
KeepRight bool
|
KeepRight bool
|
||||||
Hscroll bool
|
Hscroll bool
|
||||||
HscrollOff int
|
HscrollOff int
|
||||||
|
ScrollOff int
|
||||||
FileWord bool
|
FileWord bool
|
||||||
InfoStyle infoStyle
|
InfoStyle infoStyle
|
||||||
JumpLabels string
|
JumpLabels string
|
||||||
@@ -222,6 +226,7 @@ type Options struct {
|
|||||||
History *History
|
History *History
|
||||||
Header []string
|
Header []string
|
||||||
HeaderLines int
|
HeaderLines int
|
||||||
|
HeaderFirst bool
|
||||||
Margin [4]sizeSpec
|
Margin [4]sizeSpec
|
||||||
Padding [4]sizeSpec
|
Padding [4]sizeSpec
|
||||||
BorderShape tui.BorderShape
|
BorderShape tui.BorderShape
|
||||||
@@ -261,6 +266,7 @@ func defaultOptions() *Options {
|
|||||||
KeepRight: false,
|
KeepRight: false,
|
||||||
Hscroll: true,
|
Hscroll: true,
|
||||||
HscrollOff: 10,
|
HscrollOff: 10,
|
||||||
|
ScrollOff: 0,
|
||||||
FileWord: false,
|
FileWord: false,
|
||||||
InfoStyle: infoDefault,
|
InfoStyle: infoDefault,
|
||||||
JumpLabels: defaultJumpLabels,
|
JumpLabels: defaultJumpLabels,
|
||||||
@@ -283,6 +289,7 @@ func defaultOptions() *Options {
|
|||||||
History: nil,
|
History: nil,
|
||||||
Header: make([]string, 0),
|
Header: make([]string, 0),
|
||||||
HeaderLines: 0,
|
HeaderLines: 0,
|
||||||
|
HeaderFirst: false,
|
||||||
Margin: defaultMargin(),
|
Margin: defaultMargin(),
|
||||||
Padding: defaultMargin(),
|
Padding: defaultMargin(),
|
||||||
Unicode: true,
|
Unicode: true,
|
||||||
@@ -974,6 +981,12 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
|
|||||||
appendAction(actEnableSearch)
|
appendAction(actEnableSearch)
|
||||||
case "disable-search":
|
case "disable-search":
|
||||||
appendAction(actDisableSearch)
|
appendAction(actDisableSearch)
|
||||||
|
case "put":
|
||||||
|
if key.Type == tui.Rune && unicode.IsGraphic(key.Char) {
|
||||||
|
appendAction(actRune)
|
||||||
|
} else {
|
||||||
|
errorExit("unable to put non-printable character: " + pair[0])
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
t := isExecuteAction(specLower)
|
t := isExecuteAction(specLower)
|
||||||
if t == actIgnore {
|
if t == actIgnore {
|
||||||
@@ -1354,6 +1367,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
opts.Hscroll = false
|
opts.Hscroll = false
|
||||||
case "--hscroll-off":
|
case "--hscroll-off":
|
||||||
opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required")
|
opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required")
|
||||||
|
case "--scroll-off":
|
||||||
|
opts.ScrollOff = nextInt(allArgs, &i, "scroll offset required")
|
||||||
case "--filepath-word":
|
case "--filepath-word":
|
||||||
opts.FileWord = true
|
opts.FileWord = true
|
||||||
case "--no-filepath-word":
|
case "--no-filepath-word":
|
||||||
@@ -1421,6 +1436,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
case "--header-lines":
|
case "--header-lines":
|
||||||
opts.HeaderLines = atoi(
|
opts.HeaderLines = atoi(
|
||||||
nextString(allArgs, &i, "number of header lines required"))
|
nextString(allArgs, &i, "number of header lines required"))
|
||||||
|
case "--header-first":
|
||||||
|
opts.HeaderFirst = true
|
||||||
|
case "--no-header-first":
|
||||||
|
opts.HeaderFirst = false
|
||||||
case "--preview":
|
case "--preview":
|
||||||
opts.Preview.command = nextString(allArgs, &i, "preview command required")
|
opts.Preview.command = nextString(allArgs, &i, "preview command required")
|
||||||
case "--no-preview":
|
case "--no-preview":
|
||||||
@@ -1530,6 +1549,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
opts.Tabstop = atoi(value)
|
opts.Tabstop = atoi(value)
|
||||||
} else if match, value := optString(arg, "--hscroll-off="); match {
|
} else if match, value := optString(arg, "--hscroll-off="); match {
|
||||||
opts.HscrollOff = atoi(value)
|
opts.HscrollOff = atoi(value)
|
||||||
|
} else if match, value := optString(arg, "--scroll-off="); match {
|
||||||
|
opts.ScrollOff = atoi(value)
|
||||||
} else if match, value := optString(arg, "--jump-labels="); match {
|
} else if match, value := optString(arg, "--jump-labels="); match {
|
||||||
opts.JumpLabels = value
|
opts.JumpLabels = value
|
||||||
validateJumpLabels = true
|
validateJumpLabels = true
|
||||||
@@ -1547,6 +1568,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
errorExit("hscroll offset must be a non-negative integer")
|
errorExit("hscroll offset must be a non-negative integer")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.ScrollOff < 0 {
|
||||||
|
errorExit("scroll offset must be a non-negative integer")
|
||||||
|
}
|
||||||
|
|
||||||
if opts.Tabstop < 1 {
|
if opts.Tabstop < 1 {
|
||||||
errorExit("tab stop must be a positive integer")
|
errorExit("tab stop must be a positive integer")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ type Terminal struct {
|
|||||||
keepRight bool
|
keepRight bool
|
||||||
hscroll bool
|
hscroll bool
|
||||||
hscrollOff int
|
hscrollOff int
|
||||||
|
scrollOff int
|
||||||
wordRubout string
|
wordRubout string
|
||||||
wordNext string
|
wordNext string
|
||||||
cx int
|
cx int
|
||||||
@@ -139,6 +140,8 @@ type Terminal struct {
|
|||||||
printQuery bool
|
printQuery bool
|
||||||
history *History
|
history *History
|
||||||
cycle bool
|
cycle bool
|
||||||
|
headerFirst bool
|
||||||
|
headerLines int
|
||||||
header []string
|
header []string
|
||||||
header0 []string
|
header0 []string
|
||||||
ansi bool
|
ansi bool
|
||||||
@@ -502,6 +505,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
keepRight: opts.KeepRight,
|
keepRight: opts.KeepRight,
|
||||||
hscroll: opts.Hscroll,
|
hscroll: opts.Hscroll,
|
||||||
hscrollOff: opts.HscrollOff,
|
hscrollOff: opts.HscrollOff,
|
||||||
|
scrollOff: opts.ScrollOff,
|
||||||
wordRubout: wordRubout,
|
wordRubout: wordRubout,
|
||||||
wordNext: wordNext,
|
wordNext: wordNext,
|
||||||
cx: len(input),
|
cx: len(input),
|
||||||
@@ -527,6 +531,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
paused: opts.Phony,
|
paused: opts.Phony,
|
||||||
strong: strongAttr,
|
strong: strongAttr,
|
||||||
cycle: opts.Cycle,
|
cycle: opts.Cycle,
|
||||||
|
headerFirst: opts.HeaderFirst,
|
||||||
|
headerLines: opts.HeaderLines,
|
||||||
header: header,
|
header: header,
|
||||||
header0: header,
|
header0: header,
|
||||||
ansi: opts.Ansi,
|
ansi: opts.Ansi,
|
||||||
@@ -974,12 +980,23 @@ func (t *Terminal) updatePromptOffset() ([]rune, []rune) {
|
|||||||
return before, after
|
return before, after
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) promptLine() int {
|
||||||
|
if t.headerFirst {
|
||||||
|
max := t.window.Height() - 1
|
||||||
|
if !t.noInfoLine() {
|
||||||
|
max--
|
||||||
|
}
|
||||||
|
return util.Min(len(t.header0)+t.headerLines, max)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Terminal) placeCursor() {
|
func (t *Terminal) placeCursor() {
|
||||||
t.move(0, t.promptLen+t.queryLen[0], false)
|
t.move(t.promptLine(), t.promptLen+t.queryLen[0], false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) printPrompt() {
|
func (t *Terminal) printPrompt() {
|
||||||
t.move(0, 0, true)
|
t.move(t.promptLine(), 0, true)
|
||||||
t.prompt()
|
t.prompt()
|
||||||
|
|
||||||
before, after := t.updatePromptOffset()
|
before, after := t.updatePromptOffset()
|
||||||
@@ -1001,22 +1018,23 @@ func (t *Terminal) trimMessage(message string, maxWidth int) string {
|
|||||||
|
|
||||||
func (t *Terminal) printInfo() {
|
func (t *Terminal) printInfo() {
|
||||||
pos := 0
|
pos := 0
|
||||||
|
line := t.promptLine()
|
||||||
switch t.infoStyle {
|
switch t.infoStyle {
|
||||||
case infoDefault:
|
case infoDefault:
|
||||||
t.move(1, 0, true)
|
t.move(line+1, 0, true)
|
||||||
if t.reading {
|
if t.reading {
|
||||||
duration := int64(spinnerDuration)
|
duration := int64(spinnerDuration)
|
||||||
idx := (time.Now().UnixNano() % (duration * int64(len(t.spinner)))) / duration
|
idx := (time.Now().UnixNano() % (duration * int64(len(t.spinner)))) / duration
|
||||||
t.window.CPrint(tui.ColSpinner, t.spinner[idx])
|
t.window.CPrint(tui.ColSpinner, t.spinner[idx])
|
||||||
}
|
}
|
||||||
t.move(1, 2, false)
|
t.move(line+1, 2, false)
|
||||||
pos = 2
|
pos = 2
|
||||||
case infoInline:
|
case infoInline:
|
||||||
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
|
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
|
||||||
if pos+len(" < ") > t.window.Width() {
|
if pos+len(" < ") > t.window.Width() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.move(0, pos, true)
|
t.move(line, pos, true)
|
||||||
if t.reading {
|
if t.reading {
|
||||||
t.window.CPrint(tui.ColSpinner, " < ")
|
t.window.CPrint(tui.ColSpinner, " < ")
|
||||||
} else {
|
} else {
|
||||||
@@ -1059,11 +1077,20 @@ func (t *Terminal) printHeader() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
max := t.window.Height()
|
max := t.window.Height()
|
||||||
|
if t.headerFirst {
|
||||||
|
max--
|
||||||
|
if !t.noInfoLine() {
|
||||||
|
max--
|
||||||
|
}
|
||||||
|
}
|
||||||
var state *ansiState
|
var state *ansiState
|
||||||
for idx, lineStr := range t.header {
|
for idx, lineStr := range t.header {
|
||||||
line := idx + 2
|
line := idx
|
||||||
if t.noInfoLine() {
|
if !t.headerFirst {
|
||||||
line--
|
line++
|
||||||
|
if !t.noInfoLine() {
|
||||||
|
line++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if line >= max {
|
if line >= max {
|
||||||
continue
|
continue
|
||||||
@@ -2642,7 +2669,7 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if me.Down {
|
} else if me.Down {
|
||||||
if my == 0 && mx >= 0 {
|
if my == t.promptLine() && mx >= 0 {
|
||||||
// Prompt
|
// Prompt
|
||||||
t.cx = mx + t.xoffset
|
t.cx = mx + t.xoffset
|
||||||
} else if my >= min {
|
} else if my >= min {
|
||||||
@@ -2673,6 +2700,7 @@ func (t *Terminal) Loop() {
|
|||||||
command := t.replacePlaceholder(a.a, false, string(t.input), list)
|
command := t.replacePlaceholder(a.a, false, string(t.input), list)
|
||||||
newCommand = &command
|
newCommand = &command
|
||||||
t.reading = true
|
t.reading = true
|
||||||
|
t.version++
|
||||||
}
|
}
|
||||||
case actUnbind:
|
case actUnbind:
|
||||||
keys := parseKeyChords(a.a, "PANIC")
|
keys := parseKeyChords(a.a, "PANIC")
|
||||||
@@ -2748,9 +2776,26 @@ func (t *Terminal) constrain() {
|
|||||||
|
|
||||||
t.cy = util.Constrain(t.cy, 0, count-1)
|
t.cy = util.Constrain(t.cy, 0, count-1)
|
||||||
|
|
||||||
minOffset := t.cy - height + 1
|
minOffset := util.Max(t.cy-height+1, 0)
|
||||||
maxOffset := util.Max(util.Min(count-height, t.cy), 0)
|
maxOffset := util.Max(util.Min(count-height, t.cy), 0)
|
||||||
t.offset = util.Constrain(t.offset, minOffset, maxOffset)
|
t.offset = util.Constrain(t.offset, minOffset, maxOffset)
|
||||||
|
if t.scrollOff == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollOff := util.Min(height/2, t.scrollOff)
|
||||||
|
for {
|
||||||
|
prevOffset := t.offset
|
||||||
|
if t.cy-t.offset < scrollOff {
|
||||||
|
t.offset = util.Max(minOffset, t.offset-1)
|
||||||
|
}
|
||||||
|
if t.cy-t.offset >= height-scrollOff {
|
||||||
|
t.offset = util.Min(maxOffset, t.offset+1)
|
||||||
|
}
|
||||||
|
if t.offset == prevOffset {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) vmove(o int, allowCycle bool) {
|
func (t *Terminal) vmove(o int, allowCycle bool) {
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ func TestUnixCommands(t *testing.T) {
|
|||||||
{give{`grep {} ~/test`, ``, newItems(`~`)}, want{output: `grep '~' ~/test`}},
|
{give{`grep {} ~/test`, ``, newItems(`~`)}, want{output: `grep '~' ~/test`}},
|
||||||
|
|
||||||
// 2) problematic examples
|
// 2) problematic examples
|
||||||
|
// (not necessarily unexpected)
|
||||||
|
|
||||||
// paths that need to expand some part of it won't work (special characters and variables)
|
// paths that need to expand some part of it won't work (special characters and variables)
|
||||||
{give{`cat {}`, ``, newItems(`~/test`)}, want{output: `cat '~/test'`}},
|
{give{`cat {}`, ``, newItems(`~/test`)}, want{output: `cat '~/test'`}},
|
||||||
@@ -315,6 +316,7 @@ func TestWindowsCommands(t *testing.T) {
|
|||||||
{give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- ^"\^"C:\\test.txt\^"^"`}},
|
{give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- ^"\^"C:\\test.txt\^"^"`}},
|
||||||
|
|
||||||
// 2) problematic examples
|
// 2) problematic examples
|
||||||
|
// (not necessarily unexpected)
|
||||||
|
|
||||||
// notepad++'s parser can't handle `-n"12"` generate by fzf, expects `-n12`
|
// notepad++'s parser can't handle `-n"12"` generate by fzf, expects `-n12`
|
||||||
{give{`notepad++ -n{1} {2}`, ``, newItems(`12 C:\Work\Test Folder\File.txt`)}, want{output: `notepad++ -n^"12^" ^"C:\\Work\\Test Folder\\File.txt^"`}},
|
{give{`notepad++ -n{1} {2}`, ``, newItems(`12 C:\Work\Test Folder\File.txt`)}, want{output: `notepad++ -n^"12^" ^"C:\\Work\\Test Folder\\File.txt^"`}},
|
||||||
@@ -327,11 +329,97 @@ func TestWindowsCommands(t *testing.T) {
|
|||||||
|
|
||||||
// the "file" flag in the pattern won't create *.bat or *.cmd file so the command in the output tries to edit the file, instead of executing it
|
// the "file" flag in the pattern won't create *.bat or *.cmd file so the command in the output tries to edit the file, instead of executing it
|
||||||
// the temp file contains: `cat "C:\test.txt"`
|
// the temp file contains: `cat "C:\test.txt"`
|
||||||
|
// TODO this should actually work
|
||||||
{give{`cmd /c {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^cmd /c .*\fzf-preview-[0-9]{9}$`}},
|
{give{`cmd /c {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^cmd /c .*\fzf-preview-[0-9]{9}$`}},
|
||||||
}
|
}
|
||||||
testCommands(t, tests)
|
testCommands(t, tests)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows in Powershell
|
||||||
|
func TestPowershellCommands(t *testing.T) {
|
||||||
|
if !util.IsWindows() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
// reference: give{template, query, items}, want{output OR match}
|
||||||
|
|
||||||
|
/*
|
||||||
|
You can read each line in the following table as a pipeline that
|
||||||
|
consist of series of parsers that act upon your input (col. 1) and
|
||||||
|
each cell represents the output value.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
- exec.Command("program.exe", `\''`)
|
||||||
|
- goes to win32 api which will process it transparently as it contains no special characters, see [CommandLineToArgvW][].
|
||||||
|
- powershell command will receive it as is, that is two arguments: a literal backslash and empty string in single quotes
|
||||||
|
- native command run via/from powershell will receive only one argument: a literal backslash. Because extra parsing rules apply, see [NativeCallsFromPowershell][].
|
||||||
|
- some¹ apps have internal parser, that requires one more level of escaping (yes, this is completely application-specific, but see terminal_test.go#TestWindowsCommands)
|
||||||
|
|
||||||
|
Character⁰ CommandLineToArgvW Powershell commands Native commands from Powershell Apps requiring escapes¹ | Being tested below
|
||||||
|
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
|
||||||
|
" empty string² missing argument error ... ... |
|
||||||
|
\" literal " unbalanced quote error ... ... |
|
||||||
|
'\"' literal '"' literal " empty string empty string (match all) | yes
|
||||||
|
'\\\"' literal '\"' literal \" literal " literal " |
|
||||||
|
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
|
||||||
|
\ transparent transparent transparent regex error |
|
||||||
|
'\' transparent literal \ literal \ regex error | yes
|
||||||
|
\\ transparent transparent transparent literal \ |
|
||||||
|
'\\' transparent literal \\ literal \\ literal \ |
|
||||||
|
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
|
||||||
|
' transparent unbalanced quote error ... ... |
|
||||||
|
\' transparent literal \ and unb. quote error ... ... |
|
||||||
|
\'' transparent literal \ and empty string literal \ regex error | no, but given as example above
|
||||||
|
''' transparent unbalanced quote error ... ... |
|
||||||
|
'''' transparent literal ' literal ' literal ' | yes
|
||||||
|
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
|
||||||
|
|
||||||
|
⁰: charatecter or characters 'x' as an argument to a program in go's call: exec.Command("program.exe", `x`)
|
||||||
|
¹: native commands like grep, git grep, ripgrep
|
||||||
|
²: interpreted as a grouping quote, affects argument parser and gets removed from the result
|
||||||
|
|
||||||
|
[CommandLineToArgvW]: https://docs.microsoft.com/en-gb/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
|
||||||
|
[NativeCallsFromPowershell]: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing?view=powershell-7.1#passing-arguments-that-contain-quote-characters
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 1) working examples
|
||||||
|
|
||||||
|
{give{`Get-Content {}`, ``, newItems(`C:\test.txt`)}, want{output: `Get-Content 'C:\test.txt'`}},
|
||||||
|
{give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" '.\test.go'`}},
|
||||||
|
|
||||||
|
// example of escaping single quotes
|
||||||
|
{give{`rg -- {}`, ``, newItems(`'foobar'`)}, want{output: `rg -- '''foobar'''`}},
|
||||||
|
|
||||||
|
// chaining powershells
|
||||||
|
{give{`powershell -NoProfile -Command {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `powershell -NoProfile -Command 'cat \"C:\test.txt\"'`}},
|
||||||
|
|
||||||
|
// 2) problematic examples
|
||||||
|
// (not necessarily unexpected)
|
||||||
|
|
||||||
|
// looking for a path string will only work with escaped backslashes
|
||||||
|
{give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- 'C:\test.txt'`}},
|
||||||
|
// looking for a literal double quote will only work with triple escaped double quotes
|
||||||
|
{give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- '\"C:\test.txt\"'`}},
|
||||||
|
|
||||||
|
// Get-Content (i.e. cat alias) is parsing `"` as a part of the file path, returns an error:
|
||||||
|
// Get-Content : Cannot find drive. A drive with the name '"C:' does not exist.
|
||||||
|
{give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat '\"C:\test.txt\"'`}},
|
||||||
|
|
||||||
|
// the "file" flag in the pattern won't create *.ps1 file so the powershell will offload this "unknown" filetype
|
||||||
|
// to explorer, which will prompt user to pick editing program for the fzf-preview file
|
||||||
|
// the temp file contains: `cat "C:\test.txt"`
|
||||||
|
// TODO this should actually work
|
||||||
|
{give{`powershell -NoProfile -Command {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^powershell -NoProfile -Command .*\fzf-preview-[0-9]{9}$`}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// to force powershell-style escaping we temporarily set environment variable that fzf honors
|
||||||
|
shellBackup := os.Getenv("SHELL")
|
||||||
|
os.Setenv("SHELL", "powershell")
|
||||||
|
testCommands(t, tests)
|
||||||
|
os.Setenv("SHELL", shellBackup)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Test typical valid placeholders and parsing of them.
|
Test typical valid placeholders and parsing of them.
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,25 @@ func notifyOnCont(resizeChan chan<- os.Signal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func quoteEntry(entry string) string {
|
func quoteEntry(entry string) string {
|
||||||
escaped := strings.Replace(entry, `\`, `\\`, -1)
|
shell := os.Getenv("SHELL")
|
||||||
escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
|
if len(shell) == 0 {
|
||||||
r, _ := regexp.Compile(`[&|<>()@^%!"]`)
|
shell = "cmd"
|
||||||
return r.ReplaceAllStringFunc(escaped, func(match string) string {
|
}
|
||||||
return "^" + match
|
|
||||||
})
|
if strings.Contains(shell, "cmd") {
|
||||||
|
// backslash escaping is done here for applications
|
||||||
|
// (see ripgrep test case in terminal_test.go#TestWindowsCommands)
|
||||||
|
escaped := strings.Replace(entry, `\`, `\\`, -1)
|
||||||
|
escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
|
||||||
|
// caret is the escape character for cmd shell
|
||||||
|
r, _ := regexp.Compile(`[&|<>()@^%!"]`)
|
||||||
|
return r.ReplaceAllStringFunc(escaped, func(match string) string {
|
||||||
|
return "^" + match
|
||||||
|
})
|
||||||
|
} else if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
|
||||||
|
escaped := strings.Replace(entry, `"`, `\"`, -1)
|
||||||
|
return "'" + strings.Replace(escaped, "'", "''", -1) + "'"
|
||||||
|
} else {
|
||||||
|
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,57 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExecCommand executes the given command with cmd
|
var shellPath atomic.Value
|
||||||
|
|
||||||
|
// ExecCommand executes the given command with $SHELL
|
||||||
func ExecCommand(command string, setpgid bool) *exec.Cmd {
|
func ExecCommand(command string, setpgid bool) *exec.Cmd {
|
||||||
return ExecCommandWith("cmd", command, setpgid)
|
var shell string
|
||||||
|
if cached := shellPath.Load(); cached != nil {
|
||||||
|
shell = cached.(string)
|
||||||
|
} else {
|
||||||
|
shell = os.Getenv("SHELL")
|
||||||
|
if len(shell) == 0 {
|
||||||
|
shell = "cmd"
|
||||||
|
} else if strings.Contains(shell, "/") {
|
||||||
|
out, err := exec.Command("cygpath", "-w", shell).Output()
|
||||||
|
if err == nil {
|
||||||
|
shell = strings.Trim(string(out), "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shellPath.Store(shell)
|
||||||
|
}
|
||||||
|
return ExecCommandWith(shell, command, setpgid)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecCommandWith executes the given command with cmd. _shell parameter is
|
// ExecCommandWith executes the given command with the specified shell
|
||||||
// ignored on Windows.
|
|
||||||
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
|
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
|
||||||
// can kill preview process with its child processes at once.
|
// can kill preview process with its child processes at once.
|
||||||
func ExecCommandWith(_shell string, command string, setpgid bool) *exec.Cmd {
|
// NOTE: For "powershell", we should ideally set output encoding to UTF8,
|
||||||
cmd := exec.Command("cmd")
|
// but it is left as is now because no adverse effect has been observed.
|
||||||
|
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if strings.Contains(shell, "cmd") {
|
||||||
|
cmd = exec.Command(shell)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
HideWindow: false,
|
||||||
|
CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command),
|
||||||
|
CreationFlags: 0,
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
|
||||||
|
cmd = exec.Command(shell, "-NoProfile", "-Command", command)
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command(shell, "-c", command)
|
||||||
|
}
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
HideWindow: false,
|
HideWindow: false,
|
||||||
CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command),
|
|
||||||
CreationFlags: 0,
|
CreationFlags: 0,
|
||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
|
|||||||
@@ -2069,6 +2069,79 @@ class TestGoFZF < TestBase
|
|||||||
tmux.send_keys :Up
|
tmux.send_keys :Up
|
||||||
tmux.until { |lines| assert_includes lines[1], '[[99]]' }
|
tmux.until { |lines| assert_includes lines[1], '[[99]]' }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_reload_should_update_preview
|
||||||
|
tmux.send_keys "seq 3 | #{FZF} --bind 'ctrl-t:reload:echo 4' --preview 'echo {}' --preview-window 'nohidden'", :Enter
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '1' }
|
||||||
|
tmux.send_keys 'C-t'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '4' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_scroll_off
|
||||||
|
tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 1000, lines.item_count }
|
||||||
|
height = tmux.until { |lines| lines }.first.to_i
|
||||||
|
tmux.send_keys :PgUp
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal height + 3, lines.first.to_i
|
||||||
|
assert_equal "> #{height}", lines[3].strip
|
||||||
|
end
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_equal "> #{height + 1}", lines[3].strip }
|
||||||
|
tmux.send_keys 'l'
|
||||||
|
tmux.until { |lines| assert_equal '> 1000', lines.first.strip }
|
||||||
|
tmux.send_keys :PgDn
|
||||||
|
tmux.until { |lines| assert_equal "> #{1000 - height + 1}", lines.reverse[5].strip }
|
||||||
|
tmux.send_keys :Down
|
||||||
|
tmux.until { |lines| assert_equal "> #{1000 - height}", lines.reverse[5].strip }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_scroll_off_large
|
||||||
|
tmux.send_keys "seq 1000 | #{FZF} --scroll-off=9999", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 1000, lines.item_count }
|
||||||
|
height = tmux.until { |lines| lines }.first.to_i
|
||||||
|
tmux.send_keys :PgUp
|
||||||
|
tmux.until { |lines| assert_equal "> #{height}", lines[height / 2].strip }
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_equal "> #{height + 1}", lines[height / 2].strip }
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_equal "> #{height + 2}", lines[height / 2].strip }
|
||||||
|
tmux.send_keys :Down
|
||||||
|
tmux.until { |lines| assert_equal "> #{height + 1}", lines[height / 2].strip }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_first
|
||||||
|
tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first", :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
> 4
|
||||||
|
997/997
|
||||||
|
>
|
||||||
|
3
|
||||||
|
2
|
||||||
|
1
|
||||||
|
foobar
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
assert_equal expected.chomp, lines.reverse.take(7).reverse.join("\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_first_reverse
|
||||||
|
tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first --reverse --inline-info", :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
foobar
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
> < 997/997
|
||||||
|
> 4
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
assert_equal expected.chomp, lines.take(6).join("\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module TestShell
|
module TestShell
|
||||||
|
|||||||
Reference in New Issue
Block a user