mirror of
https://github.com/junegunn/fzf.git
synced 2026-05-16 21:45:15 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6657639578 | |||
| 4364dc1d24 | |||
| 07f3b00bd4 | |||
| 463ef212b6 | |||
| 38c88e4753 | |||
| e0d081906f |
@@ -11,4 +11,4 @@ jobs:
|
|||||||
- name: 'Checkout Repository'
|
- name: 'Checkout Repository'
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
- name: 'Dependency Review'
|
- name: 'Dependency Review'
|
||||||
uses: actions/dependency-review-action@v5
|
uses: actions/dependency-review-action@v4
|
||||||
|
|||||||
@@ -3,6 +3,19 @@ CHANGELOG
|
|||||||
|
|
||||||
0.73.0
|
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
|
- 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)
|
- `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,6 +1500,10 @@ fzf exports the following environment variables to its child processes.
|
|||||||
.br
|
.br
|
||||||
.BR FZF_KEY " The name of the last key pressed"
|
.BR FZF_KEY " The name of the last key pressed"
|
||||||
.br
|
.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 FZF_PORT " Port number when \-\-listen option is used"
|
||||||
.br
|
.br
|
||||||
.BR FZF_SOCK " Unix socket path when \-\-listen option is used"
|
.BR FZF_SOCK " Unix socket path when \-\-listen option is used"
|
||||||
@@ -1939,6 +1943,30 @@ variables starting from 1. It optionally sets \fBFZF_CLICK_FOOTER_WORD\fR
|
|||||||
if clicked on a word.
|
if clicked on a word.
|
||||||
.RE
|
.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:
|
.SS AVAILABLE ACTIONS:
|
||||||
A key or an event can be bound to one or more of the following actions.
|
A key or an event can be bound to one or more of the following actions.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -266,7 +266,7 @@ func charClassOf(char rune) charClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func bonusFor(prevClass charClass, class charClass) int16 {
|
func bonusFor(prevClass charClass, class charClass) int16 {
|
||||||
if class > charNonWord {
|
if class >= charNonWord {
|
||||||
switch prevClass {
|
switch prevClass {
|
||||||
case charWhite:
|
case charWhite:
|
||||||
// Word boundary after whitespace
|
// Word boundary after whitespace
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ func TestFuzzyMatch(t *testing.T) {
|
|||||||
scoreMatch*4+int(bonusBoundaryDelimiter)*bonusFirstCharMultiplier+int(bonusBoundaryDelimiter)*3)
|
scoreMatch*4+int(bonusBoundaryDelimiter)*bonusFirstCharMultiplier+int(bonusBoundaryDelimiter)*3)
|
||||||
assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13,
|
assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13,
|
||||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+scoreGapStart+int(bonusBoundaryDelimiter))
|
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,
|
assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10,
|
||||||
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtension)
|
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtension)
|
||||||
assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10,
|
assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10,
|
||||||
|
|||||||
+24
-1
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -1257,7 +1258,14 @@ func parseKeyChords(str string, message string) (map[tui.Event]string, []tui.Eve
|
|||||||
add(tui.F12)
|
add(tui.F12)
|
||||||
default:
|
default:
|
||||||
runes := []rune(key)
|
runes := []rune(key)
|
||||||
if len(key) == 10 && strings.HasPrefix(lkey, "ctrl-alt-") && isAlphabet(lkey[9]) {
|
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]) {
|
||||||
r := rune(lkey[9])
|
r := rune(lkey[9])
|
||||||
evt := tui.CtrlAltKey(r)
|
evt := tui.CtrlAltKey(r)
|
||||||
if r == 'h' && !util.IsWindows() {
|
if r == 'h' && !util.IsWindows() {
|
||||||
@@ -1299,6 +1307,21 @@ func parseKeyChords(str string, message string) (map[tui.Event]string, []tui.Eve
|
|||||||
return chords, list, nil
|
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) {
|
func parseScheme(str string) (string, []criterion, error) {
|
||||||
str = strings.ToLower(str)
|
str = strings.ToLower(str)
|
||||||
switch str {
|
switch str {
|
||||||
|
|||||||
@@ -299,6 +299,43 @@ func TestBind(t *testing.T) {
|
|||||||
check(tui.F1.AsEvent(), "", actAbort)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyName round-trips with the original duration
|
||||||
|
if got := (tui.Event{Type: tui.Every, Char: 2000}).KeyName(); got != "every(2)" {
|
||||||
|
t.Errorf("KeyName: %q != every(2)", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestColorSpec(t *testing.T) {
|
func TestColorSpec(t *testing.T) {
|
||||||
var base *tui.ColorTheme
|
var base *tui.ColorTheme
|
||||||
theme := tui.Dark256
|
theme := tui.Dark256
|
||||||
|
|||||||
@@ -436,6 +436,7 @@ type Terminal struct {
|
|||||||
bgSemaphores map[action]chan struct{}
|
bgSemaphores map[action]chan struct{}
|
||||||
keyChan chan tui.Event
|
keyChan chan tui.Event
|
||||||
eventChan chan tui.Event
|
eventChan chan tui.Event
|
||||||
|
timerChan chan tui.Event
|
||||||
slab *util.Slab
|
slab *util.Slab
|
||||||
theme *tui.ColorTheme
|
theme *tui.ColorTheme
|
||||||
tui tui.Renderer
|
tui tui.Renderer
|
||||||
@@ -456,6 +457,7 @@ type Terminal struct {
|
|||||||
proxyScript string
|
proxyScript string
|
||||||
numLinesCache map[int32]numLinesCacheValue
|
numLinesCache map[int32]numLinesCacheValue
|
||||||
raw bool
|
raw bool
|
||||||
|
lastActivity time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type numLinesCacheValue struct {
|
type numLinesCacheValue struct {
|
||||||
@@ -1151,6 +1153,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
|||||||
bgSemaphores: make(map[action]chan struct{}),
|
bgSemaphores: make(map[action]chan struct{}),
|
||||||
keyChan: make(chan tui.Event),
|
keyChan: make(chan tui.Event),
|
||||||
eventChan: make(chan tui.Event, 6), // start | (load + result + zero|one) | (focus) | (resize)
|
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,
|
tui: renderer,
|
||||||
ttyDefault: opts.TtyDefault,
|
ttyDefault: opts.TtyDefault,
|
||||||
ttyin: ttyin,
|
ttyin: ttyin,
|
||||||
@@ -1158,6 +1161,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
|||||||
executing: util.NewAtomicBool(false),
|
executing: util.NewAtomicBool(false),
|
||||||
lastAction: actStart,
|
lastAction: actStart,
|
||||||
lastFocus: minItem.Index(),
|
lastFocus: minItem.Index(),
|
||||||
|
lastActivity: time.Now(),
|
||||||
numLinesCache: make(map[int32]numLinesCacheValue)}
|
numLinesCache: make(map[int32]numLinesCacheValue)}
|
||||||
if opts.AcceptNth != nil {
|
if opts.AcceptNth != nil {
|
||||||
t.acceptNth = opts.AcceptNth(t.delimiter)
|
t.acceptNth = opts.AcceptNth(t.delimiter)
|
||||||
@@ -1385,6 +1389,9 @@ func (t *Terminal) environImpl(forPreview bool) []string {
|
|||||||
env = append(env, "FZF_QUERY="+string(t.input))
|
env = append(env, "FZF_QUERY="+string(t.input))
|
||||||
env = append(env, "FZF_ACTION="+t.lastAction.Name())
|
env = append(env, "FZF_ACTION="+t.lastAction.Name())
|
||||||
env = append(env, "FZF_KEY="+t.lastKey)
|
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_PROMPT="+string(t.promptString))
|
||||||
env = append(env, "FZF_GHOST="+string(t.ghost))
|
env = append(env, "FZF_GHOST="+string(t.ghost))
|
||||||
env = append(env, "FZF_POINTER="+string(t.pointer))
|
env = append(env, "FZF_POINTER="+string(t.pointer))
|
||||||
@@ -5807,6 +5814,35 @@ func (t *Terminal) addClickFooterWord(env []string) []string {
|
|||||||
return env
|
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
|
// Loop is called to start Terminal I/O
|
||||||
func (t *Terminal) Loop() error {
|
func (t *Terminal) Loop() error {
|
||||||
// prof := profile.Start(profile.ProfilePath("/tmp/"))
|
// prof := profile.Start(profile.ProfilePath("/tmp/"))
|
||||||
@@ -6314,6 +6350,7 @@ func (t *Terminal) Loop() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
t.startTimers(ctx)
|
||||||
previewDraggingPos := -1
|
previewDraggingPos := -1
|
||||||
barDragging := false
|
barDragging := false
|
||||||
pbarDragging := false
|
pbarDragging := false
|
||||||
@@ -6373,6 +6410,10 @@ func (t *Terminal) Loop() error {
|
|||||||
select {
|
select {
|
||||||
case event = <-t.keyChan:
|
case event = <-t.keyChan:
|
||||||
needBarrier = true
|
needBarrier = true
|
||||||
|
if event.Type < tui.Invalid {
|
||||||
|
t.lastActivity = time.Now()
|
||||||
|
}
|
||||||
|
case event = <-t.timerChan:
|
||||||
case event = <-t.eventChan:
|
case event = <-t.eventChan:
|
||||||
// Drain channel to process all queued events at once without rendering
|
// Drain channel to process all queued events at once without rendering
|
||||||
// the intermediate states
|
// the intermediate states
|
||||||
|
|||||||
+19
-18
@@ -133,22 +133,22 @@ func _() {
|
|||||||
_ = x[CtrlAltShiftDelete-122]
|
_ = x[CtrlAltShiftDelete-122]
|
||||||
_ = x[CtrlAltShiftPageUp-123]
|
_ = x[CtrlAltShiftPageUp-123]
|
||||||
_ = x[CtrlAltShiftPageDown-124]
|
_ = x[CtrlAltShiftPageDown-124]
|
||||||
_ = x[Invalid-125]
|
_ = x[Mouse-125]
|
||||||
_ = x[Fatal-126]
|
_ = x[DoubleClick-126]
|
||||||
_ = x[BracketedPasteBegin-127]
|
_ = x[LeftClick-127]
|
||||||
_ = x[BracketedPasteEnd-128]
|
_ = x[RightClick-128]
|
||||||
_ = x[Mouse-129]
|
_ = x[SLeftClick-129]
|
||||||
_ = x[DoubleClick-130]
|
_ = x[SRightClick-130]
|
||||||
_ = x[LeftClick-131]
|
_ = x[ScrollUp-131]
|
||||||
_ = x[RightClick-132]
|
_ = x[ScrollDown-132]
|
||||||
_ = x[SLeftClick-133]
|
_ = x[SScrollUp-133]
|
||||||
_ = x[SRightClick-134]
|
_ = x[SScrollDown-134]
|
||||||
_ = x[ScrollUp-135]
|
_ = x[PreviewScrollUp-135]
|
||||||
_ = x[ScrollDown-136]
|
_ = x[PreviewScrollDown-136]
|
||||||
_ = x[SScrollUp-137]
|
_ = x[Invalid-137]
|
||||||
_ = x[SScrollDown-138]
|
_ = x[Fatal-138]
|
||||||
_ = x[PreviewScrollUp-139]
|
_ = x[BracketedPasteBegin-139]
|
||||||
_ = x[PreviewScrollDown-140]
|
_ = x[BracketedPasteEnd-140]
|
||||||
_ = x[Resize-141]
|
_ = x[Resize-141]
|
||||||
_ = x[Change-142]
|
_ = x[Change-142]
|
||||||
_ = x[BackwardEOF-143]
|
_ = x[BackwardEOF-143]
|
||||||
@@ -163,11 +163,12 @@ func _() {
|
|||||||
_ = x[ClickHeader-152]
|
_ = x[ClickHeader-152]
|
||||||
_ = x[ClickFooter-153]
|
_ = x[ClickFooter-153]
|
||||||
_ = x[Multi-154]
|
_ = x[Multi-154]
|
||||||
|
_ = x[Every-155]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteShiftHomeShiftEndShiftPageUpShiftPageDownF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltDeleteAltHomeAltEndAltPageUpAltPageDownAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltShiftDeleteAltShiftHomeAltShiftEndAltShiftPageUpAltShiftPageDownCtrlUpCtrlDownCtrlLeftCtrlRightCtrlHomeCtrlEndCtrlBackspaceCtrlDeleteCtrlPageUpCtrlPageDownAltCtrlAltCtrlAltUpCtrlAltDownCtrlAltLeftCtrlAltRightCtrlAltHomeCtrlAltEndCtrlAltBackspaceCtrlAltDeleteCtrlAltPageUpCtrlAltPageDownCtrlShiftUpCtrlShiftDownCtrlShiftLeftCtrlShiftRightCtrlShiftHomeCtrlShiftEndCtrlShiftDeleteCtrlShiftPageUpCtrlShiftPageDownCtrlAltShiftUpCtrlAltShiftDownCtrlAltShiftLeftCtrlAltShiftRightCtrlAltShiftHomeCtrlAltShiftEndCtrlAltShiftDeleteCtrlAltShiftPageUpCtrlAltShiftPageDownInvalidFatalBracketedPasteBeginBracketedPasteEndMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeaderClickFooterMulti"
|
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteShiftHomeShiftEndShiftPageUpShiftPageDownF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltDeleteAltHomeAltEndAltPageUpAltPageDownAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltShiftDeleteAltShiftHomeAltShiftEndAltShiftPageUpAltShiftPageDownCtrlUpCtrlDownCtrlLeftCtrlRightCtrlHomeCtrlEndCtrlBackspaceCtrlDeleteCtrlPageUpCtrlPageDownAltCtrlAltCtrlAltUpCtrlAltDownCtrlAltLeftCtrlAltRightCtrlAltHomeCtrlAltEndCtrlAltBackspaceCtrlAltDeleteCtrlAltPageUpCtrlAltPageDownCtrlShiftUpCtrlShiftDownCtrlShiftLeftCtrlShiftRightCtrlShiftHomeCtrlShiftEndCtrlShiftDeleteCtrlShiftPageUpCtrlShiftPageDownCtrlAltShiftUpCtrlAltShiftDownCtrlAltShiftLeftCtrlAltShiftRightCtrlAltShiftHomeCtrlAltShiftEndCtrlAltShiftDeleteCtrlAltShiftPageUpCtrlAltShiftPageDownMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownInvalidFatalBracketedPasteBeginBracketedPasteEndResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeaderClickFooterMultiEvery"
|
||||||
|
|
||||||
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}
|
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}
|
||||||
|
|
||||||
func (i EventType) String() string {
|
func (i EventType) String() string {
|
||||||
if i < 0 || i >= EventType(len(_EventType_index)-1) {
|
if i < 0 || i >= EventType(len(_EventType_index)-1) {
|
||||||
|
|||||||
+14
-6
@@ -196,11 +196,6 @@ const (
|
|||||||
CtrlAltShiftPageUp
|
CtrlAltShiftPageUp
|
||||||
CtrlAltShiftPageDown
|
CtrlAltShiftPageDown
|
||||||
|
|
||||||
Invalid
|
|
||||||
Fatal
|
|
||||||
BracketedPasteBegin
|
|
||||||
BracketedPasteEnd
|
|
||||||
|
|
||||||
Mouse
|
Mouse
|
||||||
DoubleClick
|
DoubleClick
|
||||||
LeftClick
|
LeftClick
|
||||||
@@ -214,7 +209,15 @@ const (
|
|||||||
PreviewScrollUp
|
PreviewScrollUp
|
||||||
PreviewScrollDown
|
PreviewScrollDown
|
||||||
|
|
||||||
// Events
|
// 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
|
||||||
Resize
|
Resize
|
||||||
Change
|
Change
|
||||||
BackwardEOF
|
BackwardEOF
|
||||||
@@ -229,6 +232,7 @@ const (
|
|||||||
ClickHeader
|
ClickHeader
|
||||||
ClickFooter
|
ClickFooter
|
||||||
Multi
|
Multi
|
||||||
|
Every
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t EventType) AsEvent() Event {
|
func (t EventType) AsEvent() Event {
|
||||||
@@ -253,6 +257,10 @@ func (e Event) KeyName() string {
|
|||||||
return me.Name()
|
return me.Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e.Type == Every {
|
||||||
|
return "every(" + strconv.FormatFloat(float64(e.Char)/1000, 'f', -1, 64) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
if e.Type >= Invalid {
|
if e.Type >= Invalid {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1387,6 +1387,73 @@ class TestCore < TestInteractive
|
|||||||
tmux.until { |lines| assert_includes lines, '> 1' }
|
tmux.until { |lines| assert_includes lines, '> 1' }
|
||||||
end
|
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_labels_center
|
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.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
|
tmux.until do
|
||||||
|
|||||||
Reference in New Issue
Block a user