#!/usr/bin/env ruby # encoding: utf-8 # # ____ ____ # / __/___ / __/ # / /_/_ / / /_ # / __/ / /_/ __/ # /_/ /___/_/ Fuzzy finder for your shell # # Version: 0.8.9 (Dec 24, 2014) # Deprecation alert: # This script is no longer maintained. Use the new Go version. # # Author: Junegunn Choi # URL: https://github.com/junegunn/fzf # License: MIT # # Copyright (c) 2014 Junegunn Choi # # MIT License # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. begin require 'curses' rescue LoadError $stderr.puts 'curses gem is not installed. Try `gem install curses`.' sleep 1 exit 1 end require 'thread' require 'set' unless String.method_defined? :force_encoding class String def force_encoding *arg self end 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, :with_nth def sync @shr_mtx.synchronize { yield } end def get name sync { instance_variable_get name } end def geta(*names) sync { names.map { |name| instance_variable_get name } } end def call(name, method, *args) sync { instance_variable_get(name).send(method, *args) } end def set name, value = nil sync do instance_variable_set name, (block_given? ? yield(instance_variable_get(name)) : value) end end def initialize argv, source = $stdin @rxflag = nil @sort = ENV.fetch('FZF_DEFAULT_SORT', 1000).to_i @color = true @ansi256 = true @black = false @multi = false @mouse = true @extended = nil @select1 = false @exit0 = false @filter = nil @nth = nil @with_nth = nil @delim = nil @reverse = false @prompt = '> ' @shr_mtx = Mutex.new @expect = false @print_query = false argv = if opts = ENV['FZF_DEFAULT_OPTS'] require 'shellwords' Shellwords.shellwords(opts) + argv else argv.dup end while o = argv.shift case o when '--version' then FZF.version when '-h', '--help' then usage 0 when '-m', '--multi' then @multi = true when '+m', '--no-multi' then @multi = false when '-x', '--extended' then @extended = :fuzzy when '+x', '--no-extended' then @extended = nil when '-i' then @rxflag = Regexp::IGNORECASE when '+i' then @rxflag = 0 when '-c', '--color' then @color = true when '+c', '--no-color' then @color = false when '-2', '--256' then @ansi256 = true when '+2', '--no-256' then @ansi256 = false when '--black' then @black = true when '--no-black' then @black = false when '--mouse' then @mouse = true when '--no-mouse' then @mouse = false when '--reverse' then @reverse = true when '--no-reverse' then @reverse = false when '+s', '--no-sort' then @sort = nil when '-1', '--select-1' then @select1 = true when '+1', '--no-select-1' then @select1 = false when '-0', '--exit-0' then @exit0 = true when '+0', '--no-exit-0' then @exit0 = false when '-q', '--query' usage 1, 'query string required' unless query = argv.shift @query = query when /^-q(.*)$/, /^--query=(.*)$/ @query = $1 when '-f', '--filter' usage 1, 'query string required' unless query = argv.shift @filter = query when /^-f(.*)$/, /^--filter=(.*)$/ @filter = $1 when '-n', '--nth' usage 1, 'field expression required' unless nth = argv.shift @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 when /^-d(.+)$/, /^--delimiter=(.+)$/ @delim = FZF.build_delim_regex $1 when '-s', '--sort' usage 1, 'sort size required' unless sort = argv.shift usage 1, 'invalid sort size' unless sort =~ /^[0-9]+$/ @sort = sort.to_i when /^-s([0-9]+)$/, /^--sort=([0-9]+)$/ @sort = $1.to_i when '--prompt' usage 1, 'prompt string required' unless prompt = argv.shift @prompt = prompt when /^--prompt=(.*)$/ @prompt = $1 when '--print-query' then @print_query = true when '--no-print-query' then @print_query = false when '-e', '--extended-exact' then @extended = :exact when '+e', '--no-extended-exact' then @extended = nil when '--expect' argv.shift @expect = true when /^--expect=(.*)$/ @expect = true when '--toggle-sort', '--tiebreak', '--color', '--bind' argv.shift when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll', '--inline-info', '--no-inline-info', /^--bind=(.*)$/, /^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/ # XXX else usage 1, "illegal option: #{o}" end end @source = source.clone @evt_mtx = Mutex.new @cv = ConditionVariable.new @events = {} @new = [] @queue = Queue.new @pending = nil @rev_dir = @reverse ? -1 : 1 @stdout = $stdout.clone unless @filter # Shared variables: needs protection @query ||= '' @matches = [] @count = 0 @xcur = @query.length @ycur = 0 @yoff = 0 @dirty = Set.new @spinner = '-\|/-\|/'.split(//) @selects = {} # ordered >= 1.9 @main = Thread.current @plcount = 0 end end def parse_nth nth 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 || second && !second.empty? && (second.to_i == 0 || second.include?('.')) first = first.empty? ? 1 : first.to_i second = case second when nil then first when '' then -1 else second.to_i end Range.new(*[first, second].map { |e| e > 0 ? e - 1 : e }) } ranges == [0..-1] ? nil : ranges end def FZF.build_delim_regex delim Regexp.compile(delim) rescue (delim = Regexp.escape(delim)) Regexp.compile "(?:.*?#{delim})|(?:.+?$)" end def burp string, orig = nil @stdout.puts(orig || string.orig || string) end def start if @filter start_reader.join filter_list @new else start_reader query = get(:@query) emit(:key) { [query, query.length] } unless empty = query.empty? if @select1 || @exit0 start_search do |loaded, matches| len = empty ? get(:@count) : matches.length if loaded if @select1 && len == 1 puts @query if @print_query puts if @expect burp(empty ? matches.first : matches.first.first) exit 0 elsif @exit0 && len == 0 puts @query if @print_query puts if @expect exit 0 end end if loaded || len > 1 start_renderer Thread.new { start_loop } end end sleep else start_search start_renderer start_loop end end end def filter_list list puts @filter if @print_query matches = matcher.match(list, @filter, '', '') if @sort && matches.length <= @sort matches = FZF.sort(matches) end matches.each { |m| puts m.first } end def matcher @matcher ||= if @extended ExtendedFuzzyMatcher.new @rxflag, @extended, @nth, @delim else FuzzyMatcher.new @rxflag, @nth, @delim end end class << self def version File.open(__FILE__, 'r') do |f| f.each_line do |line| if line =~ /Version: (.*)/ $stdout.puts 'fzf ' << $1 exit end end end end def sort list list.sort_by { |tuple| rank tuple } end def rank tuple line, offsets = tuple matchlen = 0 pe = 0 offsets.sort.each do |pair| b, e = pair b = pe if pe > b pe = e if e > pe matchlen += e - b if e > b end [matchlen, line.length, line] end end def usage x, message = nil $stderr.puts message if message $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]) --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. 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: '> ') 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 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 end def emit event @evt_mtx.synchronize do @events[event] = yield @cv.broadcast end end def max_items; C.lines - 2; end def cursor_y offset = 0 @reverse ? (offset) : (C.lines - 1 - offset) end def cprint str, col C.attron(col) do addstr_safe str end if str end def addstr_safe str str = str.gsub("\0", '') rescue str C.addstr str end def print_input C.setpos cursor_y, 0 C.clrtoeol cprint @prompt, color(:prompt, true) C.attron(C::A_BOLD) do C.addstr get(:@query) end end def print_info msg = nil C.setpos cursor_y(1), 0 C.clrtoeol prefix = if spin_char = call(:@spinner, :first) cprint spin_char, color(:spinner, true) ' ' else ' ' end C.attron color(:info, false) do sync do C.addstr "#{prefix}#{@matches.length}/#{@count}" if (selected = @selects.length) > 0 C.addstr " (#{selected})" end end C.addstr msg if msg end end def refresh query, xcur = geta(:@query, :@xcur) C.setpos cursor_y, @prompt.length + width(query[0, xcur]) C.refresh end def ctrl char char.to_s.ord - 'a'.ord + 1 end def format line, limit, offsets offsets ||= [] maxe = offsets.map { |e| e.last }.max || 0 # Overflow if width(line) > limit ewidth = width(line[0...maxe]) # Stri.. if ewidth <= limit - 2 line, _ = trim line, limit - 2, false line << '..' # ..ring else # ..ri.. line = line[0...maxe] + '..' if ewidth < width(line) - 2 line, diff = trim line, limit - 2, true offsets = offsets.map { |pair| b, e = pair b += 2 - diff e += 2 - diff b = [2, b].max [b, e] } line = '..' + line end end tokens = [] index = 0 offsets.select { |pair| pair.first < pair.last }. sort_by { |pair| pair }.each do |pair| b, e = pair.map { |x| [index, x].max } tokens << [line[index...b], false] tokens << [line[b...e], true] index = e end tokens << [line[index..-1], false] if index < line.length tokens.reject { |pair| pair.first.empty? } end def print_item row, tokens, chosen, selected # Cursor C.setpos row, 0 C.clrtoeol cprint chosen ? '>' : ' ', color(:cursor, true) cprint selected ? '>' : ' ', chosen ? color(:chosen) : (selected ? color(:selected, true) : 0) # Highlighted item C.attron color(:chosen, true) if chosen tokens.each do |pair| token, highlighted = pair if highlighted cprint token, color(chosen ? :match! : :match, chosen) C.attron color(:chosen, true) if chosen else addstr_safe token end end C.attroff color(:chosen, true) if chosen end AFTER_1_9 = RUBY_VERSION.split('.').map { |e| e.rjust(3, '0') }.join >= '001009' if AFTER_1_9 @@wrx = Regexp.new '\p{Han}|\p{Katakana}|\p{Hiragana}|\p{Hangul}' def width str str.gsub(@@wrx, ' ').length rescue str.length end def trim str, len, left width = width str diff = 0 while width > len width -= ((left ? str[0, 1] : str[-1, 1]) =~ @@wrx ? 2 : 1) rescue 1 str = left ? str[1..-1] : str[0...-1] diff += 1 end [str, diff] end else def width str str.length end def trim str, len, left diff = str.length - len if diff > 0 [left ? str[diff..-1] : str[0...-diff], diff] else [str, 0] end end class ::String def ord self.unpack('c').first end end class ::Fixnum def ord self end end end def init_screen $stdout.reopen($stderr) C.init_screen C.mousemask C::ALL_MOUSE_EVENTS if @mouse C.start_color dbg = if !@black && C.respond_to?(:use_default_colors) C.use_default_colors -1 else C::COLOR_BLACK end C.raw C.noecho if @color if @ansi256 && ENV['TERM'].to_s =~ /256/ C.init_pair 1, 110, dbg C.init_pair 2, 108, dbg C.init_pair 3, 254, 236 C.init_pair 4, 151, 236 C.init_pair 5, 148, dbg C.init_pair 6, 144, dbg C.init_pair 7, 161, 236 C.init_pair 8, 168, 236 else C.init_pair 1, C::COLOR_BLUE, dbg C.init_pair 2, C::COLOR_GREEN, dbg C.init_pair 3, C::COLOR_YELLOW, C::COLOR_BLACK C.init_pair 4, C::COLOR_GREEN, C::COLOR_BLACK C.init_pair 5, C::COLOR_GREEN, dbg C.init_pair 6, C::COLOR_WHITE, dbg C.init_pair 7, C::COLOR_RED, C::COLOR_BLACK C.init_pair 8, C::COLOR_MAGENTA, C::COLOR_BLACK end def self.color sym, bold = false C.color_pair([:prompt, :match, :chosen, :match!, :spinner, :info, :cursor, :selected].index(sym) + 1) | (bold ? C::A_BOLD : 0) end else def self.color sym, bold = false case sym when :chosen bold ? C::A_REVERSE : 0 when :match C::A_UNDERLINE when :match! C::A_REVERSE | C::A_UNDERLINE else 0 end | (bold ? C::A_BOLD : 0) end end C.refresh end def start_reader stream = if @source.tty? default_command = ENV['FZF_DEFAULT_COMMAND'] if default_command && !default_command.empty? IO.popen(default_command) elsif !`which find`.empty? IO.popen("find * -path '*/\\.*' -prune -o -type f -print -o -type l -print 2> /dev/null") else exit 1 end else @source end Thread.new do 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 = [] events = {} fcache = {} q = '' delay = -5 begin while true @evt_mtx.synchronize do while true events.merge! @events if @events.empty? # No new events @cv.wait @evt_mtx next end @events.clear break end if events[:new] lists << @new set(:@count) { |c| c + @new.length } set(:@spinner) { |spinner| if e = spinner.shift spinner.push e end; spinner } @new = [] fcache.clear end end#mtx new_search = events[:key] || events.delete(:new) user_input = events[:key] progress = 0 started_at = Time.now if updated = new_search && !lists.empty? q, cx = events.delete(:key) || [q, 0] empty = matcher.empty?(q) unless matches = fcache[q] found = [] skip = false cnt = 0 lists.each do |list| cnt += list.length skip = @evt_mtx.synchronize { @events[:key] } break if skip if !empty && (progress = 100 * cnt / get(:@count)) < 100 && Time.now - started_at > 0.5 render { print_info " (#{progress}%)" } end found.concat(q.empty? ? list : matcher.match(list, q, q[0, cx], q[cx..-1])) end if skip sleep 0.1 next end matches = @sort ? found : found.reverse if !empty && @sort && matches.length <= @sort matches = FZF.sort(matches) end fcache[q] = matches end # Atomic update set(:@matches, matches) end#new_search callback = nil if callback && (updated || events[:loaded]) && callback.call(events[:loaded], matches) # This small delay reduces the number of partial lists sleep((delay = [20, delay + 5].min) * 0.01) unless user_input update_list new_search end#while rescue Exception => e @main.raise e end end end def pick sync do item = @matches[@ycur] item.is_a?(Array) ? item[0] : item end end def constrain offset, cursor, count, height original = [offset, cursor] diffpos = cursor - offset # Constrain cursor cursor = [0, [cursor, count - 1].min].max # Ceil if cursor > offset + (height - 1) offset = cursor - (height - 1) # Floor elsif offset > cursor offset = cursor end # Adjustment if count - offset < height offset = [0, count - height].max cursor = [0, [offset + diffpos, count - 1].min].max end [[offset, cursor] != original, offset, cursor] end def update_list wipe render do pos, items = sync { changed, @yoff, @ycur = constrain(@yoff, @ycur, @matches.length, max_items) wipe ||= changed [@ycur - @yoff, @matches[@yoff, max_items]] } # Wipe if items.length < @plcount @plcount.downto(items.length) do |idx| C.setpos cursor_y(idx + 2), 0 C.clrtoeol end end @plcount = items.length dirty = Set[pos] set(:@dirty) do |vs| dirty.merge vs Set.new end items.each_with_index do |item, idx| next unless wipe || dirty.include?(idx) row = cursor_y(idx + 2) chosen = idx == pos selected = @selects.include?([*item][0]) line, offsets = item tokens = format line, C.cols - 3, offsets print_item row, tokens, chosen, selected end print_info print_input end end def start_renderer init_screen Thread.new do begin while blk = @queue.shift blk.call refresh end rescue Exception => e @main.raise e end end end def render &blk @queue.push blk nil end def vselect &prc sync do @dirty << @ycur - @yoff @ycur = prc.call @ycur end 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 read_nb chars = 1, default = nil, tries = 10 tries.times do |_| begin return @tty.read_nonblock(chars).ord rescue Exception sleep 0.01 end end default end def read_nbs ords = [] while ord = read_nb ords << ord end ords end def get_mouse case ord = read_nb when 32, 36, 40, 48, # mouse-down / shift / cmd / ctrl 35, 39, 43, 51 # mouse-up / shift / cmd / ctrl x = read_nb - 33 y = read_nb - 33 { :event => (ord % 2 == 0 ? :click : :release), :x => x, :y => y, :shift => ord >= 36 } when 96, 100, 104, 112, # scroll-up / shift / cmd / ctrl 97, 101, 105, 113 # scroll-down / shift / cmd / ctrl read_nb(2) { :event => :scroll, :diff => (ord % 2 == 0 ? -1 : 1), :shift => ord >= 100 } else # e.g. 40, 43, 104, 105 read_nb(2) nil end end def get_input actions @tty ||= begin require 'io/console' IO.console rescue LoadError IO.open(IO.sysopen('/dev/tty'), 'r') end if pending = @pending @pending = nil return pending end str = '' while true ord = if str.empty? @tty.getc.ord else begin ord = @tty.read_nonblock(1).ord if (nb = num_unicode_bytes(ord)) > 1 ords = [ord] (nb - 1).times do |_| ords << @tty.read_nonblock(1).ord end # UTF-8 TODO Ruby 1.8 ords.pack('C*').force_encoding('UTF-8') else ord end rescue Exception return str end end ord = case read_nb(1, :esc) when 91, 79 case read_nb(1, nil) when 68 then ctrl(:b) when 67 then ctrl(:f) when 66 then ctrl(:j) when 65 then ctrl(:k) when 90 then :stab when 50 then read_nb; :ins when 51 then read_nb; :del when 53 then read_nb; :pgup when 54 then read_nb; :pgdn when 49 case read_nbs when [59, 50, 68] then ctrl(:a) when [59, 50, 67] then ctrl(:e) when [59, 53, 68] then :alt_b when [59, 53, 67] then :alt_f when [126] then ctrl(:a) end when 52 then read_nb; ctrl(:e) when 72 then ctrl(:a) when 70 then ctrl(:e) when 77 get_mouse end when 'b', 98 then :alt_b when 'd', 100 then :alt_d when 'f', 102 then :alt_f when :esc then :esc when 127 then :alt_bs else next end if ord == 27 return ord if ord.nil? || ord.is_a?(Hash) if actions.has_key?(ord) if str.empty? return ord else @pending = ord return str end else unless ord.is_a? String ord = [ord].pack('U*') end str << ord if ord =~ /[[:print:]]/ end end end class MouseEvent DOUBLE_CLICK_INTERVAL = 0.5 attr_reader :v def initialize v = nil @c = 0 @v = v @t = Time.at 0 end def v= v @c = (@v == v && within?) ? @c + 1 : 0 @v = v @t = Time.now end def double? v @c == 1 && @v == v && within? end def within? (Time.now - @t) < DOUBLE_CLICK_INTERVAL end end def start_loop got = nil begin input = call(:@query, :dup) cursor = input.length yanked = '' mouse_event = MouseEvent.new backword = proc { cursor = (input[0, cursor].rindex(/[^[:alnum:]][[:alnum:]]/) || -1) + 1 nil } forward = proc { cursor += (input[cursor..-1].index(/([[:alnum:]][^[:alnum:]])|(.$)/) || -1) + 1 nil } rubout = proc { |regex| pcursor = cursor cursor = (input[0, cursor].rindex(regex) || -1) + 1 if pcursor > cursor yanked = input[cursor...pcursor] input = input[0...cursor] + input[pcursor..-1] end } actions = { :esc => proc { exit 1 }, ctrl(:d) => proc { if input.empty? exit 1 elsif cursor < input.length input = input[0...cursor] + input[(cursor + 1)..-1] end }, ctrl(:m) => proc { got = pick exit 0 }, ctrl(:u) => proc { yanked = input[0...cursor] if cursor > 0 input = input[cursor..-1] cursor = 0 }, ctrl(:a) => proc { cursor = 0; nil }, ctrl(:e) => proc { cursor = input.length; nil }, ctrl(:j) => proc { vselect { |v| v - @rev_dir } }, ctrl(:k) => proc { vselect { |v| v + @rev_dir } }, ctrl(:w) => proc { rubout.call /\s\S/ }, ctrl(:y) => proc { actions[:default].call yanked }, ctrl(:h) => proc { input[cursor -= 1] = '' if cursor > 0 }, ctrl(:i) => proc { |o| if @multi && sel = pick sync do if @selects.has_key? sel @selects.delete sel else @selects[sel] = sel.orig end end vselect { |v| v + case o when :stab then 1 when :sclick then 0 else -1 end * @rev_dir } end }, ctrl(:b) => proc { cursor = [0, cursor - 1].max; nil }, ctrl(:f) => proc { cursor = [input.length, cursor + 1].min; nil }, ctrl(:l) => proc { render { C.clear; C.refresh }; update_list true }, :del => proc { input[cursor] = '' if input.length > cursor }, :pgup => proc { vselect { |v| v + @rev_dir * (max_items - 1) } }, :pgdn => proc { vselect { |v| v - @rev_dir * (max_items - 1) } }, :alt_bs => proc { rubout.call /[^[:alnum:]][[:alnum:]]/ }, :alt_b => proc { backword.call }, :alt_d => proc { pcursor = cursor forward.call if cursor > pcursor yanked = input[pcursor...cursor] input = input[0...pcursor] + input[cursor..-1] cursor = pcursor end }, :alt_f => proc { forward.call }, :default => proc { |val| case val when String input.insert cursor, val cursor += val.length when Hash event = val[:event] case event when :click, :release x, y, shift = val.values_at :x, :y, :shift y = @reverse ? (C.lines - 1 - y) : y if y == C.lines - 1 cursor = [0, [input.length, x - @prompt.length].min].max elsif x > 1 && y <= max_items tv = get(:@yoff) + max_items - y - 1 case event when :click vselect { |_| tv } actions[ctrl(:i)].call(:sclick) if shift mouse_event.v = tv when :release if !shift && mouse_event.double?(tv) actions[ctrl(:m)].call end end end when :scroll diff, shift = val.values_at :diff, :shift actions[ctrl(:i)].call(:sclick) if shift actions[ctrl(diff > 0 ? :j : :k)].call end nil end } } actions[ctrl(:p)] = actions[ctrl(:k)] actions[ctrl(:n)] = actions[ctrl(:j)] actions[:stab] = actions[ctrl(:i)] actions[127] = actions[ctrl(:h)] actions[ctrl(:q)] = actions[ctrl(:g)] = actions[ctrl(:c)] = actions[:esc] while true set(:@xcur, cursor) render { print_input } if key = get_input(actions) upd = actions.fetch(key, actions[:default]).call(key) # Dispatch key event emit(:key) { [set(:@query, input.dup), cursor] } if upd end end ensure C.close_screen q, selects = geta(:@query, :@selects) @stdout.puts q if @print_query @stdout.puts if @expect if got if selects.empty? burp got else selects.each do |sel, orig| burp sel, orig end end end end end class Matcher class MatchData def initialize n @n = n end def offset _ @n end end def initialize nth, delim @nth = nth @delim = delim @tokens_cache = {} end def tokenize str @tokens_cache[str] ||= str.tokenize(@delim, @nth) end def do_match str, pat if @nth tokenize(str).each do |pair| prefix_length, token = pair if md = token.match(pat) rescue nil return MatchData.new(md.offset(0).map { |o| o + prefix_length }) end end nil else str.match(pat) rescue nil end end end class FuzzyMatcher < Matcher attr_reader :caches, :rxflag def initialize rxflag, nth = nil, delim = nil super nth, delim @caches = Hash.new { |h, k| h[k] = {} } @regexp = {} @rxflag = rxflag end def empty? q q.empty? end def rxflag_for q @rxflag || (q =~ /[A-Z]/ ? 0 : Regexp::IGNORECASE) end def fuzzy_regex q @regexp[q] ||= begin q = q.downcase if @rxflag == Regexp::IGNORECASE Regexp.new(q.split(//).inject('') { |sum, e| e = Regexp.escape e sum << (e.length > 1 ? "(?:#{e}).*?" : # FIXME: not equivalent "#{e}[^#{e}]*?") }, rxflag_for(q)) end end def match list, q, prefix, suffix regexp = fuzzy_regex q cache = @caches[list.object_id] prefix_cache = nil (prefix.length - 1).downto(1) do |len| break if prefix_cache = cache[prefix[0, len]] end suffix_cache = nil 0.upto(suffix.length - 1) do |idx| break if suffix_cache = cache[suffix[idx..-1]] end unless suffix.empty? partial_cache = [prefix_cache, suffix_cache].compact.sort_by { |e| e.length }.first cache[q] ||= (partial_cache ? partial_cache.map { |e| e.first } : list).map { |line| # Ignore errors: e.g. invalid byte sequence in UTF-8 md = do_match(line, regexp) md && [line, [md.offset(0)]] }.compact end end class ExtendedFuzzyMatcher < FuzzyMatcher def initialize rxflag, mode = :fuzzy, nth = nil, delim = nil super rxflag, nth, delim @regexps = {} @mode = mode end def empty? q parse(q).empty? end def parse q q = q.strip @regexps[q] ||= q.split(/\s+/).map { |w| invert = if w =~ /^!/ w = w[1..-1] true end [ @regexp[w] ||= case w when '' nil when /^\^(.*)\$$/ Regexp.new('^' << Regexp.escape($1) << '$', rxflag_for(w)) when /^'/ if @mode == :fuzzy && w.length > 1 exact_regex w[1..-1] elsif @mode == :exact exact_regex w end when /^\^/ w.length > 1 ? Regexp.new('^' << Regexp.escape(w[1..-1]), rxflag_for(w)) : nil when /\$$/ w.length > 1 ? Regexp.new(Regexp.escape(w[0..-2]) << '$', rxflag_for(w)) : nil else @mode == :fuzzy ? fuzzy_regex(w) : exact_regex(w) end, invert ] }.select { |pair| pair.first } end def exact_regex w Regexp.new(Regexp.escape(w), rxflag_for(w)) end def match list, q, prefix, suffix regexps = parse q # Look for prefix cache cache = @caches[list.object_id] prefix = prefix.strip.sub(/\$\S*$/, '').sub(/(^|\s)!\S*$/, '') prefix_cache = nil (prefix.length - 1).downto(1) do |len| break if prefix_cache = cache[Set[@regexps[prefix[0, len]]]] end cache[Set[regexps]] ||= (prefix_cache ? prefix_cache.map { |e| e.first } : list).map { |line| offsets = [] regexps.all? { |pair| regexp, invert = pair md = do_match(line, regexp) if md && !invert offsets << md.offset(0) elsif !md && invert true end } && [line, offsets] }.select { |e| e } end end end#FZF FZF.new(ARGV, $stdin).start if ENV.fetch('FZF_EXECUTABLE', '1') == '1'