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:
Junegunn Choi
2026-05-17 23:54:59 +09:00
committed by GitHub
parent e0d081906f
commit fcc3c6acce
8 changed files with 248 additions and 26 deletions
+79
View File
@@ -1387,6 +1387,85 @@ 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