mirror of
https://github.com/junegunn/fzf.git
synced 2026-06-22 00:53:52 +08:00
Add every(N) bind event and FZF_IDLE_TIME env var (#4797)
- every(N) fires every N seconds (fractional, floored to 0.01s) - Encoded as tui.Every with duration in Char as milliseconds, so every(1) and every(2) coexist as distinct keymap entries - FZF_IDLE_TIME exposes whole seconds since the last user activity (keystroke or mouse event); pair with every() for idle-based patterns like auto-accept/auto-quit Close #1211
This commit is contained in:
+24
-1
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"math"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -1257,7 +1258,14 @@ func parseKeyChords(str string, message string) (map[tui.Event]string, []tui.Eve
|
||||
add(tui.F12)
|
||||
default:
|
||||
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])
|
||||
evt := tui.CtrlAltKey(r)
|
||||
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
|
||||
}
|
||||
|
||||
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,6 +299,39 @@ 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
|
||||
|
||||
+42
-1
@@ -436,6 +436,7 @@ 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
|
||||
@@ -456,6 +457,7 @@ type Terminal struct {
|
||||
proxyScript string
|
||||
numLinesCache map[int32]numLinesCacheValue
|
||||
raw bool
|
||||
lastActivity time.Time
|
||||
}
|
||||
|
||||
type numLinesCacheValue struct {
|
||||
@@ -1151,6 +1153,7 @@ 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,
|
||||
@@ -1158,6 +1161,7 @@ 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)
|
||||
@@ -1385,6 +1389,9 @@ 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))
|
||||
@@ -5807,6 +5814,35 @@ 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/"))
|
||||
@@ -6314,6 +6350,7 @@ func (t *Terminal) Loop() error {
|
||||
}
|
||||
}
|
||||
}()
|
||||
t.startTimers(ctx)
|
||||
previewDraggingPos := -1
|
||||
barDragging := false
|
||||
pbarDragging := false
|
||||
@@ -6373,6 +6410,7 @@ 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
|
||||
@@ -6437,7 +6475,10 @@ func (t *Terminal) Loop() error {
|
||||
previousInput := t.input
|
||||
previousCx := t.cx
|
||||
previousVersion := t.version
|
||||
t.lastKey = event.KeyName()
|
||||
if event.Type < tui.Invalid {
|
||||
t.lastKey = event.KeyName()
|
||||
t.lastActivity = time.Now()
|
||||
}
|
||||
updatePreviewWindow := func(forcePreview bool) {
|
||||
t.resizeWindows(forcePreview, false)
|
||||
req(reqPrompt, reqList, reqInfo, reqHeader, reqFooter)
|
||||
|
||||
+19
-18
@@ -133,22 +133,22 @@ func _() {
|
||||
_ = x[CtrlAltShiftDelete-122]
|
||||
_ = x[CtrlAltShiftPageUp-123]
|
||||
_ = x[CtrlAltShiftPageDown-124]
|
||||
_ = 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[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[Resize-141]
|
||||
_ = x[Change-142]
|
||||
_ = x[BackwardEOF-143]
|
||||
@@ -163,11 +163,12 @@ func _() {
|
||||
_ = x[ClickHeader-152]
|
||||
_ = x[ClickFooter-153]
|
||||
_ = 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 {
|
||||
if i < 0 || i >= EventType(len(_EventType_index)-1) {
|
||||
|
||||
+10
-6
@@ -196,11 +196,6 @@ const (
|
||||
CtrlAltShiftPageUp
|
||||
CtrlAltShiftPageDown
|
||||
|
||||
Invalid
|
||||
Fatal
|
||||
BracketedPasteBegin
|
||||
BracketedPasteEnd
|
||||
|
||||
Mouse
|
||||
DoubleClick
|
||||
LeftClick
|
||||
@@ -214,7 +209,15 @@ const (
|
||||
PreviewScrollUp
|
||||
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
|
||||
Change
|
||||
BackwardEOF
|
||||
@@ -229,6 +232,7 @@ const (
|
||||
ClickHeader
|
||||
ClickFooter
|
||||
Multi
|
||||
Every
|
||||
)
|
||||
|
||||
func (t EventType) AsEvent() Event {
|
||||
|
||||
Reference in New Issue
Block a user