mirror of
https://github.com/junegunn/fzf
synced 2026-07-21 16:43:18 +00:00
2fb8ae010f
Since the version 0.7.0, fzf internally used Curses.getch() call to take user input, which allowed it to support mouse input as well. However it has turned out that Curses.getch() has introduced glitches that cannot be easily handled (e.g. Try resize the terminal). So I finally decided that it's not worth the trouble and drop the mouse support.
1022 lines
26 KiB
Ruby
Executable File
1022 lines
26 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
# encoding: utf-8
|
|
#
|
|
# ____ ____
|
|
# / __/___ / __/
|
|
# / /_/_ / / /_
|
|
# / __/ / /_/ __/
|
|
# /_/ /___/_/ Fuzzy finder for your shell
|
|
#
|
|
# Version: 0.8.0 (March 6, 2014)
|
|
#
|
|
# 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.
|
|
|
|
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, :filter, :extended
|
|
|
|
class AtomicVar
|
|
def initialize value
|
|
@value = value
|
|
@mutex = Mutex.new
|
|
end
|
|
|
|
def get
|
|
@mutex.synchronize { @value }
|
|
end
|
|
|
|
def set value = nil
|
|
@mutex.synchronize do
|
|
@value = block_given? ? yield(@value) : value
|
|
end
|
|
end
|
|
|
|
def method_missing sym, *args, &blk
|
|
@mutex.synchronize { @value.send(sym, *args, &blk) }
|
|
end
|
|
end
|
|
|
|
def initialize argv, source = $stdin
|
|
@rxflag = nil
|
|
@sort = ENV.fetch('FZF_DEFAULT_SORT', 1000).to_i
|
|
@color = true
|
|
@multi = false
|
|
@extended = nil
|
|
@filter = nil
|
|
|
|
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 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 '+s', '--no-sort' then @sort = nil
|
|
when '-q', '--query'
|
|
usage 1, 'query string required' unless query = argv.shift
|
|
@query = AtomicVar.new query.dup
|
|
when /^-q(.*)$/, /^--query=(.*)$/
|
|
@query = AtomicVar.new($1)
|
|
when '-f', '--filter'
|
|
usage 1, 'query string required' unless query = argv.shift
|
|
@filter = query
|
|
when /^-f(.*)$/, /^--filter=(.*)$/
|
|
@filter = $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 '-e', '--extended-exact' then @extended = :exact
|
|
when '+e', '--no-extended-exact' then @extended = nil
|
|
else
|
|
usage 1, "illegal option: #{o}"
|
|
end
|
|
end
|
|
|
|
@source = source.clone
|
|
@mtx = Mutex.new
|
|
@cv = ConditionVariable.new
|
|
@events = {}
|
|
@new = []
|
|
@queue = Queue.new
|
|
@pending = nil
|
|
|
|
unless @filter
|
|
@query ||= AtomicVar.new('')
|
|
@cursor_x = AtomicVar.new(@query.length)
|
|
@matches = AtomicVar.new([])
|
|
@count = AtomicVar.new(0)
|
|
@vcursor = AtomicVar.new(0)
|
|
@vcursors = AtomicVar.new(Set.new)
|
|
@spinner = AtomicVar.new('-\|/-\|/'.split(//))
|
|
@selects = AtomicVar.new({}) # ordered >= 1.9
|
|
@main = Thread.current
|
|
@plcount = 0
|
|
end
|
|
end
|
|
|
|
def start
|
|
if @filter
|
|
start_reader(false).join
|
|
filter_list @new
|
|
else
|
|
@stdout = $stdout.clone
|
|
$stdout.reopen($stderr)
|
|
|
|
start_reader true
|
|
init_screen
|
|
start_renderer
|
|
start_search
|
|
start_loop
|
|
end
|
|
end
|
|
|
|
def filter_list list
|
|
matches = get_matcher.match(list, @filter, '', '')
|
|
if @sort && matches.length <= @sort
|
|
matches = sort_by_rank(matches)
|
|
end
|
|
matches.each { |m| puts m.first }
|
|
end
|
|
|
|
def get_matcher
|
|
if @extended
|
|
ExtendedFuzzyMatcher.new @rxflag, @extended
|
|
else
|
|
FuzzyMatcher.new @rxflag
|
|
end
|
|
end
|
|
|
|
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 usage x, message = nil
|
|
$stderr.puts message if message
|
|
$stderr.puts %[usage: fzf [options]
|
|
|
|
Options
|
|
-m, --multi Enable multi-select
|
|
-x, --extended Extended-search mode
|
|
-e, --extended-exact Extended-search mode (exact match)
|
|
-q, --query=STR Initial query
|
|
-f, --filter=STR Filter mode. Do not start interactive finder.
|
|
-s, --sort=MAX Maximum number of matched items to sort (default: 1000)
|
|
+s, --no-sort Do not sort the result. Keep the sequence unchanged.
|
|
-i Case-insensitive match (default: smart-case match)
|
|
+i Case-sensitive match
|
|
+c, --no-color Disable colors
|
|
|
|
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
|
|
|
|
case RUBY_PLATFORM
|
|
when /darwin/
|
|
module UConv
|
|
CHOSUNG = 0x1100
|
|
JUNGSUNG = 0x1161
|
|
JONGSUNG = 0x11A7
|
|
CHOSUNGS = 19
|
|
JUNGSUNGS = 21
|
|
JONGSUNGS = 28
|
|
JJCOUNT = JUNGSUNGS * JONGSUNGS
|
|
NFC_BEGIN = 0xAC00
|
|
NFC_END = NFC_BEGIN + CHOSUNGS * JUNGSUNGS * JONGSUNGS
|
|
|
|
def self.nfd str
|
|
str.split(//).map do |c|
|
|
cp = c.ord
|
|
if cp >= NFC_BEGIN && cp < NFC_END
|
|
chr = ''
|
|
idx = cp - NFC_BEGIN
|
|
cho = CHOSUNG + idx / JJCOUNT
|
|
jung = JUNGSUNG + (idx % JJCOUNT) / JONGSUNGS
|
|
jong = JONGSUNG + idx % JONGSUNGS
|
|
chr << cho << jung
|
|
chr << jong if jong != JONGSUNG
|
|
chr
|
|
else
|
|
c
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.to_nfc arr
|
|
[NFC_BEGIN + arr[0] * JJCOUNT +
|
|
(arr[1] || 0) * JONGSUNGS +
|
|
(arr[2] || 0)].pack('U*')
|
|
end
|
|
|
|
if String.method_defined?(:each_char)
|
|
def self.split str
|
|
str.each_char.to_a
|
|
end
|
|
else
|
|
def self.split str
|
|
str.split('')
|
|
end
|
|
end
|
|
|
|
def self.nfc str, offsets = []
|
|
ret = ''
|
|
omap = []
|
|
pend = []
|
|
split(str).each_with_index do |c, idx|
|
|
cp =
|
|
begin
|
|
c.ord
|
|
rescue Exception
|
|
next
|
|
end
|
|
omap << ret.length
|
|
unless pend.empty?
|
|
if cp >= JUNGSUNG && cp < JUNGSUNG + JUNGSUNGS
|
|
pend << cp - JUNGSUNG
|
|
next
|
|
elsif cp >= JONGSUNG && cp < JONGSUNG + JONGSUNGS
|
|
pend << cp - JONGSUNG
|
|
next
|
|
else
|
|
omap[-1] = omap[-1] + 1
|
|
ret << to_nfc(pend)
|
|
pend.clear
|
|
end
|
|
end
|
|
if cp >= CHOSUNG && cp < CHOSUNG + CHOSUNGS
|
|
pend << cp - CHOSUNG
|
|
else
|
|
ret << c
|
|
end
|
|
end
|
|
ret << to_nfc(pend) unless pend.empty?
|
|
return [ret,
|
|
offsets.map { |pair|
|
|
b, e = pair
|
|
[omap[b] || 0, omap[e] || ((omap.last || 0) + 1)] }]
|
|
end
|
|
end
|
|
|
|
def convert_item item
|
|
UConv.nfc(*item)
|
|
end
|
|
|
|
class Matcher
|
|
def query_chars q
|
|
UConv.nfd(q)
|
|
end
|
|
|
|
def sanitize q
|
|
UConv.nfd(q).join
|
|
end
|
|
end
|
|
else
|
|
def convert_item item
|
|
item
|
|
end
|
|
|
|
class Matcher
|
|
def query_chars q
|
|
q.split(//)
|
|
end
|
|
|
|
def sanitize q
|
|
q
|
|
end
|
|
end
|
|
end
|
|
|
|
def emit event
|
|
@mtx.synchronize do
|
|
@events[event] = yield
|
|
@cv.broadcast
|
|
end
|
|
end
|
|
|
|
def max_items; C.lines - 2; end
|
|
def cursor_y; C.lines - 1; end
|
|
def cprint str, col
|
|
C.attron(col) do
|
|
addstr_safe str
|
|
end if str
|
|
end
|
|
def addstr_safe str
|
|
C.addstr str.gsub("\0", '')
|
|
end
|
|
|
|
def print_input
|
|
C.setpos cursor_y, 0
|
|
C.clrtoeol
|
|
cprint '> ', color(:prompt, true)
|
|
C.attron(C::A_BOLD) do
|
|
C.addstr @query.get
|
|
end
|
|
end
|
|
|
|
def print_info msg = nil
|
|
C.setpos cursor_y - 1, 0
|
|
C.clrtoeol
|
|
prefix =
|
|
if spinner = @spinner.first
|
|
cprint spinner, color(:spinner, true)
|
|
' '
|
|
else
|
|
' '
|
|
end
|
|
C.attron color(:info, false) do
|
|
C.addstr "#{prefix}#{@matches.length}/#{@count.get}"
|
|
if (selected = @selects.length) > 0
|
|
C.addstr " (#{selected})"
|
|
end
|
|
C.addstr msg if msg
|
|
end
|
|
end
|
|
|
|
def refresh
|
|
C.setpos cursor_y, 2 + width(@query[0, @cursor_x.get])
|
|
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
|
|
|
|
def sort_by_rank list
|
|
list.sort_by { |tuple|
|
|
line, offsets = tuple
|
|
matchlen = 0
|
|
pe = nil
|
|
offsets.sort.each do |pair|
|
|
b, e = pair
|
|
b = pe if pe && pe > b
|
|
pe = e
|
|
matchlen += e - b
|
|
end
|
|
[matchlen, line.length, line]
|
|
}
|
|
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
|
|
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
|
|
C.init_screen
|
|
C.start_color
|
|
dbg =
|
|
if C.respond_to?(:use_default_colors)
|
|
C.use_default_colors
|
|
-1
|
|
else
|
|
C::COLOR_BLACK
|
|
end
|
|
C.raw
|
|
C.noecho
|
|
|
|
if @color
|
|
if 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 curses
|
|
stream =
|
|
if @source.tty?
|
|
if default_command = ENV['FZF_DEFAULT_COMMAND']
|
|
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
|
|
while line = stream.gets
|
|
emit(:new) { @new << line.chomp }
|
|
end
|
|
emit(:loaded) { true }
|
|
@spinner.clear if curses
|
|
end
|
|
end
|
|
|
|
def start_search
|
|
matcher = get_matcher
|
|
searcher = Thread.new {
|
|
lists = []
|
|
events = {}
|
|
fcache = {}
|
|
q = ''
|
|
delay = -5
|
|
|
|
begin
|
|
while true
|
|
@mtx.synchronize do
|
|
while true
|
|
events.merge! @events
|
|
|
|
if @events.empty? # No new events
|
|
@cv.wait @mtx
|
|
next
|
|
end
|
|
@events.clear
|
|
break
|
|
end
|
|
|
|
if events[:new]
|
|
lists << @new
|
|
@count.set { |c| c + @new.length }
|
|
@spinner.set { |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 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 = @mtx.synchronize { @events[:key] }
|
|
break if skip
|
|
|
|
if !empty && (progress = 100 * cnt / @count.get) < 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
|
|
next if skip
|
|
matches = @sort ? found : found.reverse
|
|
if !empty && @sort && matches.length <= @sort
|
|
matches = sort_by_rank(matches)
|
|
end
|
|
fcache[q] = matches
|
|
end
|
|
|
|
# Atomic update
|
|
@matches.set matches
|
|
end#new_search
|
|
|
|
# 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
|
|
|
|
def pick
|
|
items = @matches[0, max_items]
|
|
curr = [0, [@vcursor.get, items.length - 1].min].max
|
|
[*items.fetch(curr, [])][0]
|
|
end
|
|
|
|
def update_list wipe
|
|
render do
|
|
items = @matches[0, 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
|
|
|
|
maxc = C.cols - 3
|
|
vcursor = @vcursor.set { |v| [0, [v, items.length - 1].min].max }
|
|
cleanse = Set[vcursor]
|
|
@vcursors.set { |vs|
|
|
cleanse.merge vs
|
|
Set.new
|
|
}
|
|
items.each_with_index do |item, idx|
|
|
next unless wipe || cleanse.include?(idx)
|
|
row = cursor_y - idx - 2
|
|
chosen = idx == vcursor
|
|
selected = @selects.include?([*item][0])
|
|
line, offsets = convert_item item
|
|
tokens = format line, maxc, offsets
|
|
print_item row, tokens, chosen, selected
|
|
end
|
|
print_info
|
|
print_input
|
|
end
|
|
end
|
|
|
|
def start_renderer
|
|
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
|
|
@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 get_input actions
|
|
@tty ||= IO.open(IO.sysopen('/dev/tty'), 'r')
|
|
|
|
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 ord = (@tty.read_nonblock(1).ord rescue :esc)
|
|
when 91
|
|
case (@tty.read_nonblock(1).ord rescue 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
|
|
else next
|
|
end
|
|
when 'b', 98 then :alt_b
|
|
when 'f', 102 then :alt_f
|
|
when :esc then :esc
|
|
else next
|
|
end if ord == 27
|
|
|
|
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
|
|
|
|
def start_loop
|
|
got = nil
|
|
begin
|
|
input = @query.get.dup
|
|
cursor = input.length
|
|
backword = proc {
|
|
cursor = (input[0, cursor].rindex(/\s\S/) || -1) + 1
|
|
}
|
|
actions = {
|
|
:esc => proc { exit 1 },
|
|
ctrl(:d) => proc { exit 1 if input.empty? },
|
|
ctrl(:m) => proc {
|
|
got = pick
|
|
exit 0
|
|
},
|
|
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 { 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]
|
|
},
|
|
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
|
|
vselect { |v| v + (o == :stab ? 1 : -1) }
|
|
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 },
|
|
:alt_b => proc { backword.call; nil },
|
|
:alt_f => proc {
|
|
cursor += (input[cursor..-1].index(/(\S\s)|(.$)/) || -1) + 1
|
|
nil
|
|
},
|
|
}
|
|
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]
|
|
|
|
emit(:key) { [@query.get, cursor] } unless @query.empty?
|
|
while true
|
|
@cursor_x.set cursor
|
|
render { print_input }
|
|
|
|
if key = get_input(actions)
|
|
upd = actions.fetch(key, proc { |str|
|
|
input.insert cursor, str
|
|
cursor += str.length
|
|
}).call(key)
|
|
|
|
# Dispatch key event
|
|
emit(:key) { [@query.set(input.dup), cursor] } if upd
|
|
end
|
|
end
|
|
ensure
|
|
C.close_screen
|
|
if got
|
|
if @selects.empty?
|
|
@stdout.puts got
|
|
else
|
|
@selects.each do |sel, _|
|
|
@stdout.puts sel
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
class FuzzyMatcher < Matcher
|
|
attr_reader :caches, :rxflag
|
|
|
|
def initialize rxflag
|
|
@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(query_chars(q).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 = line.match(regexp) rescue nil
|
|
md && [line, [md.offset(0)]]
|
|
}.compact
|
|
end
|
|
end
|
|
|
|
class ExtendedFuzzyMatcher < FuzzyMatcher
|
|
def initialize rxflag, mode = :fuzzy
|
|
super rxflag
|
|
@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('^' << sanitize(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('^' << sanitize(Regexp.escape(w[1..-1])), rxflag_for(w)) : nil
|
|
when /\$$/
|
|
w.length > 1 ?
|
|
Regexp.new(sanitize(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(sanitize(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 = line.match(regexp) rescue nil
|
|
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'
|
|
|