From 76a3ef8c37bd6b853f79e022d6d4eae35f41d1ba Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 1 Nov 2014 13:46:24 +0900 Subject: [PATCH 1/6] Add --with-nth option (#102) --- .travis.yml | 9 ++++ README.md | 49 ++++++++--------- Rakefile | 1 + fzf | 137 +++++++++++++++++++++++++++++------------------ test/test_fzf.rb | 34 +++++++++++- 5 files changed, 151 insertions(+), 79 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f4e97df --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: ruby +rvm: + - "1.8.7" + - "1.9.3" + - "2.0.0" + - "2.1.1" + +install: gem install curses minitest + diff --git a/README.md b/README.md index 679c8f1..0b52864 100644 --- a/README.md +++ b/README.md @@ -65,38 +65,39 @@ Usage usage: fzf [options] Search - -x, --extended Extended-search mode - -e, --extended-exact Extended-search mode (exact match) - -i Case-insensitive match (default: smart-case match) - +i Case-sensitive match - -n, --nth=N[,..] Comma-separated list of field index expressions - for limiting search scope. Each can be a non-zero - integer or a range expression ([BEGIN]..[END]) - -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) + -x, --extended Extended-search mode + -e, --extended-exact Extended-search mode (exact match) + -i Case-insensitive match (default: smart-case match) + +i Case-sensitive match + -n, --nth=N[,..] Comma-separated list of field index expressions + for limiting search scope. Each can be a non-zero + integer or a range expression ([BEGIN]..[END]) + --with-nth=N[,..] Transform the item using index expressions for search + -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) Search result - -s, --sort=MAX Maximum number of matched items to sort (default: 1000) - +s, --no-sort Do not sort the result. Keep the sequence unchanged. + -s, --sort=MAX Maximum number of matched items to sort (default: 1000) + +s, --no-sort Do not sort the result. Keep the sequence unchanged. Interface - -m, --multi Enable multi-select with tab/shift-tab - --no-mouse Disable mouse - +c, --no-color Disable colors - +2, --no-256 Disable 256-color - --black Use black background - --reverse Reverse orientation - --prompt=STR Input prompt (default: '> ') + -m, --multi Enable multi-select with tab/shift-tab + --no-mouse Disable mouse + +c, --no-color Disable colors + +2, --no-256 Disable 256-color + --black Use black background + --reverse Reverse orientation + --prompt=STR Input prompt (default: '> ') Scripting - -q, --query=STR Start the finder with the given query - -1, --select-1 Automatically select the only match - -0, --exit-0 Exit immediately when there's no match - -f, --filter=STR Filter mode. Do not start interactive finder. - --print-query Print query as the first line + -q, --query=STR Start the finder with the given query + -1, --select-1 Automatically select the only match + -0, --exit-0 Exit immediately when there's no match + -f, --filter=STR Filter mode. Do not start interactive finder. + --print-query Print query as the first line Environment variables - FZF_DEFAULT_COMMAND Default command to use when input is tty - FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m --sort 10000") + FZF_DEFAULT_COMMAND Default command to use when input is tty + FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m --sort 10000") ``` fzf will launch curses-based finder, read the list from STDIN, and write the diff --git a/Rakefile b/Rakefile index 1c999e7..933a039 100644 --- a/Rakefile +++ b/Rakefile @@ -6,3 +6,4 @@ Rake::TestTask.new(:test) do |test| test.verbose = true end +task :default => :test diff --git a/fzf b/fzf index 0345412..2304958 100755 --- a/fzf +++ b/fzf @@ -7,7 +7,7 @@ # / __/ / /_/ __/ # /_/ /___/_/ Fuzzy finder for your shell # -# Version: 0.8.7 (Aug 17, 2014) +# Version: 0.8.8 (Nov 1, 2014) # # Author: Junegunn Choi # URL: https://github.com/junegunn/fzf @@ -53,11 +53,34 @@ unless String.method_defined? :force_encoding end end +class String + attr_accessor :orig + + def tokenize delim, nth + unless delim + # AWK default + prefix_length = (index(/\S/) || 0) rescue 0 + tokens = scan(/\S+\s*/) rescue [] + else + prefix_length = 0 + tokens = scan(delim) rescue [] + end + nth.map { |n| + if n.begin == 0 && n.end == -1 + [prefix_length, tokens.join] + elsif part = tokens[n] + [prefix_length + (tokens[0...(n.begin)] || []).join.length, + part.join] + end + }.compact + end +end + class FZF C = Curses attr_reader :rxflag, :sort, :nth, :color, :black, :ansi256, :reverse, :prompt, :mouse, :multi, :query, :select1, :exit0, :filter, :extended, - :print_query + :print_query, :with_nth def sync @shr_mtx.synchronize { yield } @@ -95,6 +118,7 @@ class FZF @exit0 = false @filter = nil @nth = nil + @with_nth = nil @delim = nil @reverse = false @prompt = '> ' @@ -148,6 +172,11 @@ class FZF @nth = parse_nth nth when /^-n([0-9,-\.]+)$/, /^--nth=([0-9,-\.]+)$/ @nth = parse_nth $1 + when '--with-nth' + usage 1, 'field expression required' unless nth = argv.shift + @with_nth = parse_nth nth + when /^--with-nth=([0-9,-\.]+)$/ + @with_nth = parse_nth $1 when '-d', '--delimiter' usage 1, 'delimiter required' unless delim = argv.shift @delim = FZF.build_delim_regex delim @@ -181,6 +210,7 @@ class FZF @queue = Queue.new @pending = nil @rev_dir = @reverse ? -1 : 1 + @stdout = $stdout.clone unless @filter # Shared variables: needs protection @@ -200,7 +230,7 @@ class FZF end def parse_nth nth - nth.split(',').map { |expr| + ranges = nth.split(',').map { |expr| x = proc { usage 1, "invalid field expression: #{expr}" } first, second = expr.split('..', 2) x.call if !first.empty? && first.to_i == 0 || @@ -215,6 +245,7 @@ class FZF Range.new(*[first, second].map { |e| e > 0 ? e - 1 : e }) } + ranges == [0..-1] ? nil : ranges end def FZF.build_delim_regex delim @@ -222,6 +253,10 @@ class FZF Regexp.compile "(?:.*?#{delim})|(?:.+?$)" end + def burp string + @stdout.puts(string.orig || string) + end + def start if @filter start_reader.join @@ -236,7 +271,7 @@ class FZF if loaded if @select1 && len == 1 puts @query if @print_query - puts empty ? matches.first : matches.first.first + burp(empty ? matches.first : matches.first.first) exit 0 elsif @exit0 && len == 0 puts @query if @print_query @@ -312,39 +347,40 @@ class FZF $stderr.puts %[usage: fzf [options] Search - -x, --extended Extended-search mode - -e, --extended-exact Extended-search mode (exact match) - -i Case-insensitive match (default: smart-case match) - +i Case-sensitive match - -n, --nth=N[,..] Comma-separated list of field index expressions - for limiting search scope. Each can be a non-zero - integer or a range expression ([BEGIN]..[END]) - -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) + -x, --extended Extended-search mode + -e, --extended-exact Extended-search mode (exact match) + -i Case-insensitive match (default: smart-case match) + +i Case-sensitive match + -n, --nth=N[,..] Comma-separated list of field index expressions + for limiting search scope. Each can be a non-zero + integer or a range expression ([BEGIN]..[END]) + --with-nth=N[,..] Transform the item using index expressions for search + -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) Search result - -s, --sort=MAX Maximum number of matched items to sort (default: 1000) - +s, --no-sort Do not sort the result. Keep the sequence unchanged. + -s, --sort=MAX Maximum number of matched items to sort (default: 1000) + +s, --no-sort Do not sort the result. Keep the sequence unchanged. Interface - -m, --multi Enable multi-select with tab/shift-tab - --no-mouse Disable mouse - +c, --no-color Disable colors - +2, --no-256 Disable 256-color - --black Use black background - --reverse Reverse orientation - --prompt=STR Input prompt (default: '> ') + -m, --multi Enable multi-select with tab/shift-tab + --no-mouse Disable mouse + +c, --no-color Disable colors + +2, --no-256 Disable 256-color + --black Use black background + --reverse Reverse orientation + --prompt=STR Input prompt (default: '> ') Scripting - -q, --query=STR Start the finder with the given query - -1, --select-1 Automatically select the only match - -0, --exit-0 Exit immediately when there's no match - -f, --filter=STR Filter mode. Do not start interactive finder. - --print-query Print query as the first line + -q, --query=STR Start the finder with the given query + -1, --select-1 Automatically select the only match + -0, --exit-0 Exit immediately when there's no match + -f, --filter=STR Filter mode. Do not start interactive finder. + --print-query Print query as the first line Environment variables - FZF_DEFAULT_COMMAND Default command to use when input is tty - FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m --sort 10000")] + $/ + $/ - exit x + FZF_DEFAULT_COMMAND Default command to use when input is tty + FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m --sort 10000")] + $/ + $/ + exit x end def emit event @@ -520,7 +556,6 @@ class FZF end def init_screen - @stdout = $stdout.clone $stdout.reopen($stderr) C.init_screen @@ -595,14 +630,28 @@ class FZF end Thread.new do - while line = stream.gets - emit(:new) { @new << line.chomp } + if @with_nth + while line = stream.gets + emit(:new) { @new << transform(line) } + end + else + while line = stream.gets + emit(:new) { @new << line.chomp } + end end emit(:loaded) { true } @spinner.clear if @spinner end end + def transform line + line = line.chomp + mut = (line =~ / $/ ? line : line + ' '). + tokenize(@delim, @with_nth).map { |e| e.last }.join('').sub(/ *$/, '') + mut.orig = line + mut + end + def start_search &callback Thread.new do lists = [] @@ -1080,10 +1129,10 @@ class FZF @stdout.puts q if @print_query if got if selects.empty? - @stdout.puts got + burp got else selects.each do |sel, _| - @stdout.puts sel + burp sel end end end @@ -1108,25 +1157,7 @@ class FZF end def tokenize str - @tokens_cache[str] ||= - begin - unless @delim - # AWK default - prefix_length = (str.index(/\S/) || 0) rescue 0 - tokens = str.scan(/\S+\s*/) rescue [] - else - prefix_length = 0 - tokens = str.scan(@delim) rescue [] - end - @nth.map { |n| - if n.begin == 0 && n.end == -1 - [prefix_length, tokens.join] - elsif part = tokens[n] - [prefix_length + (tokens[0...(n.begin)] || []).join.length, - part.join] - end - }.compact - end + @tokens_cache[str] ||= str.tokenize(@delim, @nth) end def do_match str, pat diff --git a/test/test_fzf.rb b/test/test_fzf.rb index 806df8a..6fbd426 100644 --- a/test/test_fzf.rb +++ b/test/test_fzf.rb @@ -1,6 +1,7 @@ #!/usr/bin/env ruby # encoding: utf-8 +require 'rubygems' require 'curses' require 'timeout' require 'stringio' @@ -25,6 +26,7 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal nil, fzf.rxflag assert_equal true, fzf.mouse assert_equal nil, fzf.nth + assert_equal nil, fzf.with_nth assert_equal true, fzf.color assert_equal false, fzf.black assert_equal true, fzf.ansi256 @@ -47,7 +49,7 @@ class TestFZF < MiniTest::Unit::TestCase ENV['FZF_DEFAULT_OPTS'] = '-x -m -s 10000 -q " hello world " +c +2 --select-1 -0 ' << - '--no-mouse -f "goodbye world" --black --nth=3,-1,2 --reverse --print-query' + '--no-mouse -f "goodbye world" --black --with-nth=3,-3..,2 --nth=3,-1,2 --reverse --print-query' fzf = FZF.new [] assert_equal 10000, fzf.sort assert_equal ' hello world ', @@ -65,13 +67,14 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal true, fzf.reverse assert_equal true, fzf.print_query assert_equal [2..2, -1..-1, 1..1], fzf.nth + assert_equal [2..2, -3..-1, 1..1], fzf.with_nth end def test_option_parser # Long opts fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello --select-1 --exit-0 --filter=howdy --extended-exact - --no-mouse --no-256 --nth=1 --reverse --prompt (hi) + --no-mouse --no-256 --nth=1 --with-nth=.. --reverse --prompt (hi) --print-query] assert_equal 2000, fzf.sort assert_equal true, fzf.multi @@ -86,6 +89,7 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal 'howdy', fzf.filter assert_equal :exact, fzf.extended assert_equal [0..0], fzf.nth + assert_equal nil, fzf.with_nth assert_equal true, fzf.reverse assert_equal '(hi)', fzf.prompt assert_equal true, fzf.print_query @@ -638,6 +642,32 @@ class TestFZF < MiniTest::Unit::TestCase assert_fzf_output %w[--exit-0], '', '' end + def test_with_nth + source = "hello world\nbatman" + assert_fzf_output %w[-0 -1 --with-nth=2,1 -x -q ^worl], + source, 'hello world' + assert_fzf_output %w[-0 -1 --with-nth=2,1 -x -q llo$], + source, 'hello world' + assert_fzf_output %w[-0 -1 --with-nth=.. -x -q llo$], + source, '' + assert_fzf_output %w[-0 -1 --with-nth=2,2,2,..,1 -x -q worlworlworlhellworlhell], + source, 'hello world' + assert_fzf_output %w[-0 -1 --with-nth=1,1,-1,1 -x -q batbatbatbat], + source, 'batman' + end + + def test_with_nth_transform + fzf = FZF.new %w[--with-nth 2..,1] + assert_equal 'my world hello', fzf.transform('hello my world') + assert_equal 'my world hello', fzf.transform('hello my world') + assert_equal 'my world hello', fzf.transform('hello my world ') + + fzf = FZF.new %w[--with-nth 2,-1,2] + assert_equal 'my world my', fzf.transform('hello my world') + assert_equal 'world world world', fzf.transform('hello world') + assert_equal 'world world world', fzf.transform('hello world ') + end + def test_ranking_overlap_match_regions list = [ '1 3 4 2', From db37e675756ef51b623326d54c29a86e98781544 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 1 Nov 2014 14:52:29 +0900 Subject: [PATCH 2/6] Skip failing tests on Ruby 1.8 --- test/test_fzf.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/test_fzf.rb b/test/test_fzf.rb index 6fbd426..f61fb34 100644 --- a/test/test_fzf.rb +++ b/test/test_fzf.rb @@ -172,13 +172,12 @@ class TestFZF < MiniTest::Unit::TestCase end end - # FIXME Only on 1.9 or above def test_width fzf = FZF.new [] assert_equal 5, fzf.width('abcde') assert_equal 4, fzf.width('한글') assert_equal 5, fzf.width('한글.') - end + end if RUBY_VERSION >= '1.9' def test_trim fzf = FZF.new [] @@ -191,7 +190,7 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal ['가나a', 6], fzf.trim('가나ab라마바사.', 5, false) assert_equal ['가나ab', 5], fzf.trim('가나ab라마바사.', 6, false) assert_equal ['가나ab', 5], fzf.trim('가나ab라마바사.', 7, false) - end + end if RUBY_VERSION >= '1.9' def test_format fzf = FZF.new [] From ba9365c438a9889bbe3b0e3dc35a8961dd9b4e33 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 3 Nov 2014 23:48:37 +0900 Subject: [PATCH 3/6] Fix --with-nth option on Ruby 1.8 --- fzf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fzf b/fzf index 2304958..06d6e49 100755 --- a/fzf +++ b/fzf @@ -743,7 +743,8 @@ class FZF def pick sync do - [*@matches.fetch(@ycur, [])][0] + item = @matches[@ycur] + item.is_a?(Array) ? item[0] : item end end From 3c47b7fa5f8d8a62df3302fcbb17b003274aef08 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 3 Nov 2014 23:58:10 +0900 Subject: [PATCH 4/6] Fix --with-nth option on --multi --- fzf | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fzf b/fzf index 06d6e49..b0ffcf0 100755 --- a/fzf +++ b/fzf @@ -253,8 +253,8 @@ class FZF Regexp.compile "(?:.*?#{delim})|(?:.+?$)" end - def burp string - @stdout.puts(string.orig || string) + def burp string, orig = nil + @stdout.puts(orig || string.orig || string) end def start @@ -1050,7 +1050,7 @@ class FZF if @selects.has_key? sel @selects.delete sel else - @selects[sel] = 1 + @selects[sel] = sel.orig end end vselect { |v| v + case o @@ -1132,8 +1132,8 @@ class FZF if selects.empty? burp got else - selects.each do |sel, _| - burp sel + selects.each do |sel, orig| + burp sel, orig end end end From da03a66e6915b74256dd8a70761a0cfccabd021c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 4 Nov 2014 19:01:15 +0900 Subject: [PATCH 5/6] Add test cases for --with-nth option --- fzf | 2 +- test/test_fzf.rb | 106 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 96 insertions(+), 12 deletions(-) diff --git a/fzf b/fzf index b0ffcf0..c5fcace 100755 --- a/fzf +++ b/fzf @@ -7,7 +7,7 @@ # / __/ / /_/ __/ # /_/ /___/_/ Fuzzy finder for your shell # -# Version: 0.8.8 (Nov 1, 2014) +# Version: 0.8.8 (Nov 4, 2014) # # Author: Junegunn Choi # URL: https://github.com/junegunn/fzf diff --git a/test/test_fzf.rb b/test/test_fzf.rb index f61fb34..726c2a7 100644 --- a/test/test_fzf.rb +++ b/test/test_fzf.rb @@ -11,6 +11,47 @@ $LOAD_PATH.unshift File.expand_path('../..', __FILE__) ENV['FZF_EXECUTABLE'] = '0' load 'fzf' +class MockTTY + def initialize + @buffer = '' + @mutex = Mutex.new + @condv = ConditionVariable.new + end + + def read_nonblock sz + @mutex.synchronize do + take sz + end + end + + def take sz + if @buffer.length >= sz + ret = @buffer[0, sz] + @buffer = @buffer[sz..-1] + ret + end + end + + def getc + while true + @mutex.synchronize do + if char = take(1) + return char + else + @condv.wait(@mutex) + end + end + end + end + + def << str + @mutex.synchronize do + @buffer << str + @condv.broadcast + end + end +end + class TestFZF < MiniTest::Unit::TestCase def setup ENV.delete 'FZF_DEFAULT_SORT' @@ -566,25 +607,34 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal [[list[0], [[8, 9]]]], matcher.match(list, '^s', '', '') end - def stream_for str + def stream_for str, delay = 0 StringIO.new(str).tap do |sio| sio.instance_eval do alias org_gets gets def gets - org_gets.tap { |e| sleep 0.5 unless e.nil? } + org_gets.tap { |e| sleep(@delay) unless e.nil? } + end + + def reopen _ end end + sio.instance_variable_set :@delay, delay end end def assert_fzf_output opts, given, expected stream = stream_for given - output = StringIO.new + output = stream_for '' begin + tty = MockTTY.new $stdout = output - FZF.new(opts, stream).start + fzf = FZF.new(opts, stream) + fzf.instance_variable_set :@tty, tty + thr = block_given? && Thread.new { yield tty } + fzf.start + thr && thr.join rescue SystemExit => e assert_equal 0, e.status assert_equal expected, output.string.chomp @@ -616,15 +666,12 @@ class TestFZF < MiniTest::Unit::TestCase end def test_select_1_ambiguity - stream = stream_for "Hello\nWorld" begin - Timeout::timeout(2) do - FZF.new(%w[--query=o --select-1], stream).start + Timeout::timeout(0.5) do + assert_fzf_output %w[--query=o --select-1], "hello\nworld", "should not match" end - flunk 'Should not reach here' - rescue Exception => e + rescue Timeout::Error Curses.close_screen - assert_instance_of Timeout::Error, e end end @@ -717,13 +764,50 @@ class TestFZF < MiniTest::Unit::TestCase tmp << 'hello ' << [0xff].pack('C*') << ' world' << $/ << [0xff].pack('C*') tmp.close begin - Timeout::timeout(1) do + Timeout::timeout(0.5) do FZF.new(%w[-n..,1,2.. -q^ -x], File.open(tmp.path)).start end rescue Timeout::Error + Curses.close_screen end ensure tmp.unlink end + + def test_with_nth_mock_tty + d = 0.1 + # Manual selection with input + assert_fzf_output ["--with-nth=2,1"], "hello world", "hello world" do |tty| + tty << "world" + tty << "hell" + sleep d + tty << "\r" + end + + # Manual selection without input + assert_fzf_output ["--with-nth=2,1"], "hello world", "hello world" do |tty| + sleep d + tty << "\r" + end + + # Manual selection with input and --multi + lines = "hello world\ngoodbye world" + assert_fzf_output %w[-m --with-nth=2,1], lines, lines do |tty| + sleep d + tty << "o" + sleep d + tty << "\e[Z\e[Z" + sleep d + tty << "\r" + end + + # Manual selection without input and --multi + assert_fzf_output %w[-m --with-nth=2,1], lines, lines do |tty| + sleep d + tty << "\e[Z\e[Z" + sleep d + tty << "\r" + end + end end From 7571baadb4c56b1d4bee8732b98c9ca7f3948f67 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 4 Nov 2014 19:32:31 +0900 Subject: [PATCH 6/6] Fix test failure on Ruby 1.8.7 Hashes are unordered on Ruby 1.8 --- test/test_fzf.rb | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/test/test_fzf.rb b/test/test_fzf.rb index 726c2a7..2bb7dce 100644 --- a/test/test_fzf.rb +++ b/test/test_fzf.rb @@ -33,6 +33,7 @@ class MockTTY end def getc + sleep 0.1 while true @mutex.synchronize do if char = take(1) @@ -627,6 +628,10 @@ class TestFZF < MiniTest::Unit::TestCase stream = stream_for given output = stream_for '' + def sorted_lines line + line.split($/).sort + end + begin tty = MockTTY.new $stdout = output @@ -637,7 +642,7 @@ class TestFZF < MiniTest::Unit::TestCase thr && thr.join rescue SystemExit => e assert_equal 0, e.status - assert_equal expected, output.string.chomp + assert_equal sorted_lines(expected), sorted_lines(output.string) ensure $stdout = STDOUT end @@ -775,37 +780,29 @@ class TestFZF < MiniTest::Unit::TestCase end def test_with_nth_mock_tty - d = 0.1 # Manual selection with input assert_fzf_output ["--with-nth=2,1"], "hello world", "hello world" do |tty| tty << "world" tty << "hell" - sleep d tty << "\r" end # Manual selection without input assert_fzf_output ["--with-nth=2,1"], "hello world", "hello world" do |tty| - sleep d tty << "\r" end # Manual selection with input and --multi lines = "hello world\ngoodbye world" assert_fzf_output %w[-m --with-nth=2,1], lines, lines do |tty| - sleep d tty << "o" - sleep d tty << "\e[Z\e[Z" - sleep d tty << "\r" end # Manual selection without input and --multi assert_fzf_output %w[-m --with-nth=2,1], lines, lines do |tty| - sleep d tty << "\e[Z\e[Z" - sleep d tty << "\r" end end