Compare commits

..

20 Commits

Author SHA1 Message Date
Junegunn Choi
0012183ede 0.60.3 2025-03-03 17:10:49 +09:00
Junegunn Choi
8916cbc6ab [windows] Prevent fzf from consuming user input while paused
This partly fixes #4260.

fzf still can consume the first key stroke.
2025-03-03 14:04:16 +09:00
junegunn
21ce70054f Deploying to master from @ junegunn/fzf@3ba82b6d87 🚀 2025-03-02 00:02:11 +00:00
Junegunn Choi
3ba82b6d87 Make truncateQuery faster
https://github.com/junegunn/fzf/issues/4292#issuecomment-2687051731
2025-02-27 15:49:15 +09:00
Junegunn Choi
e771c5d057 Update README 2025-02-27 14:01:13 +09:00
Junegunn Choi
4e5e925e39 Increase the query length limit from 300 to 1000
Close #4292
2025-02-27 11:43:58 +09:00
Junegunn Choi
b7248d4115 Remove temp files before 'become' when using --tmux option
Close #4283

But the temp files for the `f` flags in the 'become' template will not
be removed, because we will need them after "becoming" another program.

  e.g. fzf --bind 'enter:become:cat {f}'
2025-02-26 20:47:09 +09:00
Junegunn Choi
639253840f Trim trailing whitespaces after processing ANSI sequences
Close #4282
2025-02-26 16:17:12 +09:00
Junegunn Choi
710ebdf9c1 Make --accept-nth compatible with --select-1
Fix #4287
2025-02-26 00:25:23 +09:00
bitraid
bb64d84ce4 [fish] Enable multiple history commands insertion (#4280)
Enable inserting multiple history commands. To disable, set `--no-multi`
through `$FZF_CTRL_R_OPTS`.

Also, remove the usage of `become` action, to avoid leaving behind
temporary files.

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2025-02-26 00:18:56 +09:00
alex-huff
cd1da27ff2 Fix condition for using item numlines cache (#4285) 2025-02-25 20:25:26 +09:00
Junegunn Choi
c1accc2e5b Use '/' as path separator on MSYS2
Fix #4281
2025-02-25 10:12:19 +09:00
Junegunn Choi
e4489dcbc1 Fix regression: Trim trailing whitespaces when using --with-nth
https://github.com/junegunn/fzf/issues/4272#issuecomment-2677279620
2025-02-24 18:40:13 +09:00
Junegunn Choi
c0d407f7ce 0.60.2 2025-02-23 19:52:57 +09:00
Junegunn Choi
461115afde Add support for {n} in --with-nth and --accept-nth templates
Close #4275
2025-02-23 19:47:56 +09:00
junegunn
bae1965231 Deploying to master from @ junegunn/fzf@b89c77ec9a 🚀 2025-02-23 00:02:08 +00:00
Junegunn Choi
b89c77ec9a Mention that actions after accept or abort are ignored (#4271) 2025-02-22 22:19:16 +09:00
Junegunn Choi
1ca5f09d7b Explain the difference of template from a single field index expression
Close #4272
2025-02-22 22:14:49 +09:00
Junegunn Choi
d79902ae59 Fix 'jump' when pointer is empty
Fix #4270
2025-02-22 19:05:30 +09:00
phanium
77568e114f Don't trim last field when delimiter is regex (#4266) 2025-02-21 22:21:55 +09:00
20 changed files with 220 additions and 74 deletions

View File

@@ -1,6 +1,25 @@
CHANGELOG CHANGELOG
========= =========
0.60.3
------
- Bug fixes and improvements
- [fish] Enable multiple history commands insertion (#4280) (@bitraid)
- [walker] Append '/' to directory entries on MSYS2 (#4281)
- Trim trailing whitespaces after processing ANSI sequences (#4282)
- Remove temp files before `become` when using `--tmux` option (#4283)
- Fix condition for using item numlines cache (#4285) (@alex-huff)
- Make `--accept-nth` compatible with `--select-1` (#4287)
- Increase the query length limit from 300 to 1000 (#4292)
- [windows] Prevent fzf from consuming user input while paused (#4260)
0.60.2
------
- Template for `--with-nth` and `--accept-nth` now supports `{n}` which evaluates to the zero-based ordinal index of the item
- Fixed a regression that caused the last field in the "nth" expression to be trimmed when a regular expression delimiter is used
- Thanks to @phanen for the fix
- Fixed 'jump' action when the pointer is an empty string
0.60.1 0.60.1
------ ------
- Bug fixes and minor improvements - Bug fixes and minor improvements

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.60.1 version=0.60.3
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2

View File

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

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf\-tmux 1 "Feb 2025" "fzf 0.60.1" "fzf\-tmux - open fzf in tmux split pane" .TH fzf\-tmux 1 "Mar 2025" "fzf 0.60.3" "fzf\-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf\-tmux - open fzf in tmux split pane fzf\-tmux - open fzf in tmux split pane

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Feb 2025" "fzf 0.60.1" "fzf - a command-line fuzzy finder" .TH fzf 1 "Mar 2025" "fzf 0.60.3" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -122,7 +122,9 @@ fields.
.BI "\-\-with\-nth=" "N[,..] or TEMPLATE" .BI "\-\-with\-nth=" "N[,..] or TEMPLATE"
Transform the presentation of each line using the field index expressions. Transform the presentation of each line using the field index expressions.
For advanced transformation, you can provide a template containing field index For advanced transformation, you can provide a template containing field index
expressions in curly braces. expressions in curly braces. When you use a template, the trailing delimiter is
stripped from each expression, giving you more control over the output.
\fB{n}\fR in template evaluates to the zero-based ordinal index of the line.
.RS .RS
e.g. e.g.
@@ -130,13 +132,16 @@ e.g.
echo foo bar baz | fzf --with-nth 2.. echo foo bar baz | fzf --with-nth 2..
# Use template to rearrange fields # Use template to rearrange fields
echo foo,bar,baz | fzf --delimiter , --with-nth '{1},{3},{2},{1..2}' echo foo,bar,baz | fzf --delimiter , --with-nth '{n},{1},{3},{2},{1..2}'
.RE .RE
.TP .TP
.BI "\-\-accept\-nth=" "N[,..] or TEMPLATE" .BI "\-\-accept\-nth=" "N[,..] or TEMPLATE"
Define which fields to print on accept. The last delimiter is stripped from the Define which fields to print on accept. The last delimiter is stripped from the
output. For advanced transformation, you can provide a template containing output. For advanced transformation, you can provide a template containing
field index expressions in curly braces. field index expressions in curly braces. When you use a template, the trailing
delimiter is stripped from each expression, giving you more control over the
output. \fB{n}\fR in template evaluates to the zero-based ordinal index of the
line.
.RS .RS
e.g. e.g.
@@ -144,7 +149,7 @@ e.g.
echo foo bar baz | fzf --accept-nth 2 echo foo bar baz | fzf --accept-nth 2
# Template # Template
echo foo bar baz | fzf --accept-nth '1st: {1}, 2nd: {2}, 3rd: {3}' echo foo bar baz | fzf --accept-nth 'Index: {n}, 1st: {1}, 2nd: {2}, 3rd: {3}'
.RE .RE
.TP .TP
.B "+s, \-\-no\-sort" .B "+s, \-\-no\-sort"
@@ -1719,6 +1724,9 @@ e.g.
\fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all+accept'\fR \fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all+accept'\fR
\fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all' \-\-bind 'ctrl\-a:+accept'\fR \fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all' \-\-bind 'ctrl\-a:+accept'\fR
Any action after a terminal action that exits fzf, such as \fBaccept\fR or
\fBabort\fR, is ignored.
.SS ACTION ARGUMENT .SS ACTION ARGUMENT
An action denoted with \fB(...)\fR suffix takes an argument. An action denoted with \fB(...)\fR suffix takes an argument.

View File

@@ -116,10 +116,9 @@ function fzf_key_bindings
set -l fzf_query (commandline | string escape) set -l fzf_query (commandline | string escape)
set -lx FZF_DEFAULT_OPTS (__fzf_defaults '' \ set -lx FZF_DEFAULT_OPTS (__fzf_defaults '' \
'--nth=2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign="\t↳ "' \ '--nth=2..,.. --scheme=history --multi --wrap-sign="\t↳ "' \
"--highlight-line --no-multi $FZF_CTRL_R_OPTS --read0 --print0" \ "--bind=ctrl-r:toggle-sort --highlight-line $FZF_CTRL_R_OPTS" \
"--bind='enter:become:string replace -a -- \n\t \n {2..} | string collect'" \ '--accept-nth=2.. --read0 --print0 --with-shell='(status fish-path)\\ -c)
'--with-shell='(status fish-path)\\ -c)
set -lx FZF_DEFAULT_OPTS_FILE set -lx FZF_DEFAULT_OPTS_FILE
set -lx FZF_DEFAULT_COMMAND set -lx FZF_DEFAULT_COMMAND
@@ -138,8 +137,12 @@ function fzf_key_bindings
# Merge history from other sessions before searching # Merge history from other sessions before searching
test -z "$fish_private_mode"; and builtin history merge test -z "$fish_private_mode"; and builtin history merge
set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query) if set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query | string split0)
and commandline -- $result commandline -- (string replace -a -- \n\t \n $result[1])
test (count $result) -gt 1; and for i in $result[2..-1]
commandline -i -- (string replace -a -- \n\t \n \n$i)
end
end
commandline -f repaint commandline -f repaint
end end

View File

@@ -26,7 +26,7 @@ const (
previewCancelWait = 500 * time.Millisecond previewCancelWait = 500 * time.Millisecond
previewChunkDelay = 100 * time.Millisecond previewChunkDelay = 100 * time.Millisecond
previewDelayed = 500 * time.Millisecond previewDelayed = 500 * time.Millisecond
maxPatternLength = 300 maxPatternLength = 1000
maxMulti = math.MaxInt32 maxMulti = math.MaxInt32
// Matcher // Matcher

View File

@@ -128,13 +128,14 @@ func Run(opts *Options) (int, error) {
} }
} }
} }
transformed := nthTransformer(tokens) transformed := nthTransformer(tokens, itemIndex)
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, transformed) header = append(header, transformed)
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return false return false
} }
item.text, item.colors = ansiProcessor(stringBytes(transformed)) item.text, item.colors = ansiProcessor(stringBytes(transformed))
item.text.TrimTrailingWhitespaces()
item.text.Index = itemIndex item.text.Index = itemIndex
item.origText = &data item.origText = &data
itemIndex++ itemIndex++
@@ -476,8 +477,17 @@ func Run(opts *Options) (int, error) {
if len(opts.Expect) > 0 { if len(opts.Expect) > 0 {
opts.Printer("") opts.Printer("")
} }
transformer := func(item *Item) string {
return item.AsString(opts.Ansi)
}
if opts.AcceptNth != nil {
fn := opts.AcceptNth(opts.Delimiter)
transformer = func(item *Item) string {
return item.acceptNth(opts.Ansi, opts.Delimiter, fn)
}
}
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
opts.Printer(val.Get(i).item.AsString(opts.Ansi)) opts.Printer(transformer(val.Get(i).item))
} }
if count == 0 { if count == 0 {
exitCode = ExitNoMatch exitCode = ExitNoMatch

View File

@@ -51,3 +51,9 @@ func (item *Item) AsString(stripAnsi bool) string {
} }
return item.text.ToString() return item.text.ToString()
} }
func (item *Item) acceptNth(stripAnsi bool, delimiter Delimiter, transformer func([]Token, int32) string) string {
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
transformed := transformer(tokens, item.Index())
return StripLastDelimiter(transformed, delimiter)
}

View File

@@ -544,8 +544,8 @@ type Options struct {
Case Case Case Case
Normalize bool Normalize bool
Nth []Range Nth []Range
WithNth func(Delimiter) func([]Token) string WithNth func(Delimiter) func([]Token, int32) string
AcceptNth func(Delimiter) func([]Token) string AcceptNth func(Delimiter) func([]Token, int32) string
Delimiter Delimiter Delimiter Delimiter
Sort int Sort int
Track trackOption Track trackOption
@@ -769,22 +769,22 @@ func splitNth(str string) ([]Range, error) {
return ranges, nil return ranges, nil
} }
func nthTransformer(str string) (func(Delimiter) func([]Token) string, error) { func nthTransformer(str string) (func(Delimiter) func([]Token, int32) string, error) {
// ^[0-9,-.]+$" // ^[0-9,-.]+$"
if match, _ := regexp.MatchString("^[0-9,-.]+$", str); match { if match, _ := regexp.MatchString("^[0-9,-.]+$", str); match {
nth, err := splitNth(str) nth, err := splitNth(str)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return func(Delimiter) func([]Token) string { return func(Delimiter) func([]Token, int32) string {
return func(tokens []Token) string { return func(tokens []Token, index int32) string {
return JoinTokens(Transform(tokens, nth)) return JoinTokens(Transform(tokens, nth))
} }
}, nil }, nil
} }
// {...} {...} ... // {...} {...} ...
placeholder := regexp.MustCompile("{[0-9,-.]+}") placeholder := regexp.MustCompile("{[0-9,-.]+}|{n}")
indexes := placeholder.FindAllStringIndex(str, -1) indexes := placeholder.FindAllStringIndex(str, -1)
if indexes == nil { if indexes == nil {
return nil, errors.New("template should include at least 1 placeholder: " + str) return nil, errors.New("template should include at least 1 placeholder: " + str)
@@ -792,6 +792,7 @@ func nthTransformer(str string) (func(Delimiter) func([]Token) string, error) {
type NthParts struct { type NthParts struct {
str string str string
index bool
nth []Range nth []Range
} }
@@ -801,7 +802,10 @@ func nthTransformer(str string) (func(Delimiter) func([]Token) string, error) {
if idx < index[0] { if idx < index[0] {
parts = append(parts, NthParts{str: str[idx:index[0]]}) parts = append(parts, NthParts{str: str[idx:index[0]]})
} }
if nth, err := splitNth(str[index[0]+1 : index[1]-1]); err == nil { expr := str[index[0]+1 : index[1]-1]
if expr == "n" {
parts = append(parts, NthParts{index: true})
} else if nth, err := splitNth(expr); err == nil {
parts = append(parts, NthParts{nth: nth}) parts = append(parts, NthParts{nth: nth})
} }
idx = index[1] idx = index[1]
@@ -810,12 +814,16 @@ func nthTransformer(str string) (func(Delimiter) func([]Token) string, error) {
parts = append(parts, NthParts{str: str[idx:]}) parts = append(parts, NthParts{str: str[idx:]})
} }
return func(delimiter Delimiter) func([]Token) string { return func(delimiter Delimiter) func([]Token, int32) string {
return func(tokens []Token) string { return func(tokens []Token, index int32) string {
str := "" str := ""
for _, holder := range parts { for _, holder := range parts {
if holder.nth != nil { if holder.nth != nil {
str += StripLastDelimiter(JoinTokens(Transform(tokens, holder.nth)), delimiter) str += StripLastDelimiter(JoinTokens(Transform(tokens, holder.nth)), delimiter)
} else if holder.index {
if index >= 0 {
str += strconv.Itoa(int(index))
}
} else { } else {
str += holder.str str += holder.str
} }

View File

@@ -59,12 +59,12 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
}) })
}() }()
var command string var command, input string
commandPrefix += ` --no-force-tty-in --proxy-script "$0"` commandPrefix += ` --no-force-tty-in --proxy-script "$0"`
if opts.Input == nil && (opts.ForceTtyIn || util.IsTty(os.Stdin)) { if opts.Input == nil && (opts.ForceTtyIn || util.IsTty(os.Stdin)) {
command = fmt.Sprintf(`%s > %q`, commandPrefix, output) command = fmt.Sprintf(`%s > %q`, commandPrefix, output)
} else { } else {
input, err := fifo("proxy-input") input, err = fifo("proxy-input")
if err != nil { if err != nil {
return ExitError, err return ExitError, err
} }
@@ -148,6 +148,9 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
if err != nil { if err != nil {
return ExitError, err return ExitError, err
} }
os.Remove(temp)
os.Remove(input)
os.Remove(output)
executor.Become(ttyin, env, command) executor.Become(ttyin, env, command)
} }
return code, err return code, err

View File

@@ -277,6 +277,9 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
ignoresFull := []string{} ignoresFull := []string{}
ignoresSuffix := []string{} ignoresSuffix := []string{}
sep := string(os.PathSeparator) sep := string(os.PathSeparator)
if _, ok := os.LookupEnv("MSYSTEM"); ok {
sep = "/"
}
for _, ignore := range ignores { for _, ignore := range ignores {
if strings.ContainsRune(ignore, os.PathSeparator) { if strings.ContainsRune(ignore, os.PathSeparator) {
if strings.HasPrefix(ignore, sep) { if strings.HasPrefix(ignore, sep) {

View File

@@ -305,7 +305,7 @@ type Terminal struct {
nthAttr tui.Attr nthAttr tui.Attr
nth []Range nth []Range
nthCurrent []Range nthCurrent []Range
acceptNth func([]Token) string acceptNth func([]Token, int32) string
tabstop int tabstop int
margin [4]sizeSpec margin [4]sizeSpec
padding [4]sizeSpec padding [4]sizeSpec
@@ -1347,7 +1347,7 @@ func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
} }
if cached, prs := t.numLinesCache[item.Index()]; prs { if cached, prs := t.numLinesCache[item.Index()]; prs {
// Can we use this cache? Let's be conservative. // Can we use this cache? Let's be conservative.
if cached.atMost >= atMost { if cached.atMost <= atMost {
return cached.numLines, false return cached.numLines, false
} }
} }
@@ -1575,9 +1575,7 @@ func (t *Terminal) output() bool {
} }
if t.acceptNth != nil { if t.acceptNth != nil {
transform = func(item *Item) string { transform = func(item *Item) string {
tokens := Tokenize(item.AsString(t.ansi), t.delimiter) return item.acceptNth(t.ansi, t.delimiter, t.acceptNth)
transformed := t.acceptNth(tokens)
return StripLastDelimiter(transformed, t.delimiter)
} }
} }
found := len(t.selected) > 0 found := len(t.selected) > 0
@@ -2288,7 +2286,11 @@ func (t *Terminal) move(y int, x int, clear bool) {
} }
func (t *Terminal) truncateQuery() { func (t *Terminal) truncateQuery() {
t.input, _ = t.trimRight(t.input, maxPatternLength) // We're limiting the length of the query not to make fzf unresponsive when
// the user accidentally pastes a huge chunk of text. Therefore, we're not
// interested in the exact display width of the query. We just limit the
// number of runes.
t.input = t.input[:util.Min(len(t.input), maxPatternLength)]
t.cx = util.Constrain(t.cx, 0, len(t.input)) t.cx = util.Constrain(t.cx, 0, len(t.input))
} }
@@ -2755,11 +2757,15 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
item := result.item item := result.item
_, selected := t.selected[item.Index()] _, selected := t.selected[item.Index()]
label := "" label := ""
extraWidth := 0
if t.jumping != jumpDisabled { if t.jumping != jumpDisabled {
if index < len(t.jumpLabels) { if index < len(t.jumpLabels) {
// Striped // Striped
current = index%2 == 0 current = index%2 == 0
label = t.jumpLabels[index:index+1] + strings.Repeat(" ", t.pointerLen-1) label = t.jumpLabels[index:index+1] + strings.Repeat(" ", util.Max(0, t.pointerLen-1))
if t.pointerLen == 0 {
extraWidth = 1
}
} }
} else if current { } else if current {
label = t.pointer label = t.pointer
@@ -2788,6 +2794,7 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1) maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
postTask := func(lineNum int, width int, wrapped bool, forceRedraw bool) { postTask := func(lineNum int, width int, wrapped bool, forceRedraw bool) {
width += extraWidth
if (current || selected) && t.highlightLine { if (current || selected) && t.highlightLine {
color := tui.ColSelected color := tui.ColSelected
if current { if current {

View File

@@ -225,9 +225,11 @@ func StripLastDelimiter(str string, delimiter Delimiter) string {
locs := delimiter.regex.FindAllStringIndex(str, -1) locs := delimiter.regex.FindAllStringIndex(str, -1)
if len(locs) > 0 { if len(locs) > 0 {
lastLoc := locs[len(locs)-1] lastLoc := locs[len(locs)-1]
if lastLoc[1] == len(str) {
str = str[:lastLoc[0]] str = str[:lastLoc[0]]
} }
} }
}
return strings.TrimRightFunc(str, unicode.IsSpace) return strings.TrimRightFunc(str, unicode.IsSpace)
} }

View File

@@ -8,6 +8,7 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"unicode/utf8" "unicode/utf8"
@@ -95,7 +96,6 @@ func (r *LightRenderer) flushRaw(sequence string) {
// Light renderer // Light renderer
type LightRenderer struct { type LightRenderer struct {
closed *util.AtomicBool
theme *ColorTheme theme *ColorTheme
mouse bool mouse bool
forceBlack bool forceBlack bool
@@ -120,6 +120,7 @@ type LightRenderer struct {
showCursor bool showCursor bool
// Windows only // Windows only
mutex sync.Mutex
ttyinChannel chan byte ttyinChannel chan byte
inHandle uintptr inHandle uintptr
outHandle uintptr outHandle uintptr
@@ -151,7 +152,6 @@ func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse
out = os.Stderr out = os.Stderr
} }
r := LightRenderer{ r := LightRenderer{
closed: util.NewAtomicBool(false),
theme: theme, theme: theme,
forceBlack: forceBlack, forceBlack: forceBlack,
mouse: mouse, mouse: mouse,
@@ -775,9 +775,8 @@ func (r *LightRenderer) Close() {
} }
r.disableMouse() r.disableMouse()
r.flush() r.flush()
r.closePlatform()
r.restoreTerminal() r.restoreTerminal()
r.closed.Set(true) r.closePlatform()
} }
func (r *LightRenderer) Top() int { func (r *LightRenderer) Top() int {

View File

@@ -18,6 +18,7 @@ const (
var ( var (
consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS) consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS)
consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN) consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN)
counter = uint64(0)
) )
// IsLightRendererSupported checks to see if the Light renderer is supported // IsLightRendererSupported checks to see if the Light renderer is supported
@@ -61,27 +62,11 @@ func (r *LightRenderer) initPlatform() error {
} }
r.inHandle = uintptr(inHandle) r.inHandle = uintptr(inHandle)
r.setupTerminal()
// channel for non-blocking reads. Buffer to make sure // channel for non-blocking reads. Buffer to make sure
// we get the ESC sets: // we get the ESC sets:
r.ttyinChannel = make(chan byte, 1024) r.ttyinChannel = make(chan byte, 1024)
// the following allows for non-blocking IO. r.setupTerminal()
// syscall.SetNonblock() is a NOOP under Windows.
go func() {
fd := int(r.inHandle)
b := make([]byte, 1)
for !r.closed.Get() {
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
_, err := util.Read(fd, b)
if err == nil {
r.ttyinChannel <- b[0]
}
}
}()
return nil return nil
} }
@@ -100,18 +85,42 @@ func openTtyOut() (*os.File, error) {
return os.Stderr, nil return os.Stderr, nil
} }
func (r *LightRenderer) setupTerminal() error { func (r *LightRenderer) setupTerminal() {
if err := windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput); err != nil { windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput)
return err windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
// The following allows for non-blocking IO.
// syscall.SetNonblock() is a NOOP under Windows.
current := counter
go func() {
fd := int(r.inHandle)
b := make([]byte, 1)
for {
if _, err := util.Read(fd, b); err == nil {
r.mutex.Lock()
// This condition prevents the goroutine from running after the renderer
// has been closed or paused.
if current != counter {
r.mutex.Unlock()
break
} }
return windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput) r.ttyinChannel <- b[0]
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
r.mutex.Unlock()
}
}
}()
} }
func (r *LightRenderer) restoreTerminal() error { func (r *LightRenderer) restoreTerminal() {
if err := windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput); err != nil { r.mutex.Lock()
return err counter++
} // We're setting ENABLE_VIRTUAL_TERMINAL_INPUT to allow escape sequences to be read during 'execute'.
return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput) // e.g. fzf --bind 'enter:execute:less {}'
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput|windows.ENABLE_VIRTUAL_TERMINAL_INPUT)
windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
r.mutex.Unlock()
} }
func (r *LightRenderer) Size() TermSize { func (r *LightRenderer) Size() TermSize {

View File

@@ -184,6 +184,11 @@ func (chars *Chars) TrailingWhitespaces() int {
return whitespaces return whitespaces
} }
func (chars *Chars) TrimTrailingWhitespaces() {
whitespaces := chars.TrailingWhitespaces()
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
}
func (chars *Chars) TrimSuffix(runes []rune) { func (chars *Chars) TrimSuffix(runes []rune) {
lastIdx := len(chars.slice) lastIdx := len(chars.slice)
firstIdx := lastIdx - len(runes) firstIdx := lastIdx - len(runes)

View File

@@ -238,6 +238,11 @@ class TestCore < TestInteractive
assert_equal %w[5555 55], fzf_output_lines assert_equal %w[5555 55], fzf_output_lines
end end
def test_select_1_accept_nth
tmux.send_keys "seq 1 100 | #{fzf(:with_nth, '..,..', :print_query, :q, 5555, :'1', :accept_nth, '"{1} // {1}"')}", :Enter
assert_equal ['5555', '55 // 55'], fzf_output_lines
end
def test_exit_0 def test_exit_0
tmux.send_keys "seq 1 100 | #{fzf(:with_nth, '..,..', :print_query, :q, 555_555, :'0')}", :Enter tmux.send_keys "seq 1 100 | #{fzf(:with_nth, '..,..', :print_query, :q, 555_555, :'0')}", :Enter
assert_equal %w[555555], fzf_output_lines assert_equal %w[555555], fzf_output_lines
@@ -827,6 +832,24 @@ class TestCore < TestInteractive
tmux.until { |lines| assert(lines.any? { it.include?('jump cancelled at 3') }) } tmux.until { |lines| assert(lines.any? { it.include?('jump cancelled at 3') }) }
end end
def test_jump_no_pointer
tmux.send_keys "seq 100 | #{FZF} --pointer= --jump-labels 12345 --bind ctrl-j:jump", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.send_keys 'C-j'
tmux.until { |lines| assert_equal '5 5', lines[-7] }
tmux.send_keys 'C-c'
tmux.until { |lines| assert_equal ' 5', lines[-7] }
end
def test_jump_no_pointer_no_marker
tmux.send_keys "seq 100 | #{FZF} --pointer= --marker= --jump-labels 12345 --bind ctrl-j:jump", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.send_keys 'C-j'
tmux.until { |lines| assert_equal '55', lines[-7] }
tmux.send_keys 'C-c'
tmux.until { |lines| assert_equal '5', lines[-7] }
end
def test_pointer def test_pointer
tmux.send_keys "seq 10 | #{fzf("--pointer '>>'")}", :Enter tmux.send_keys "seq 10 | #{fzf("--pointer '>>'")}", :Enter
# Assert that specified pointer is displayed # Assert that specified pointer is displayed
@@ -1773,12 +1796,21 @@ class TestCore < TestInteractive
end end
end end
def test_accept_nth_template def test_accept_nth_regex_delimiter_strip_last
tmux.send_keys %(echo "foo ,bar,baz" | #{FZF} -d, --accept-nth '1st: {1}, 3rd: {3}, 2nd: {2}' --sync --bind start:accept > #{tempname}), :Enter tmux.send_keys %((echo "foo:,bar:,baz"; echo "foo:,bar:,baz:,qux:,") | #{FZF} --multi --delimiter='[:,]+' --accept-nth 2.. --sync --bind 'load:select-all+accept' > #{tempname}), :Enter
wait do wait do
assert_path_exists tempname assert_path_exists tempname
# Last delimiter and the whitespaces are removed # Last delimiter and the whitespaces are removed
assert_equal ['1st: foo, 3rd: baz, 2nd: bar'], File.readlines(tempname, chomp: true) assert_equal ['bar:,baz', 'bar:,baz:,qux'], File.readlines(tempname, chomp: true)
end
end
def test_accept_nth_template
tmux.send_keys %(echo "foo ,bar,baz" | #{FZF} -d, --accept-nth '[{n}] 1st: {1}, 3rd: {3}, 2nd: {2}' --sync --bind start:accept > #{tempname}), :Enter
wait do
assert_path_exists tempname
# Last delimiter and the whitespaces are removed
assert_equal ['[0] 1st: foo, 3rd: baz, 2nd: bar'], File.readlines(tempname, chomp: true)
end end
end end
end end

View File

@@ -482,4 +482,36 @@ class TestFish < TestBase
tmux.send_keys "set -g #{name} '#{val}'", :Enter tmux.send_keys "set -g #{name} '#{val}'", :Enter
tmux.prepare tmux.prepare
end end
def test_ctrl_r_multi
tmux.send_keys ':', :Enter
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
tmux.prepare
tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter
tmux.prepare
tmux.send_keys 'C-l', 'C-r'
block = <<~BLOCK
echo "foo
bar"
echo "bar
foo"
BLOCK
tmux.until do |lines|
block.lines.each_with_index do |line, idx|
assert_includes lines[-6 + idx], line.chomp
end
end
tmux.send_keys :BTab, :BTab
tmux.until { |lines| assert_includes lines[-2], '(2)' }
tmux.send_keys :Enter
block = <<~BLOCK
echo "bar
foo"
echo "foo
bar"
BLOCK
tmux.until do |lines|
assert_equal block.lines.map(&:chomp), lines
end
end
end end