Merge pull request #17 from junegunn/mouse

Add mouse support
This commit is contained in:
Junegunn Choi 2014-01-30 07:37:01 -08:00
commit 7280e8ebc2
4 changed files with 135 additions and 45 deletions

View File

@ -60,6 +60,7 @@ usage: fzf [options]
-i Case-insensitive match (default: smart-case match) -i Case-insensitive match (default: smart-case match)
+i Case-sensitive match +i Case-sensitive match
+c, --no-color Disable colors +c, --no-color Disable colors
--no-mouse Disable mouse
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty 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 If you enable multi-select mode with `-m` option, you can select multiple items
with TAB or Shift-TAB key. 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 ### Extended-search mode
With `-x` or `--extended` option, fzf will start in "extended-search mode". With `-x` or `--extended` option, fzf will start in "extended-search mode".

166
fzf
View File

@ -7,7 +7,7 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ Fuzzy finder for your shell # /_/ /___/_/ Fuzzy finder for your shell
# #
# Version: 0.6.2-devel (January 22, 2014) # Version: 0.7.0 (January 30, 2014)
# #
# Author: Junegunn Choi # Author: Junegunn Choi
# URL: https://github.com/junegunn/fzf # URL: https://github.com/junegunn/fzf
@ -40,9 +40,17 @@ require 'thread'
require 'curses' require 'curses'
require 'set' require 'set'
unless String.method_defined? :force_encoding
class String
def force_encoding *arg
self
end
end
end
class FZF class FZF
C = Curses C = Curses
attr_reader :rxflag, :sort, :color, :multi, :query, :extended attr_reader :rxflag, :sort, :color, :mouse, :multi, :query, :extended
class AtomicVar class AtomicVar
def initialize value def initialize value
@ -73,6 +81,7 @@ class FZF
@color = true @color = true
@multi = false @multi = false
@extended = false @extended = false
@mouse = true
argv = argv =
if opts = ENV['FZF_DEFAULT_OPTS'] if opts = ENV['FZF_DEFAULT_OPTS']
@ -93,6 +102,7 @@ class FZF
when '+i' then @rxflag = 0 when '+i' then @rxflag = 0
when '-c', '--color' then @color = true when '-c', '--color' then @color = true
when '+c', '--no-color' then @color = false when '+c', '--no-color' then @color = false
when '--no-mouse' then @mouse = false
when '+s', '--no-sort' then @sort = nil when '+s', '--no-sort' then @sort = nil
when '-q', '--query' when '-q', '--query'
usage 1, 'query string required' unless query = argv.shift usage 1, 'query string required' unless query = argv.shift
@ -110,7 +120,7 @@ class FZF
end end
end end
@source = source @source = source.clone
@mtx = Mutex.new @mtx = Mutex.new
@cv = ConditionVariable.new @cv = ConditionVariable.new
@events = {} @events = {}
@ -163,6 +173,7 @@ class FZF
-i Case-insensitive match (default: smart-case match) -i Case-insensitive match (default: smart-case match)
+i Case-sensitive match +i Case-sensitive match
+c, --no-color Disable colors +c, --no-color Disable colors
--no-mouse Disable mouse
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
@ -463,6 +474,11 @@ class FZF
def init_screen def init_screen
C.init_screen C.init_screen
if @mouse
C.mouseinterval 0
C.mousemask C::ALL_MOUSE_EVENTS
end
C.stdscr.keypad(true)
C.start_color C.start_color
dbg = dbg =
if C.respond_to?(:use_default_colors) if C.respond_to?(:use_default_colors)
@ -472,6 +488,7 @@ class FZF
C::COLOR_BLACK C::COLOR_BLACK
end end
C.raw C.raw
C.nonl
C.noecho C.noecho
if @color if @color
@ -527,6 +544,7 @@ class FZF
exit 1 exit 1
end end
else else
$stdin.reopen IO.open(IO.sysopen('/dev/tty'), 'r')
@source @source
end end
@ -665,6 +683,7 @@ class FZF
def start_renderer def start_renderer
Thread.new do Thread.new do
begin begin
refresh
while blk = @queue.shift while blk = @queue.shift
blk.call blk.call
refresh refresh
@ -680,10 +699,32 @@ class FZF
nil nil
end 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 def start_loop
got = nil got = nil
begin begin
tty = IO.open(IO.sysopen('/dev/tty'), 'r')
input = @query.get.dup input = @query.get.dup
cursor = input.length cursor = input.length
backword = proc { backword = proc {
@ -699,74 +740,115 @@ class FZF
ctrl(:u) => proc { input = input[cursor..-1]; cursor = 0 }, ctrl(:u) => proc { input = input[cursor..-1]; cursor = 0 },
ctrl(:a) => proc { cursor = 0; nil }, ctrl(:a) => proc { cursor = 0; nil },
ctrl(:e) => proc { cursor = input.length; nil }, ctrl(:e) => proc { cursor = input.length; nil },
ctrl(:j) => proc { @vcursor.set { |v| @vcursors << v; v - 1 }; update_list false }, ctrl(:j) => proc { vselect { |v| v - 1 } },
ctrl(:k) => proc { @vcursor.set { |v| @vcursors << v; v + 1 }; update_list false }, ctrl(:k) => proc { vselect { |v| v + 1 } },
ctrl(:w) => proc { ctrl(:w) => proc {
pcursor = cursor pcursor = cursor
backword.call backword.call
input = input[0...cursor] + input[pcursor..-1] input = input[0...cursor] + input[pcursor..-1]
}, },
127 => proc { input[cursor -= 1] = '' if cursor > 0 }, ctrl(:h) => proc { input[cursor -= 1] = '' if cursor > 0 },
9 => proc { |o| ctrl(:i) => proc { |o|
if @multi && sel = pick if @multi && sel = pick
if @selects.has_key? sel if @selects.has_key? sel
@selects.delete sel @selects.delete sel
else else
@selects[sel] = 1 @selects[sel] = 1
end end
@vcursor.set { |v| vselect { |v|
@vcursors << v v + case o
v + (o == :stab ? 1 : -1) when :select then 0
when C::KEY_BTAB then 1
else -1
end
} }
update_list false
end end
}, },
:left => proc { cursor = [0, cursor - 1].max; nil }, ctrl(:b) => proc { cursor = [0, cursor - 1].max; nil },
:right => proc { cursor = [input.length, cursor + 1].min; nil }, ctrl(:f) => proc { cursor = [input.length, cursor + 1].min; nil },
:alt_b => proc { backword.call; nil }, :alt_b => proc { backword.call; nil },
:alt_f => proc { :alt_f => proc {
cursor += (input[cursor..-1].index(/(\S\s)|(.$)/) || -1) + 1 cursor += (input[cursor..-1].index(/(\S\s)|(.$)/) || -1) + 1
nil nil
}, },
} }
actions[ctrl(:b)] = actions[:left] actions[C::KEY_UP] = actions[ctrl(:p)] = actions[ctrl(:k)]
actions[ctrl(:f)] = actions[:right] actions[C::KEY_DOWN] = actions[ctrl(:n)] = actions[ctrl(:j)]
actions[ctrl(:h)] = actions[127] actions[C::KEY_LEFT] = actions[ctrl(:b)]
actions[ctrl(:n)] = actions[ctrl(:j)] actions[C::KEY_RIGHT] = actions[ctrl(:f)]
actions[ctrl(:p)] = actions[ctrl(:k)] 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[ctrl(:q)] = actions[ctrl(:g)] = actions[ctrl(:c)] = actions[:esc]
actions[:stab] = actions[9]
emit(:key) { [@query.get, cursor] } unless @query.empty? emit(:key) { [@query.get, cursor] } unless @query.empty?
pmv = nil
while true while true
@cursor_x.set cursor @cursor_x.set cursor
render { print_input } render { print_input }
ord = tty.getc.ord C.stdscr.timeout = -1
ord = ch = C.getch
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
upd = actions.fetch(ord, proc { |ord| case ch
char = [ord].pack('U*') when C::KEY_MOUSE
if char =~ /[[:print:]]/ if m = C.getmouse
input.insert cursor, char 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 cursor += 1
end end
}).call(ord) }).call(ch)
# Dispatch key event # Dispatch key event
emit(:key) { [@query.set(input.dup), cursor] } if upd emit(:key) { [@query.set(input.dup), cursor] } if upd

View File

@ -1,7 +1,7 @@
# coding: utf-8 # coding: utf-8
Gem::Specification.new do |spec| Gem::Specification.new do |spec|
spec.name = 'fzf' spec.name = 'fzf'
spec.version = '0.6.2' spec.version = '0.7.0'
spec.authors = ['Junegunn Choi'] spec.authors = ['Junegunn Choi']
spec.email = ['junegunn.c@gmail.com'] spec.email = ['junegunn.c@gmail.com']
spec.description = %q{Fuzzy finder for your shell} spec.description = %q{Fuzzy finder for your shell}

View File

@ -20,6 +20,7 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal false, fzf.multi assert_equal false, fzf.multi
assert_equal true, fzf.color assert_equal true, fzf.color
assert_equal nil, fzf.rxflag assert_equal nil, fzf.rxflag
assert_equal true, fzf.mouse
end end
def test_environment_variables def test_environment_variables
@ -28,7 +29,7 @@ class TestFZF < MiniTest::Unit::TestCase
fzf = FZF.new [] fzf = FZF.new []
assert_equal 20000, fzf.sort 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 [] fzf = FZF.new []
assert_equal 10000, fzf.sort assert_equal 10000, fzf.sort
assert_equal ' hello world ', assert_equal ' hello world ',
@ -36,14 +37,16 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal true, fzf.extended assert_equal true, fzf.extended
assert_equal true, fzf.multi assert_equal true, fzf.multi
assert_equal false, fzf.color assert_equal false, fzf.color
assert_equal false, fzf.mouse
end end
def test_option_parser def test_option_parser
# Long opts # 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 2000, fzf.sort
assert_equal true, fzf.multi assert_equal true, fzf.multi
assert_equal false, fzf.color assert_equal false, fzf.color
assert_equal false, fzf.mouse
assert_equal 0, fzf.rxflag assert_equal 0, fzf.rxflag
assert_equal 'hello', fzf.query.get assert_equal 'hello', fzf.query.get
assert_equal true, fzf.extended assert_equal true, fzf.extended
@ -53,6 +56,7 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal nil, fzf.sort assert_equal nil, fzf.sort
assert_equal false, fzf.multi assert_equal false, fzf.multi
assert_equal true, fzf.color assert_equal true, fzf.color
assert_equal true, fzf.mouse
assert_equal 1, fzf.rxflag assert_equal 1, fzf.rxflag
assert_equal 'hello', fzf.query.get assert_equal 'hello', fzf.query.get
assert_equal false, fzf.extended assert_equal false, fzf.extended