mirror of
https://github.com/junegunn/fzf.git
synced 2026-05-17 22:09:55 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 463a48c140 |
@@ -11,4 +11,4 @@ jobs:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v5
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
uses: actions/dependency-review-action@v5
|
||||
|
||||
@@ -3,19 +3,6 @@ CHANGELOG
|
||||
|
||||
0.73.0
|
||||
------
|
||||
- Timer-driven `every(N)` event for `--bind`, where `N` is seconds (fractional, floored to `0.01`). Ticks that overlap an in-flight action are coalesced, so a slow `reload` cannot accumulate a backlog.
|
||||
- New `FZF_IDLE_TIME` (whole seconds) and `FZF_IDLE_TIME_MS` (milliseconds) environment variables exported to child processes, holding the elapsed time since the last user activity. Pair with `every(N)` to build idle-based behavior such as auto-accept or auto-quit (#1211).
|
||||
```sh
|
||||
# Live process list; --track --id-nth 2 keeps the cursor on the same PID across reloads
|
||||
fzf --header-lines 1 --track --id-nth 2 --bind 'start,every(2):reload-sync:ps -ef'
|
||||
|
||||
# Auto-accept after 10 seconds of inactivity, with a countdown in the footer after 5s
|
||||
fzf --bind 'every(1):bg-transform:
|
||||
if [[ $FZF_IDLE_TIME -lt 5 ]]; then echo change-footer:
|
||||
elif [[ $FZF_IDLE_TIME -lt 10 ]]; then echo "change-footer:auto-accept in $((10 - FZF_IDLE_TIME))s"
|
||||
else echo accept
|
||||
fi'
|
||||
```
|
||||
- Bug fixes
|
||||
- `change-preview-window` no longer resets `wrap` / `wrap-word` state set via `toggle-preview-wrap` / `toggle-preview-wrap-word`. Layout fields still snap to the preset, so cycling and the empty-token reset behave as before. The new spec can still override by including `wrap` or `nowrap` explicitly. (#4791)
|
||||
|
||||
|
||||
@@ -1500,10 +1500,6 @@ fzf exports the following environment variables to its child processes.
|
||||
.br
|
||||
.BR FZF_KEY " The name of the last key pressed"
|
||||
.br
|
||||
.BR FZF_IDLE_TIME " Whole seconds since the last user activity"
|
||||
.br
|
||||
.BR FZF_IDLE_TIME_MS " Milliseconds since the last user activity"
|
||||
.br
|
||||
.BR FZF_PORT " Port number when \-\-listen option is used"
|
||||
.br
|
||||
.BR FZF_SOCK " Unix socket path when \-\-listen option is used"
|
||||
@@ -1943,30 +1939,6 @@ variables starting from 1. It optionally sets \fBFZF_CLICK_FOOTER_WORD\fR
|
||||
if clicked on a word.
|
||||
.RE
|
||||
|
||||
\fIevery(N)\fR
|
||||
.RS
|
||||
Triggered every \fIN\fR seconds (\fIN\fR can be a fractional number, e.g.
|
||||
\fB0.5\fR). The minimum interval is \fB0.01\fR seconds; values are floored
|
||||
to that.
|
||||
|
||||
Combine with the \fBFZF_IDLE_TIME\fR (whole seconds) and
|
||||
\fBFZF_IDLE_TIME_MS\fR (milliseconds) environment variables to build
|
||||
idle\-based behavior without a separate event.
|
||||
|
||||
e.g.
|
||||
\fB# Live process list, refreshed every 2 seconds.
|
||||
# --track --id-nth 2 keeps the cursor on the same PID across reloads.
|
||||
fzf \-\-header\-lines 1 \-\-track \-\-id\-nth 2 \\
|
||||
\-\-bind 'start,every(2):reload\-sync:ps \-ef'
|
||||
|
||||
# Auto\-accept after 10 seconds of inactivity, with a countdown in the footer after 5s.
|
||||
fzf \-\-bind 'every(1):bg\-transform:
|
||||
if [[ $FZF_IDLE_TIME \-lt 5 ]]; then echo change\-footer:
|
||||
elif [[ $FZF_IDLE_TIME \-lt 10 ]]; then echo "change\-footer:auto\-accept in $((10 \- FZF_IDLE_TIME))s"
|
||||
else echo accept
|
||||
fi'\fR
|
||||
.RE
|
||||
|
||||
.SS AVAILABLE ACTIONS:
|
||||
A key or an event can be bound to one or more of the following actions.
|
||||
|
||||
|
||||
+1
-1
@@ -266,7 +266,7 @@ func charClassOf(char rune) charClass {
|
||||
}
|
||||
|
||||
func bonusFor(prevClass charClass, class charClass) int16 {
|
||||
if class >= charNonWord {
|
||||
if class > charNonWord {
|
||||
switch prevClass {
|
||||
case charWhite:
|
||||
// Word boundary after whitespace
|
||||
|
||||
@@ -57,15 +57,6 @@ func TestFuzzyMatch(t *testing.T) {
|
||||
scoreMatch*4+int(bonusBoundaryDelimiter)*bonusFirstCharMultiplier+int(bonusBoundaryDelimiter)*3)
|
||||
assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+scoreGapStart+int(bonusBoundaryDelimiter))
|
||||
// Non-word character at start of input is treated as a strong boundary
|
||||
assertMatch(t, fn, false, forward, ".vimrc", ".vimrc", 0, 6,
|
||||
scoreMatch*6+int(bonusBoundaryWhite)*(bonusFirstCharMultiplier+5))
|
||||
// Non-word character right after a delimiter inherits the delimiter boundary
|
||||
assertMatch(t, fn, false, forward, "/.vimrc", ".vimrc", 1, 7,
|
||||
scoreMatch*6+int(bonusBoundaryDelimiter)*(bonusFirstCharMultiplier+5))
|
||||
// Non-word character in the middle of a word stays at bonusNonWord
|
||||
assertMatch(t, fn, false, forward, "a.vimrc", ".vimrc", 1, 7,
|
||||
scoreMatch*6+bonusBoundary*(bonusFirstCharMultiplier+5))
|
||||
assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10,
|
||||
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtension)
|
||||
assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10,
|
||||
|
||||
+1
-24
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"math"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -1258,14 +1257,7 @@ func parseKeyChords(str string, message string) (map[tui.Event]string, []tui.Eve
|
||||
add(tui.F12)
|
||||
default:
|
||||
runes := []rune(key)
|
||||
if strings.HasPrefix(lkey, "every(") && strings.HasSuffix(lkey, ")") {
|
||||
evt, err := parseEveryEvent(key[6 : len(key)-1])
|
||||
if err != nil {
|
||||
return nil, list, err
|
||||
}
|
||||
chords[evt] = key
|
||||
list = append(list, evt)
|
||||
} else if len(key) == 10 && strings.HasPrefix(lkey, "ctrl-alt-") && isAlphabet(lkey[9]) {
|
||||
if len(key) == 10 && strings.HasPrefix(lkey, "ctrl-alt-") && isAlphabet(lkey[9]) {
|
||||
r := rune(lkey[9])
|
||||
evt := tui.CtrlAltKey(r)
|
||||
if r == 'h' && !util.IsWindows() {
|
||||
@@ -1307,21 +1299,6 @@ func parseKeyChords(str string, message string) (map[tui.Event]string, []tui.Eve
|
||||
return chords, list, nil
|
||||
}
|
||||
|
||||
func parseEveryEvent(arg string) (tui.Event, error) {
|
||||
secs, err := strconv.ParseFloat(strings.TrimSpace(arg), 64)
|
||||
if err != nil || math.IsNaN(secs) || math.IsInf(secs, 0) || secs <= 0 {
|
||||
return tui.Event{}, errors.New("every() requires a positive number of seconds")
|
||||
}
|
||||
if secs < 0.01 {
|
||||
secs = 0.01
|
||||
}
|
||||
ms := math.Round(secs * 1000)
|
||||
if ms > math.MaxInt32 {
|
||||
return tui.Event{}, errors.New("every() interval is too large")
|
||||
}
|
||||
return tui.Event{Type: tui.Every, Char: rune(int32(ms))}, nil
|
||||
}
|
||||
|
||||
func parseScheme(str string) (string, []criterion, error) {
|
||||
str = strings.ToLower(str)
|
||||
switch str {
|
||||
|
||||
@@ -299,39 +299,6 @@ func TestBind(t *testing.T) {
|
||||
check(tui.F1.AsEvent(), "", actAbort)
|
||||
}
|
||||
|
||||
func TestParseEveryEvent(t *testing.T) {
|
||||
pairs, _, err := parseKeyChords("every(2),every(0.5)", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(pairs) != 2 {
|
||||
t.Errorf("expected 2 distinct every events, got %d", len(pairs))
|
||||
}
|
||||
if pairs[(tui.Event{Type: tui.Every, Char: 2000})] != "every(2)" {
|
||||
t.Errorf("every(2) not registered")
|
||||
}
|
||||
if pairs[(tui.Event{Type: tui.Every, Char: 500})] != "every(0.5)" {
|
||||
t.Errorf("every(0.5) not registered")
|
||||
}
|
||||
|
||||
// Floor at 0.01s -> 10ms
|
||||
pairs, _, err = parseKeyChords("every(0.001)", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if pairs[(tui.Event{Type: tui.Every, Char: 10})] != "every(0.001)" {
|
||||
t.Errorf("every(0.001) should floor to 10ms")
|
||||
}
|
||||
|
||||
// Reject zero, negatives, and overflow (>= 2^31 ms = ~24.85 days)
|
||||
for _, bad := range []string{"every(0)", "every(-1)", "every(abc)", "every()", "every(2147484)"} {
|
||||
if _, _, err := parseKeyChords(bad, ""); err == nil {
|
||||
t.Errorf("%s should be rejected", bad)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestColorSpec(t *testing.T) {
|
||||
var base *tui.ColorTheme
|
||||
theme := tui.Dark256
|
||||
|
||||
+1
-42
@@ -436,7 +436,6 @@ type Terminal struct {
|
||||
bgSemaphores map[action]chan struct{}
|
||||
keyChan chan tui.Event
|
||||
eventChan chan tui.Event
|
||||
timerChan chan tui.Event
|
||||
slab *util.Slab
|
||||
theme *tui.ColorTheme
|
||||
tui tui.Renderer
|
||||
@@ -457,7 +456,6 @@ type Terminal struct {
|
||||
proxyScript string
|
||||
numLinesCache map[int32]numLinesCacheValue
|
||||
raw bool
|
||||
lastActivity time.Time
|
||||
}
|
||||
|
||||
type numLinesCacheValue struct {
|
||||
@@ -1153,7 +1151,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
||||
bgSemaphores: make(map[action]chan struct{}),
|
||||
keyChan: make(chan tui.Event),
|
||||
eventChan: make(chan tui.Event, 6), // start | (load + result + zero|one) | (focus) | (resize)
|
||||
timerChan: make(chan tui.Event), // unbuffered: every() ticks coalesce when main loop is busy
|
||||
tui: renderer,
|
||||
ttyDefault: opts.TtyDefault,
|
||||
ttyin: ttyin,
|
||||
@@ -1161,7 +1158,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
||||
executing: util.NewAtomicBool(false),
|
||||
lastAction: actStart,
|
||||
lastFocus: minItem.Index(),
|
||||
lastActivity: time.Now(),
|
||||
numLinesCache: make(map[int32]numLinesCacheValue)}
|
||||
if opts.AcceptNth != nil {
|
||||
t.acceptNth = opts.AcceptNth(t.delimiter)
|
||||
@@ -1389,9 +1385,6 @@ func (t *Terminal) environImpl(forPreview bool) []string {
|
||||
env = append(env, "FZF_QUERY="+string(t.input))
|
||||
env = append(env, "FZF_ACTION="+t.lastAction.Name())
|
||||
env = append(env, "FZF_KEY="+t.lastKey)
|
||||
idleMs := time.Since(t.lastActivity).Milliseconds()
|
||||
env = append(env, fmt.Sprintf("FZF_IDLE_TIME=%d", idleMs/1000))
|
||||
env = append(env, fmt.Sprintf("FZF_IDLE_TIME_MS=%d", idleMs))
|
||||
env = append(env, "FZF_PROMPT="+string(t.promptString))
|
||||
env = append(env, "FZF_GHOST="+string(t.ghost))
|
||||
env = append(env, "FZF_POINTER="+string(t.pointer))
|
||||
@@ -5814,35 +5807,6 @@ func (t *Terminal) addClickFooterWord(env []string) []string {
|
||||
return env
|
||||
}
|
||||
|
||||
// startTimers spawns a goroutine per every() bind event. Forwarding ticks
|
||||
// onto the unbuffered timerChan lets the ticker drop overlapping ticks
|
||||
// while the main loop is busy.
|
||||
func (t *Terminal) startTimers(ctx context.Context) {
|
||||
for evt := range t.keymap {
|
||||
switch evt.Type {
|
||||
case tui.Every:
|
||||
d := time.Duration(evt.Char) * time.Millisecond
|
||||
evt := evt
|
||||
go func() {
|
||||
ticker := time.NewTicker(d)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case t.timerChan <- evt:
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loop is called to start Terminal I/O
|
||||
func (t *Terminal) Loop() error {
|
||||
// prof := profile.Start(profile.ProfilePath("/tmp/"))
|
||||
@@ -6350,7 +6314,6 @@ func (t *Terminal) Loop() error {
|
||||
}
|
||||
}
|
||||
}()
|
||||
t.startTimers(ctx)
|
||||
previewDraggingPos := -1
|
||||
barDragging := false
|
||||
pbarDragging := false
|
||||
@@ -6410,7 +6373,6 @@ func (t *Terminal) Loop() error {
|
||||
select {
|
||||
case event = <-t.keyChan:
|
||||
needBarrier = true
|
||||
case event = <-t.timerChan:
|
||||
case event = <-t.eventChan:
|
||||
// Drain channel to process all queued events at once without rendering
|
||||
// the intermediate states
|
||||
@@ -6475,10 +6437,7 @@ func (t *Terminal) Loop() error {
|
||||
previousInput := t.input
|
||||
previousCx := t.cx
|
||||
previousVersion := t.version
|
||||
if event.Type < tui.Invalid {
|
||||
t.lastKey = event.KeyName()
|
||||
t.lastActivity = time.Now()
|
||||
}
|
||||
t.lastKey = event.KeyName()
|
||||
updatePreviewWindow := func(forcePreview bool) {
|
||||
t.resizeWindows(forcePreview, false)
|
||||
req(reqPrompt, reqList, reqInfo, reqHeader, reqFooter)
|
||||
|
||||
+18
-19
@@ -133,22 +133,22 @@ func _() {
|
||||
_ = x[CtrlAltShiftDelete-122]
|
||||
_ = x[CtrlAltShiftPageUp-123]
|
||||
_ = x[CtrlAltShiftPageDown-124]
|
||||
_ = x[Mouse-125]
|
||||
_ = x[DoubleClick-126]
|
||||
_ = x[LeftClick-127]
|
||||
_ = x[RightClick-128]
|
||||
_ = x[SLeftClick-129]
|
||||
_ = x[SRightClick-130]
|
||||
_ = x[ScrollUp-131]
|
||||
_ = x[ScrollDown-132]
|
||||
_ = x[SScrollUp-133]
|
||||
_ = x[SScrollDown-134]
|
||||
_ = x[PreviewScrollUp-135]
|
||||
_ = x[PreviewScrollDown-136]
|
||||
_ = x[Invalid-137]
|
||||
_ = x[Fatal-138]
|
||||
_ = x[BracketedPasteBegin-139]
|
||||
_ = x[BracketedPasteEnd-140]
|
||||
_ = x[Invalid-125]
|
||||
_ = x[Fatal-126]
|
||||
_ = x[BracketedPasteBegin-127]
|
||||
_ = x[BracketedPasteEnd-128]
|
||||
_ = x[Mouse-129]
|
||||
_ = x[DoubleClick-130]
|
||||
_ = x[LeftClick-131]
|
||||
_ = x[RightClick-132]
|
||||
_ = x[SLeftClick-133]
|
||||
_ = x[SRightClick-134]
|
||||
_ = x[ScrollUp-135]
|
||||
_ = x[ScrollDown-136]
|
||||
_ = x[SScrollUp-137]
|
||||
_ = x[SScrollDown-138]
|
||||
_ = x[PreviewScrollUp-139]
|
||||
_ = x[PreviewScrollDown-140]
|
||||
_ = x[Resize-141]
|
||||
_ = x[Change-142]
|
||||
_ = x[BackwardEOF-143]
|
||||
@@ -163,12 +163,11 @@ func _() {
|
||||
_ = x[ClickHeader-152]
|
||||
_ = x[ClickFooter-153]
|
||||
_ = x[Multi-154]
|
||||
_ = x[Every-155]
|
||||
}
|
||||
|
||||
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteShiftHomeShiftEndShiftPageUpShiftPageDownF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltDeleteAltHomeAltEndAltPageUpAltPageDownAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltShiftDeleteAltShiftHomeAltShiftEndAltShiftPageUpAltShiftPageDownCtrlUpCtrlDownCtrlLeftCtrlRightCtrlHomeCtrlEndCtrlBackspaceCtrlDeleteCtrlPageUpCtrlPageDownAltCtrlAltCtrlAltUpCtrlAltDownCtrlAltLeftCtrlAltRightCtrlAltHomeCtrlAltEndCtrlAltBackspaceCtrlAltDeleteCtrlAltPageUpCtrlAltPageDownCtrlShiftUpCtrlShiftDownCtrlShiftLeftCtrlShiftRightCtrlShiftHomeCtrlShiftEndCtrlShiftDeleteCtrlShiftPageUpCtrlShiftPageDownCtrlAltShiftUpCtrlAltShiftDownCtrlAltShiftLeftCtrlAltShiftRightCtrlAltShiftHomeCtrlAltShiftEndCtrlAltShiftDeleteCtrlAltShiftPageUpCtrlAltShiftPageDownMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownInvalidFatalBracketedPasteBeginBracketedPasteEndResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeaderClickFooterMultiEvery"
|
||||
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteShiftHomeShiftEndShiftPageUpShiftPageDownF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltDeleteAltHomeAltEndAltPageUpAltPageDownAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltShiftDeleteAltShiftHomeAltShiftEndAltShiftPageUpAltShiftPageDownCtrlUpCtrlDownCtrlLeftCtrlRightCtrlHomeCtrlEndCtrlBackspaceCtrlDeleteCtrlPageUpCtrlPageDownAltCtrlAltCtrlAltUpCtrlAltDownCtrlAltLeftCtrlAltRightCtrlAltHomeCtrlAltEndCtrlAltBackspaceCtrlAltDeleteCtrlAltPageUpCtrlAltPageDownCtrlShiftUpCtrlShiftDownCtrlShiftLeftCtrlShiftRightCtrlShiftHomeCtrlShiftEndCtrlShiftDeleteCtrlShiftPageUpCtrlShiftPageDownCtrlAltShiftUpCtrlAltShiftDownCtrlAltShiftLeftCtrlAltShiftRightCtrlAltShiftHomeCtrlAltShiftEndCtrlAltShiftDeleteCtrlAltShiftPageUpCtrlAltShiftPageDownInvalidFatalBracketedPasteBeginBracketedPasteEndMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeaderClickFooterMulti"
|
||||
|
||||
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 157, 173, 182, 191, 199, 208, 214, 220, 228, 230, 234, 238, 243, 247, 250, 256, 263, 272, 281, 291, 302, 311, 319, 330, 343, 345, 347, 349, 351, 353, 355, 357, 359, 361, 364, 367, 370, 382, 387, 394, 401, 409, 418, 425, 431, 440, 451, 461, 473, 485, 498, 512, 524, 535, 549, 565, 571, 579, 587, 596, 604, 611, 624, 634, 644, 656, 659, 666, 675, 686, 697, 709, 720, 730, 746, 759, 772, 787, 798, 811, 824, 838, 851, 863, 878, 893, 910, 924, 940, 956, 973, 989, 1004, 1022, 1040, 1060, 1065, 1076, 1085, 1095, 1105, 1116, 1124, 1134, 1143, 1154, 1169, 1186, 1193, 1198, 1217, 1234, 1240, 1246, 1257, 1262, 1266, 1271, 1274, 1278, 1284, 1288, 1298, 1309, 1320, 1325, 1330}
|
||||
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 157, 173, 182, 191, 199, 208, 214, 220, 228, 230, 234, 238, 243, 247, 250, 256, 263, 272, 281, 291, 302, 311, 319, 330, 343, 345, 347, 349, 351, 353, 355, 357, 359, 361, 364, 367, 370, 382, 387, 394, 401, 409, 418, 425, 431, 440, 451, 461, 473, 485, 498, 512, 524, 535, 549, 565, 571, 579, 587, 596, 604, 611, 624, 634, 644, 656, 659, 666, 675, 686, 697, 709, 720, 730, 746, 759, 772, 787, 798, 811, 824, 838, 851, 863, 878, 893, 910, 924, 940, 956, 973, 989, 1004, 1022, 1040, 1060, 1067, 1072, 1091, 1108, 1113, 1124, 1133, 1143, 1153, 1164, 1172, 1182, 1191, 1202, 1217, 1234, 1240, 1246, 1257, 1262, 1266, 1271, 1274, 1278, 1284, 1288, 1298, 1309, 1320, 1325}
|
||||
|
||||
func (i EventType) String() string {
|
||||
if i < 0 || i >= EventType(len(_EventType_index)-1) {
|
||||
|
||||
+6
-10
@@ -196,6 +196,11 @@ const (
|
||||
CtrlAltShiftPageUp
|
||||
CtrlAltShiftPageDown
|
||||
|
||||
Invalid
|
||||
Fatal
|
||||
BracketedPasteBegin
|
||||
BracketedPasteEnd
|
||||
|
||||
Mouse
|
||||
DoubleClick
|
||||
LeftClick
|
||||
@@ -209,15 +214,7 @@ const (
|
||||
PreviewScrollUp
|
||||
PreviewScrollDown
|
||||
|
||||
// Synthetic / non-user events. Everything from Invalid onward is
|
||||
// either internally generated or a state-change notification, not
|
||||
// direct user input. Use `>= Invalid` to gate activity tracking.
|
||||
// BracketedPasteBegin/End sit here too: they enclose user input
|
||||
// (which arrives as Rune events) and should not appear in FZF_KEY.
|
||||
Invalid
|
||||
Fatal
|
||||
BracketedPasteBegin
|
||||
BracketedPasteEnd
|
||||
// Events
|
||||
Resize
|
||||
Change
|
||||
BackwardEOF
|
||||
@@ -232,7 +229,6 @@ const (
|
||||
ClickHeader
|
||||
ClickFooter
|
||||
Multi
|
||||
Every
|
||||
)
|
||||
|
||||
func (t EventType) AsEvent() Event {
|
||||
|
||||
@@ -1387,85 +1387,6 @@ class TestCore < TestInteractive
|
||||
tmux.until { |lines| assert_includes lines, '> 1' }
|
||||
end
|
||||
|
||||
def test_every_event
|
||||
tmux.send_keys %(seq 100 | fzf --bind 'every(0.2):transform-prompt(cat #{tempname})'), :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
# Trigger external state changes; the every() tick should pick them up.
|
||||
writelines(['AAA>'])
|
||||
tmux.until { |lines| assert_includes lines[-1], 'AAA>' }
|
||||
writelines(['BBB>'])
|
||||
tmux.until { |lines| assert_includes lines[-1], 'BBB>' }
|
||||
end
|
||||
|
||||
def test_every_event_multiple_independent_timers
|
||||
# Two timers with different durations should fire independently.
|
||||
fast = tempname + '.fast'
|
||||
slow = tempname + '.slow'
|
||||
FileUtils.rm_f(fast)
|
||||
FileUtils.rm_f(slow)
|
||||
tmux.send_keys %(seq 100 | fzf \\
|
||||
--bind 'every(0.1):execute-silent(printf . >> #{fast})' \\
|
||||
--bind 'every(0.5):execute-silent(printf . >> #{slow})'), :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
sleep 1.2
|
||||
a = File.exist?(fast) ? File.size(fast) : 0
|
||||
b = File.exist?(slow) ? File.size(slow) : 0
|
||||
# Sanity: faster timer fired more times.
|
||||
assert a > b, "fast timer should fire more (#{a} vs #{b})"
|
||||
# Sanity: slow timer fired at least once.
|
||||
assert b >= 1, "slow timer should have fired at least once (#{b})"
|
||||
ensure
|
||||
FileUtils.rm_f(fast)
|
||||
FileUtils.rm_f(slow)
|
||||
end
|
||||
|
||||
def test_every_event_unbind
|
||||
tmux.send_keys %(seq 100 | fzf --bind 'every(0.1):transform-header(date +%S.%N)' --bind 'space:unbind(every(0.1))+change-header(STOPPED)'), :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
# Header should be ticking
|
||||
tmux.until { |lines| assert_match(/^ \d{2}\.\d+/, lines[-3]) }
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_includes lines[-3], 'STOPPED' }
|
||||
sleep 0.4
|
||||
# Header must stay STOPPED after the unbind
|
||||
assert_includes tmux.capture[-3], 'STOPPED'
|
||||
end
|
||||
|
||||
def test_fzf_idle_time_env
|
||||
# FZF_IDLE_TIME + FZF_IDLE_TIME_MS combined with every() implement idle-based behavior.
|
||||
tmux.send_keys %(seq 100 | fzf --bind 'every(0.2):transform-header(echo "s=$FZF_IDLE_TIME ms_ok=$((FZF_IDLE_TIME_MS / 1000 == FZF_IDLE_TIME))")'), :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
# Idle counter advances without any input; ms/1000 stays consistent with seconds.
|
||||
tmux.until { |lines| assert_includes lines[-3], 's=1 ms_ok=1' }
|
||||
tmux.until { |lines| assert_includes lines[-3], 's=2 ms_ok=1' }
|
||||
# Any keystroke resets the counter
|
||||
tmux.send_keys 'x'
|
||||
tmux.until { |lines| assert_includes lines[-3], 's=0 ms_ok=1' }
|
||||
tmux.send_keys :BSpace
|
||||
# And it advances again afterwards
|
||||
tmux.until { |lines| assert_includes lines[-3], 's=1 ms_ok=1' }
|
||||
end
|
||||
|
||||
def test_every_event_rejects_invalid_arg
|
||||
%w[every(0) every(-1) every(abc) every()].each do |spec|
|
||||
tmux.send_keys %(seq 1 | fzf --bind '#{spec}:abort' 2>&1; echo done=$?), :Enter
|
||||
tmux.until { |lines| assert(lines.any? { |l| l.include?('done=2') }) }
|
||||
tmux.send_keys 'clear', :Enter
|
||||
end
|
||||
end
|
||||
|
||||
def test_fzf_key_ignores_synthetic_events
|
||||
tmux.send_keys %(seq 100 | fzf --bind 'every(0.2):transform-prompt(echo "[$FZF_KEY]> ")'), :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
# No user input yet: prompt should show empty FZF_KEY
|
||||
tmux.until { |lines| assert_includes lines[-1], '[]>' }
|
||||
tmux.send_keys 'x'
|
||||
tmux.until { |lines| assert_includes lines[-1], '[x]>' }
|
||||
# every() ticks shouldn't overwrite FZF_KEY
|
||||
sleep 1
|
||||
assert_includes tmux.capture[-1], '[x]>'
|
||||
end
|
||||
|
||||
def test_labels_center
|
||||
tmux.send_keys 'echo x | fzf --border --border-label foobar --preview : --preview-label barfoo --bind "space:change-border-label(foobarfoo)+change-preview-label(barfoobar),enter:transform-border-label(echo foo{}foo)+transform-preview-label(echo bar{}bar)"', :Enter
|
||||
tmux.until do
|
||||
|
||||
Reference in New Issue
Block a user