diff --git a/README.md b/README.md index 0faed38..61e83fe 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ usage: fzf [options] -i Case-insensitive match (default: smart-case match) +i Case-sensitive match +c, --no-color Disable colors + --no-mouse Disable mouse Environment variables FZF_DEFAULT_COMMAND Default command to use when input is tty @@ -103,6 +104,9 @@ The following readline key bindings should also work as expected. If you enable multi-select mode with `-m` option, you can select multiple items with TAB or Shift-TAB key. +You can also use mouse. Click on an item to select it or shift-click to select +multiple items. Use mouse wheel to move the cursor up and down. + ### Extended-search mode With `-x` or `--extended` option, fzf will start in "extended-search mode". diff --git a/fzf b/fzf index 5decd48..7528aab 100755 --- a/fzf +++ b/fzf @@ -7,7 +7,7 @@ # / __/ / /_/ __/ # /_/ /___/_/ Fuzzy finder for your shell # -# Version: 0.6.2-devel (January 22, 2014) +# Version: 0.7.0 (January 30, 2014) # # Author: Junegunn Choi # URL: https://github.com/junegunn/fzf @@ -40,9 +40,17 @@ require 'thread' require 'curses' require 'set' +unless String.method_defined? :force_encoding + class String + def force_encoding *arg + self + end + end +end + class FZF C = Curses - attr_reader :rxflag, :sort, :color, :multi, :query, :extended + attr_reader :rxflag, :sort, :color, :mouse, :multi, :query, :extended class AtomicVar def initialize value @@ -73,6 +81,7 @@ class FZF @color = true @multi = false @extended = false + @mouse = true argv = if opts = ENV['FZF_DEFAULT_OPTS'] @@ -93,6 +102,7 @@ class FZF when '+i' then @rxflag = 0 when '-c', '--color' then @color = true when '+c', '--no-color' then @color = false + when '--no-mouse' then @mouse = false when '+s', '--no-sort' then @sort = nil when '-q', '--query' usage 1, 'query string required' unless query = argv.shift @@ -110,7 +120,7 @@ class FZF end end - @source = source + @source = source.clone @mtx = Mutex.new @cv = ConditionVariable.new @events = {} @@ -163,6 +173,7 @@ class FZF -i Case-insensitive match (default: smart-case match) +i Case-sensitive match +c, --no-color Disable colors + --no-mouse Disable mouse Environment variables FZF_DEFAULT_COMMAND Default command to use when input is tty @@ -463,6 +474,11 @@ class FZF def init_screen C.init_screen + if @mouse + C.mouseinterval 0 + C.mousemask C::ALL_MOUSE_EVENTS + end + C.stdscr.keypad(true) C.start_color dbg = if C.respond_to?(:use_default_colors) @@ -472,6 +488,7 @@ class FZF C::COLOR_BLACK end C.raw + C.nonl C.noecho if @color @@ -527,6 +544,7 @@ class FZF exit 1 end else + $stdin.reopen IO.open(IO.sysopen('/dev/tty'), 'r') @source end @@ -665,6 +683,7 @@ class FZF def start_renderer Thread.new do begin + refresh while blk = @queue.shift blk.call refresh @@ -680,10 +699,32 @@ class FZF nil end + def vselect &prc + @vcursor.set { |v| @vcursors << v; prc.call v } + update_list false + end + + def num_unicode_bytes chr + # http://en.wikipedia.org/wiki/UTF-8 + if chr & 0b10000000 > 0 + bytes = 0 + 7.downto(2) do |shift| + break if (chr >> shift) & 0x1 == 0 + bytes += 1 + end + bytes + else + 1 + end + end + + def test_mouse st, *states + states.any? { |s| s & st > 0 } + end + def start_loop got = nil begin - tty = IO.open(IO.sysopen('/dev/tty'), 'r') input = @query.get.dup cursor = input.length backword = proc { @@ -699,74 +740,115 @@ class FZF ctrl(:u) => proc { input = input[cursor..-1]; cursor = 0 }, ctrl(:a) => proc { cursor = 0; nil }, ctrl(:e) => proc { cursor = input.length; nil }, - ctrl(:j) => proc { @vcursor.set { |v| @vcursors << v; v - 1 }; update_list false }, - ctrl(:k) => proc { @vcursor.set { |v| @vcursors << v; v + 1 }; update_list false }, + ctrl(:j) => proc { vselect { |v| v - 1 } }, + ctrl(:k) => proc { vselect { |v| v + 1 } }, ctrl(:w) => proc { pcursor = cursor backword.call input = input[0...cursor] + input[pcursor..-1] }, - 127 => proc { input[cursor -= 1] = '' if cursor > 0 }, - 9 => proc { |o| + ctrl(:h) => proc { input[cursor -= 1] = '' if cursor > 0 }, + ctrl(:i) => proc { |o| if @multi && sel = pick if @selects.has_key? sel @selects.delete sel else @selects[sel] = 1 end - @vcursor.set { |v| - @vcursors << v - v + (o == :stab ? 1 : -1) + vselect { |v| + v + case o + when :select then 0 + when C::KEY_BTAB then 1 + else -1 + end } - update_list false end }, - :left => proc { cursor = [0, cursor - 1].max; nil }, - :right => proc { cursor = [input.length, cursor + 1].min; nil }, + ctrl(:b) => proc { cursor = [0, cursor - 1].max; nil }, + ctrl(:f) => proc { cursor = [input.length, cursor + 1].min; nil }, :alt_b => proc { backword.call; nil }, :alt_f => proc { cursor += (input[cursor..-1].index(/(\S\s)|(.$)/) || -1) + 1 nil }, } - actions[ctrl(:b)] = actions[:left] - actions[ctrl(:f)] = actions[:right] - actions[ctrl(:h)] = actions[127] - actions[ctrl(:n)] = actions[ctrl(:j)] - actions[ctrl(:p)] = actions[ctrl(:k)] + actions[C::KEY_UP] = actions[ctrl(:p)] = actions[ctrl(:k)] + actions[C::KEY_DOWN] = actions[ctrl(:n)] = actions[ctrl(:j)] + actions[C::KEY_LEFT] = actions[ctrl(:b)] + actions[C::KEY_RIGHT] = actions[ctrl(:f)] + actions[C::KEY_BTAB] = actions[:select] = actions[ctrl(:i)] + actions[C::KEY_BACKSPACE] = actions[127] = actions[ctrl(:h)] actions[ctrl(:q)] = actions[ctrl(:g)] = actions[ctrl(:c)] = actions[:esc] - actions[:stab] = actions[9] emit(:key) { [@query.get, cursor] } unless @query.empty? + pmv = nil while true @cursor_x.set cursor render { print_input } - ord = tty.getc.ord - ord = - case ord = (tty.read_nonblock(1).ord rescue :esc) - when 91 - case (tty.read_nonblock(1).ord rescue nil) - when 68 then :left - when 67 then :right - when 66 then ctrl(:j) - when 65 then ctrl(:k) - when 90 then :stab - else next - end - when 'b'.ord then :alt_b - when 'f'.ord then :alt_f - when :esc then :esc - else next - end if ord == 27 + C.stdscr.timeout = -1 + ch = C.getch - upd = actions.fetch(ord, proc { |ord| - char = [ord].pack('U*') - if char =~ /[[:print:]]/ - input.insert cursor, char + case ch + when C::KEY_MOUSE + if m = C.getmouse + st = m.bstate + if test_mouse(st, C::BUTTON1_PRESSED, C::BUTTON1_RELEASED) + if m.y == cursor_y + # TODO Wide-characters + cursor = [0, [input.length, m.x - 2].min].max + elsif m.x > 1 && m.y <= max_items + vselect { |v| + tv = max_items - m.y - 1 + if test_mouse(st, C::BUTTON1_RELEASED) + if test_mouse(st, C::BUTTON_SHIFT) + ch = :select + elsif pmv == tv + ch = ctrl(:m) + end + pmv = tv + end + tv + } + end + elsif test_mouse(st, 0x8000000, C::BUTTON2_PRESSED) + ch = C::KEY_DOWN + elsif test_mouse(st, C::BUTTON4_PRESSED) + ch = C::KEY_UP + end + end + when 27 + C.stdscr.timeout = 0 + ch = + case ch2 = C.getch + when 'b' then :alt_b + when 'f' then :alt_f + when nil then :esc + else + ch2 + end + end + + upd = actions.fetch(ch, proc { |ch| + if ch.is_a?(Fixnum) + # Ruby 1.8 + if (ch.chr rescue '') =~ /[[:print:]]/ + ch = ch.chr + elsif (nch = num_unicode_bytes(ch)) > 1 + chs = [ch] + (nch - 1).times do |i| + chs << C.getch + end + # UTF-8 TODO Ruby 1.8 + ch = chs.pack('C*').force_encoding('UTF-8') + end + end + + if ch.is_a?(String) && ch =~ /[[:print:]]/ + input.insert cursor, ch cursor += 1 end - }).call(ord) + }).call(ch) # Dispatch key event emit(:key) { [@query.set(input.dup), cursor] } if upd diff --git a/fzf.gemspec b/fzf.gemspec index c61b9e9..c3cd8cd 100644 --- a/fzf.gemspec +++ b/fzf.gemspec @@ -1,7 +1,7 @@ # coding: utf-8 Gem::Specification.new do |spec| spec.name = 'fzf' - spec.version = '0.6.2' + spec.version = '0.7.0' spec.authors = ['Junegunn Choi'] spec.email = ['junegunn.c@gmail.com'] spec.description = %q{Fuzzy finder for your shell} diff --git a/test/test_fzf.rb b/test/test_fzf.rb index 7310735..3f61f0f 100644 --- a/test/test_fzf.rb +++ b/test/test_fzf.rb @@ -20,6 +20,7 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal false, fzf.multi assert_equal true, fzf.color assert_equal nil, fzf.rxflag + assert_equal true, fzf.mouse end def test_environment_variables @@ -28,7 +29,7 @@ class TestFZF < MiniTest::Unit::TestCase fzf = FZF.new [] assert_equal 20000, fzf.sort - ENV['FZF_DEFAULT_OPTS'] = '-x -m -s 10000 -q " hello world " +c' + ENV['FZF_DEFAULT_OPTS'] = '-x -m -s 10000 -q " hello world " +c --no-mouse' fzf = FZF.new [] assert_equal 10000, fzf.sort assert_equal ' hello world ', @@ -36,14 +37,16 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal true, fzf.extended assert_equal true, fzf.multi assert_equal false, fzf.color + assert_equal false, fzf.mouse end def test_option_parser # Long opts - fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello --extended] + fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello --extended --no-mouse] assert_equal 2000, fzf.sort assert_equal true, fzf.multi assert_equal false, fzf.color + assert_equal false, fzf.mouse assert_equal 0, fzf.rxflag assert_equal 'hello', fzf.query.get assert_equal true, fzf.extended @@ -53,6 +56,7 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal nil, fzf.sort assert_equal false, fzf.multi assert_equal true, fzf.color + assert_equal true, fzf.mouse assert_equal 1, fzf.rxflag assert_equal 'hello', fzf.query.get assert_equal false, fzf.extended