PageRenderTime 56ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/fzf

https://github.com/izumineid/fzf
Ruby | 1180 lines | 1111 code | 31 blank | 38 comment | 37 complexity | 65ff37a3227548ef7d5fbde2939d292e MD5 | raw file
  1. #!/usr/bin/env ruby
  2. # encoding: utf-8
  3. #
  4. # ____ ____
  5. # / __/___ / __/
  6. # / /_/_ / / /_
  7. # / __/ / /_/ __/
  8. # /_/ /___/_/ Fuzzy finder for your shell
  9. #
  10. # Version: 0.8.5 (Jun 12, 2014)
  11. #
  12. # Author: Junegunn Choi
  13. # URL: https://github.com/junegunn/fzf
  14. # License: MIT
  15. #
  16. # Copyright (c) 2014 Junegunn Choi
  17. #
  18. # MIT License
  19. #
  20. # Permission is hereby granted, free of charge, to any person obtaining
  21. # a copy of this software and associated documentation files (the
  22. # "Software"), to deal in the Software without restriction, including
  23. # without limitation the rights to use, copy, modify, merge, publish,
  24. # distribute, sublicense, and/or sell copies of the Software, and to
  25. # permit persons to whom the Software is furnished to do so, subject to
  26. # the following conditions:
  27. #
  28. # The above copyright notice and this permission notice shall be
  29. # included in all copies or substantial portions of the Software.
  30. #
  31. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  32. # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  33. # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  34. # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  35. # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  36. # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  37. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  38. require 'thread'
  39. require 'curses'
  40. require 'set'
  41. unless String.method_defined? :force_encoding
  42. class String
  43. def force_encoding *arg
  44. self
  45. end
  46. end
  47. end
  48. class FZF
  49. C = Curses
  50. attr_reader :rxflag, :sort, :nth, :color, :black, :ansi256, :reverse,
  51. :mouse, :multi, :query, :select1, :exit0, :filter, :extended
  52. class AtomicVar
  53. def initialize value
  54. @value = value
  55. @mutex = Mutex.new
  56. end
  57. def get
  58. @mutex.synchronize { @value }
  59. end
  60. def set value = nil
  61. @mutex.synchronize do
  62. @value = block_given? ? yield(@value) : value
  63. end
  64. end
  65. def method_missing sym, *args, &blk
  66. @mutex.synchronize { @value.send(sym, *args, &blk) }
  67. end
  68. end
  69. def initialize argv, source = $stdin
  70. @rxflag = nil
  71. @sort = ENV.fetch('FZF_DEFAULT_SORT', 1000).to_i
  72. @color = true
  73. @ansi256 = true
  74. @black = false
  75. @multi = false
  76. @mouse = true
  77. @extended = nil
  78. @select1 = false
  79. @exit0 = false
  80. @filter = nil
  81. @nth = nil
  82. @delim = nil
  83. @reverse = false
  84. argv =
  85. if opts = ENV['FZF_DEFAULT_OPTS']
  86. require 'shellwords'
  87. Shellwords.shellwords(opts) + argv
  88. else
  89. argv.dup
  90. end
  91. while o = argv.shift
  92. case o
  93. when '--version' then FZF.version
  94. when '-h', '--help' then usage 0
  95. when '-m', '--multi' then @multi = true
  96. when '+m', '--no-multi' then @multi = false
  97. when '-x', '--extended' then @extended = :fuzzy
  98. when '+x', '--no-extended' then @extended = nil
  99. when '-i' then @rxflag = Regexp::IGNORECASE
  100. when '+i' then @rxflag = 0
  101. when '-c', '--color' then @color = true
  102. when '+c', '--no-color' then @color = false
  103. when '-2', '--256' then @ansi256 = true
  104. when '+2', '--no-256' then @ansi256 = false
  105. when '--black' then @black = true
  106. when '--no-black' then @black = false
  107. when '--mouse' then @mouse = true
  108. when '--no-mouse' then @mouse = false
  109. when '--reverse' then @reverse = true
  110. when '--no-reverse' then @reverse = false
  111. when '+s', '--no-sort' then @sort = nil
  112. when '-1', '--select-1' then @select1 = true
  113. when '+1', '--no-select-1' then @select1 = false
  114. when '-0', '--exit-0' then @exit0 = true
  115. when '+0', '--no-exit-0' then @exit0 = false
  116. when '-q', '--query'
  117. usage 1, 'query string required' unless query = argv.shift
  118. @query = AtomicVar.new query.dup
  119. when /^-q(.*)$/, /^--query=(.*)$/
  120. @query = AtomicVar.new($1)
  121. when '-f', '--filter'
  122. usage 1, 'query string required' unless query = argv.shift
  123. @filter = query
  124. when /^-f(.*)$/, /^--filter=(.*)$/
  125. @filter = $1
  126. when '-n', '--nth'
  127. usage 1, 'field number required' unless nth = argv.shift
  128. usage 1, 'invalid field number' if nth.to_i == 0
  129. @nth = parse_nth nth
  130. when /^-n([0-9,-]+)$/, /^--nth=([0-9,-]+)$/
  131. @nth = parse_nth $1
  132. when '-d', '--delimiter'
  133. usage 1, 'delimiter required' unless delim = argv.shift
  134. @delim = FZF.build_delim_regex delim
  135. when /^-d(.+)$/, /^--delimiter=(.+)$/
  136. @delim = FZF.build_delim_regex $1
  137. when '-s', '--sort'
  138. usage 1, 'sort size required' unless sort = argv.shift
  139. usage 1, 'invalid sort size' unless sort =~ /^[0-9]+$/
  140. @sort = sort.to_i
  141. when /^-s([0-9]+)$/, /^--sort=([0-9]+)$/
  142. @sort = $1.to_i
  143. when '-e', '--extended-exact' then @extended = :exact
  144. when '+e', '--no-extended-exact' then @extended = nil
  145. else
  146. usage 1, "illegal option: #{o}"
  147. end
  148. end
  149. @source = source.clone
  150. @mtx = Mutex.new
  151. @cv = ConditionVariable.new
  152. @events = {}
  153. @new = []
  154. @queue = Queue.new
  155. @pending = nil
  156. unless @filter
  157. @query ||= AtomicVar.new('')
  158. @cursor_x = AtomicVar.new(@query.length)
  159. @matches = AtomicVar.new([])
  160. @count = AtomicVar.new(0)
  161. @vcursor = AtomicVar.new(0)
  162. @vcursors = AtomicVar.new(Set.new)
  163. @spinner = AtomicVar.new('-\|/-\|/'.split(//))
  164. @selects = AtomicVar.new({}) # ordered >= 1.9
  165. @main = Thread.current
  166. @plcount = 0
  167. end
  168. end
  169. def parse_nth nth
  170. nth.split(',').map { |n|
  171. ni = n.to_i
  172. usage 1, "invalid field number: #{n}" if ni == 0
  173. ni
  174. }
  175. end
  176. def FZF.build_delim_regex delim
  177. Regexp.compile(delim) rescue (delim = Regexp.escape(delim))
  178. Regexp.compile "(?:.*?#{delim})|(?:.+?$)"
  179. end
  180. def start
  181. if @filter
  182. start_reader.join
  183. filter_list @new
  184. else
  185. start_reader
  186. emit(:key) { q = @query.get; [q, q.length] } unless empty = @query.empty?
  187. if @select1 || @exit0
  188. start_search do |loaded, matches|
  189. len = empty ? @count.get : matches.length
  190. if loaded
  191. if @select1 && len == 1
  192. puts empty ? matches.first : matches.first.first
  193. exit 0
  194. elsif @exit0 && len == 0
  195. exit 0
  196. end
  197. end
  198. if loaded || len > 1
  199. start_renderer
  200. Thread.new { start_loop }
  201. end
  202. end
  203. sleep
  204. else
  205. start_search
  206. start_renderer
  207. start_loop
  208. end
  209. end
  210. end
  211. def filter_list list
  212. matches = matcher.match(list, @filter, '', '')
  213. if @sort && matches.length <= @sort
  214. matches = FZF.sort(matches)
  215. end
  216. matches.each { |m| puts m.first }
  217. end
  218. def matcher
  219. @matcher ||=
  220. if @extended
  221. ExtendedFuzzyMatcher.new @rxflag, @extended, @nth, @delim
  222. else
  223. FuzzyMatcher.new @rxflag, @nth, @delim
  224. end
  225. end
  226. class << self
  227. def version
  228. File.open(__FILE__, 'r') do |f|
  229. f.each_line do |line|
  230. if line =~ /Version: (.*)/
  231. $stdout.puts 'fzf ' << $1
  232. exit
  233. end
  234. end
  235. end
  236. end
  237. def sort list
  238. list.sort_by { |tuple| rank tuple }
  239. end
  240. def rank tuple
  241. line, offsets = tuple
  242. matchlen = 0
  243. pe = 0
  244. offsets.sort.each do |pair|
  245. b, e = pair
  246. b = pe if pe > b
  247. pe = e if e > pe
  248. matchlen += e - b if e > b
  249. end
  250. [matchlen, line.length, line]
  251. end
  252. end
  253. def usage x, message = nil
  254. $stderr.puts message if message
  255. $stderr.puts %[usage: fzf [options]
  256. Search
  257. -x, --extended Extended-search mode
  258. -e, --extended-exact Extended-search mode (exact match)
  259. -i Case-insensitive match (default: smart-case match)
  260. +i Case-sensitive match
  261. -n, --nth=[-]N[,..] Comma-separated list of field indexes for limiting
  262. search scope (positive or negative integers)
  263. -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
  264. Search result
  265. -s, --sort=MAX Maximum number of matched items to sort (default: 1000)
  266. +s, --no-sort Do not sort the result. Keep the sequence unchanged.
  267. Interface
  268. -m, --multi Enable multi-select with tab/shift-tab
  269. --no-mouse Disable mouse
  270. +c, --no-color Disable colors
  271. +2, --no-256 Disable 256-color
  272. --black Use black background
  273. --reverse Reverse orientation
  274. Scripting
  275. -q, --query=STR Start the finder with the given query
  276. -1, --select-1 Automatically select the only match
  277. -0, --exit-0 Exit immediately when there's no match
  278. -f, --filter=STR Filter mode. Do not start interactive finder.
  279. Environment variables
  280. FZF_DEFAULT_COMMAND Default command to use when input is tty
  281. FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m --sort 10000")] + $/ + $/
  282. exit x
  283. end
  284. def emit event
  285. @mtx.synchronize do
  286. @events[event] = yield
  287. @cv.broadcast
  288. end
  289. end
  290. def max_items; C.lines - 2; end
  291. def cursor_y offset = 0
  292. @reverse ? (offset) : (C.lines - 1 - offset)
  293. end
  294. def cprint str, col
  295. C.attron(col) do
  296. addstr_safe str
  297. end if str
  298. end
  299. def addstr_safe str
  300. C.addstr str.gsub("\0", '')
  301. end
  302. def print_input
  303. C.setpos cursor_y, 0
  304. C.clrtoeol
  305. cprint '> ', color(:prompt, true)
  306. C.attron(C::A_BOLD) do
  307. C.addstr @query.get
  308. end
  309. end
  310. def print_info msg = nil
  311. C.setpos cursor_y(1), 0
  312. C.clrtoeol
  313. prefix =
  314. if spinner = @spinner.first
  315. cprint spinner, color(:spinner, true)
  316. ' '
  317. else
  318. ' '
  319. end
  320. C.attron color(:info, false) do
  321. C.addstr "#{prefix}#{@matches.length}/#{@count.get}"
  322. if (selected = @selects.length) > 0
  323. C.addstr " (#{selected})"
  324. end
  325. C.addstr msg if msg
  326. end
  327. end
  328. def refresh
  329. C.setpos cursor_y, 2 + width(@query[0, @cursor_x.get])
  330. C.refresh
  331. end
  332. def ctrl char
  333. char.to_s.ord - 'a'.ord + 1
  334. end
  335. def format line, limit, offsets
  336. offsets ||= []
  337. maxe = offsets.map { |e| e.last }.max || 0
  338. # Overflow
  339. if width(line) > limit
  340. ewidth = width(line[0...maxe])
  341. # Stri..
  342. if ewidth <= limit - 2
  343. line, _ = trim line, limit - 2, false
  344. line << '..'
  345. # ..ring
  346. else
  347. # ..ri..
  348. line = line[0...maxe] + '..' if ewidth < width(line) - 2
  349. line, diff = trim line, limit - 2, true
  350. offsets = offsets.map { |pair|
  351. b, e = pair
  352. b += 2 - diff
  353. e += 2 - diff
  354. b = [2, b].max
  355. [b, e]
  356. }
  357. line = '..' + line
  358. end
  359. end
  360. tokens = []
  361. index = 0
  362. offsets.select { |pair| pair.first < pair.last }.
  363. sort_by { |pair| pair }.each do |pair|
  364. b, e = pair.map { |x| [index, x].max }
  365. tokens << [line[index...b], false]
  366. tokens << [line[b...e], true]
  367. index = e
  368. end
  369. tokens << [line[index..-1], false] if index < line.length
  370. tokens.reject { |pair| pair.first.empty? }
  371. end
  372. def print_item row, tokens, chosen, selected
  373. # Cursor
  374. C.setpos row, 0
  375. C.clrtoeol
  376. cprint chosen ? '>' : ' ', color(:cursor, true)
  377. cprint selected ? '>' : ' ',
  378. chosen ? color(:chosen) : (selected ? color(:selected, true) : 0)
  379. # Highlighted item
  380. C.attron color(:chosen, true) if chosen
  381. tokens.each do |pair|
  382. token, highlighted = pair
  383. if highlighted
  384. cprint token, color(chosen ? :match! : :match, chosen)
  385. C.attron color(:chosen, true) if chosen
  386. else
  387. addstr_safe token
  388. end
  389. end
  390. C.attroff color(:chosen, true) if chosen
  391. end
  392. AFTER_1_9 = RUBY_VERSION.split('.').map { |e| e.rjust(3, '0') }.join >= '001009'
  393. if AFTER_1_9
  394. @@wrx = Regexp.new '\p{Han}|\p{Katakana}|\p{Hiragana}|\p{Hangul}'
  395. def width str
  396. str.gsub(@@wrx, ' ').length rescue str.length
  397. end
  398. def trim str, len, left
  399. width = width str
  400. diff = 0
  401. while width > len
  402. width -= ((left ? str[0, 1] : str[-1, 1]) =~ @@wrx ? 2 : 1) rescue 1
  403. str = left ? str[1..-1] : str[0...-1]
  404. diff += 1
  405. end
  406. [str, diff]
  407. end
  408. else
  409. def width str
  410. str.length
  411. end
  412. def trim str, len, left
  413. diff = str.length - len
  414. if diff > 0
  415. [left ? str[diff..-1] : str[0...-diff], diff]
  416. else
  417. [str, 0]
  418. end
  419. end
  420. class ::String
  421. def ord
  422. self.unpack('c').first
  423. end
  424. end
  425. class ::Fixnum
  426. def ord
  427. self
  428. end
  429. end
  430. end
  431. def init_screen
  432. @stdout = $stdout.clone
  433. $stdout.reopen($stderr)
  434. C.init_screen
  435. C.mousemask C::ALL_MOUSE_EVENTS if @mouse
  436. C.start_color
  437. dbg =
  438. if !@black && C.respond_to?(:use_default_colors)
  439. C.use_default_colors
  440. -1
  441. else
  442. C::COLOR_BLACK
  443. end
  444. C.raw
  445. C.noecho
  446. if @color
  447. if @ansi256 && ENV['TERM'].to_s =~ /256/
  448. C.init_pair 1, 110, dbg
  449. C.init_pair 2, 108, dbg
  450. C.init_pair 3, 254, 236
  451. C.init_pair 4, 151, 236
  452. C.init_pair 5, 148, dbg
  453. C.init_pair 6, 144, dbg
  454. C.init_pair 7, 161, 236
  455. C.init_pair 8, 168, 236
  456. else
  457. C.init_pair 1, C::COLOR_BLUE, dbg
  458. C.init_pair 2, C::COLOR_GREEN, dbg
  459. C.init_pair 3, C::COLOR_YELLOW, C::COLOR_BLACK
  460. C.init_pair 4, C::COLOR_GREEN, C::COLOR_BLACK
  461. C.init_pair 5, C::COLOR_GREEN, dbg
  462. C.init_pair 6, C::COLOR_WHITE, dbg
  463. C.init_pair 7, C::COLOR_RED, C::COLOR_BLACK
  464. C.init_pair 8, C::COLOR_MAGENTA, C::COLOR_BLACK
  465. end
  466. def self.color sym, bold = false
  467. C.color_pair([:prompt, :match, :chosen, :match!,
  468. :spinner, :info, :cursor, :selected].index(sym) + 1) |
  469. (bold ? C::A_BOLD : 0)
  470. end
  471. else
  472. def self.color sym, bold = false
  473. case sym
  474. when :chosen
  475. bold ? C::A_REVERSE : 0
  476. when :match
  477. C::A_UNDERLINE
  478. when :match!
  479. C::A_REVERSE | C::A_UNDERLINE
  480. else
  481. 0
  482. end | (bold ? C::A_BOLD : 0)
  483. end
  484. end
  485. C.refresh
  486. end
  487. def start_reader
  488. stream =
  489. if @source.tty?
  490. if default_command = ENV['FZF_DEFAULT_COMMAND']
  491. IO.popen(default_command)
  492. elsif !`which find`.empty?
  493. IO.popen("find * -path '*/\\.*' -prune -o -type f -print -o -type l -print 2> /dev/null")
  494. else
  495. exit 1
  496. end
  497. else
  498. @source
  499. end
  500. Thread.new do
  501. while line = stream.gets
  502. emit(:new) { @new << line.chomp }
  503. end
  504. emit(:loaded) { true }
  505. @spinner.clear if @spinner
  506. end
  507. end
  508. def start_search &callback
  509. Thread.new do
  510. lists = []
  511. events = {}
  512. fcache = {}
  513. q = ''
  514. delay = -5
  515. begin
  516. while true
  517. @mtx.synchronize do
  518. while true
  519. events.merge! @events
  520. if @events.empty? # No new events
  521. @cv.wait @mtx
  522. next
  523. end
  524. @events.clear
  525. break
  526. end
  527. if events[:new]
  528. lists << @new
  529. @count.set { |c| c + @new.length }
  530. @spinner.set { |spinner|
  531. if e = spinner.shift
  532. spinner.push e
  533. end; spinner
  534. }
  535. @new = []
  536. fcache.clear
  537. end
  538. end#mtx
  539. new_search = events[:key] || events.delete(:new)
  540. user_input = events[:key]
  541. progress = 0
  542. started_at = Time.now
  543. if updated = new_search && !lists.empty?
  544. q, cx = events.delete(:key) || [q, 0]
  545. empty = matcher.empty?(q)
  546. unless matches = fcache[q]
  547. found = []
  548. skip = false
  549. cnt = 0
  550. lists.each do |list|
  551. cnt += list.length
  552. skip = @mtx.synchronize { @events[:key] }
  553. break if skip
  554. if !empty && (progress = 100 * cnt / @count.get) < 100 && Time.now - started_at > 0.5
  555. render { print_info " (#{progress}%)" }
  556. end
  557. found.concat(q.empty? ? list :
  558. matcher.match(list, q, q[0, cx], q[cx..-1]))
  559. end
  560. next if skip
  561. matches = @sort ? found : found.reverse
  562. if !empty && @sort && matches.length <= @sort
  563. matches = FZF.sort(matches)
  564. end
  565. fcache[q] = matches
  566. end
  567. # Atomic update
  568. @matches.set matches
  569. end#new_search
  570. callback = nil if callback &&
  571. (updated || events[:loaded]) &&
  572. callback.call(events[:loaded], matches)
  573. # This small delay reduces the number of partial lists
  574. sleep((delay = [20, delay + 5].min) * 0.01) unless user_input
  575. update_list new_search
  576. end#while
  577. rescue Exception => e
  578. @main.raise e
  579. end
  580. end
  581. end
  582. def pick
  583. items = @matches[0, max_items]
  584. curr = [0, [@vcursor.get, items.length - 1].min].max
  585. [*items.fetch(curr, [])][0]
  586. end
  587. def update_list wipe
  588. render do
  589. items = @matches[0, max_items]
  590. # Wipe
  591. if items.length < @plcount
  592. @plcount.downto(items.length) do |idx|
  593. C.setpos cursor_y(idx + 2), 0
  594. C.clrtoeol
  595. end
  596. end
  597. @plcount = items.length
  598. maxc = C.cols - 3
  599. vcursor = @vcursor.set { |v| [0, [v, items.length - 1].min].max }
  600. cleanse = Set[vcursor]
  601. @vcursors.set { |vs|
  602. cleanse.merge vs
  603. Set.new
  604. }
  605. items.each_with_index do |item, idx|
  606. next unless wipe || cleanse.include?(idx)
  607. row = cursor_y(idx + 2)
  608. chosen = idx == vcursor
  609. selected = @selects.include?([*item][0])
  610. line, offsets = item
  611. tokens = format line, maxc, offsets
  612. print_item row, tokens, chosen, selected
  613. end
  614. print_info
  615. print_input
  616. end
  617. end
  618. def start_renderer
  619. init_screen
  620. Thread.new do
  621. begin
  622. while blk = @queue.shift
  623. blk.call
  624. refresh
  625. end
  626. rescue Exception => e
  627. @main.raise e
  628. end
  629. end
  630. end
  631. def render &blk
  632. @queue.push blk
  633. nil
  634. end
  635. def vselect &prc
  636. @vcursor.set { |v| @vcursors << v; prc.call v }
  637. update_list false
  638. end
  639. def num_unicode_bytes chr
  640. # http://en.wikipedia.org/wiki/UTF-8
  641. if chr & 0b10000000 > 0
  642. bytes = 0
  643. 7.downto(2) do |shift|
  644. break if (chr >> shift) & 0x1 == 0
  645. bytes += 1
  646. end
  647. bytes
  648. else
  649. 1
  650. end
  651. end
  652. def read_nb chars = 1, default = nil, tries = 10
  653. tries.times do |_|
  654. begin
  655. return @tty.read_nonblock(chars).ord
  656. rescue Exception
  657. sleep 0.01
  658. end
  659. end
  660. default
  661. end
  662. def read_nbs
  663. ords = []
  664. while ord = read_nb
  665. ords << ord
  666. end
  667. ords
  668. end
  669. def get_mouse
  670. case ord = read_nb
  671. when 32, 36, 40, 48, # mouse-down / shift / cmd / ctrl
  672. 35, 39, 43, 51 # mouse-up / shift / cmd / ctrl
  673. x = read_nb - 33
  674. y = read_nb - 33
  675. { :event => (ord % 2 == 0 ? :click : :release),
  676. :x => x, :y => y, :shift => ord >= 36 }
  677. when 96, 100, 104, 112, # scroll-up / shift / cmd / ctrl
  678. 97, 101, 105, 113 # scroll-down / shift / cmd / ctrl
  679. read_nb(2)
  680. { :event => :scroll, :diff => (ord % 2 == 0 ? -1 : 1), :shift => ord >= 100 }
  681. else
  682. # e.g. 40, 43, 104, 105
  683. read_nb(2)
  684. nil
  685. end
  686. end
  687. def get_input actions
  688. @tty ||= IO.open(IO.sysopen('/dev/tty'), 'r')
  689. if pending = @pending
  690. @pending = nil
  691. return pending
  692. end
  693. str = ''
  694. while true
  695. ord =
  696. if str.empty?
  697. @tty.getc.ord
  698. else
  699. begin
  700. ord = @tty.read_nonblock(1).ord
  701. if (nb = num_unicode_bytes(ord)) > 1
  702. ords = [ord]
  703. (nb - 1).times do |_|
  704. ords << @tty.read_nonblock(1).ord
  705. end
  706. # UTF-8 TODO Ruby 1.8
  707. ords.pack('C*').force_encoding('UTF-8')
  708. else
  709. ord
  710. end
  711. rescue Exception
  712. return str
  713. end
  714. end
  715. ord =
  716. case read_nb(1, :esc)
  717. when 91, 79
  718. case read_nb(1, nil)
  719. when 68 then ctrl(:b)
  720. when 67 then ctrl(:f)
  721. when 66 then ctrl(:j)
  722. when 65 then ctrl(:k)
  723. when 90 then :stab
  724. when 50 then read_nb; :ins
  725. when 51 then read_nb; :del
  726. when 53 then read_nb; :pgup
  727. when 54 then read_nb; :pgdn
  728. when 49
  729. case read_nbs
  730. when [59, 50, 68] then ctrl(:a)
  731. when [59, 50, 67] then ctrl(:e)
  732. when [126] then ctrl(:a)
  733. end
  734. when 52 then read_nb; ctrl(:e)
  735. when 72 then ctrl(:a)
  736. when 70 then ctrl(:e)
  737. when 77
  738. get_mouse
  739. end
  740. when 'b', 98 then :alt_b
  741. when 'f', 102 then :alt_f
  742. when :esc then :esc
  743. else next
  744. end if ord == 27
  745. return ord if ord.nil? || ord.is_a?(Hash)
  746. if actions.has_key?(ord)
  747. if str.empty?
  748. return ord
  749. else
  750. @pending = ord
  751. return str
  752. end
  753. else
  754. unless ord.is_a? String
  755. ord = [ord].pack('U*')
  756. end
  757. str << ord if ord =~ /[[:print:]]/
  758. end
  759. end
  760. end
  761. class MouseEvent
  762. DOUBLE_CLICK_INTERVAL = 0.5
  763. attr_reader :v
  764. def initialize v = nil
  765. @c = 0
  766. @v = v
  767. @t = Time.at 0
  768. end
  769. def v= v
  770. @c = (@v == v && within?) ? @c + 1 : 0
  771. @v = v
  772. @t = Time.now
  773. end
  774. def double? v
  775. @c == 1 && @v == v && within?
  776. end
  777. def within?
  778. (Time.now - @t) < DOUBLE_CLICK_INTERVAL
  779. end
  780. end
  781. def start_loop
  782. got = nil
  783. begin
  784. input = @query.get.dup
  785. cursor = input.length
  786. yanked = ''
  787. mouse_event = MouseEvent.new
  788. backword = proc {
  789. cursor = (input[0, cursor].rindex(/\s\S/) || -1) + 1
  790. }
  791. actions = {
  792. :esc => proc { exit 1 },
  793. ctrl(:d) => proc {
  794. if input.empty?
  795. exit 1
  796. elsif cursor < input.length
  797. input = input[0...cursor] + input[(cursor + 1)..-1]
  798. end
  799. },
  800. ctrl(:m) => proc {
  801. got = pick
  802. exit 0
  803. },
  804. ctrl(:u) => proc {
  805. yanked = input[0...cursor] if cursor > 0
  806. input = input[cursor..-1]
  807. cursor = 0
  808. },
  809. ctrl(:a) => proc { cursor = 0; nil },
  810. ctrl(:e) => proc { cursor = input.length; nil },
  811. ctrl(:j) => proc { vselect { |v| v - (@reverse ? -1 : 1) } },
  812. ctrl(:k) => proc { vselect { |v| v + (@reverse ? -1 : 1) } },
  813. ctrl(:w) => proc {
  814. pcursor = cursor
  815. backword.call
  816. yanked = input[cursor...pcursor] if pcursor > cursor
  817. input = input[0...cursor] + input[pcursor..-1]
  818. },
  819. ctrl(:y) => proc { actions[:default].call yanked },
  820. ctrl(:h) => proc { input[cursor -= 1] = '' if cursor > 0 },
  821. ctrl(:i) => proc { |o|
  822. if @multi && sel = pick
  823. if @selects.has_key? sel
  824. @selects.delete sel
  825. else
  826. @selects[sel] = 1
  827. end
  828. vselect { |v| v + case o
  829. when :stab then 1
  830. when :sclick then 0
  831. else -1
  832. end * (@reverse ? -1 : 1) }
  833. end
  834. },
  835. ctrl(:b) => proc { cursor = [0, cursor - 1].max; nil },
  836. ctrl(:f) => proc { cursor = [input.length, cursor + 1].min; nil },
  837. ctrl(:l) => proc { render { C.clear; C.refresh }; update_list true },
  838. :del => proc { input[cursor] = '' if input.length > cursor },
  839. :pgup => proc { vselect { |_| max_items } },
  840. :pgdn => proc { vselect { |_| 0 } },
  841. :alt_b => proc { backword.call; nil },
  842. :alt_f => proc {
  843. cursor += (input[cursor..-1].index(/(\S\s)|(.$)/) || -1) + 1
  844. nil
  845. },
  846. :default => proc { |val|
  847. case val
  848. when String
  849. input.insert cursor, val
  850. cursor += val.length
  851. when Hash
  852. event = val[:event]
  853. case event
  854. when :click, :release
  855. x, y, shift = val.values_at :x, :y, :shift
  856. if y == cursor_y
  857. cursor = [0, [input.length, x - 2].min].max
  858. elsif x > 1 && y <= max_items
  859. tv = max_items - y - 1
  860. case event
  861. when :click
  862. vselect { |_| tv }
  863. actions[ctrl(:i)].call(:sclick) if shift
  864. mouse_event.v = tv
  865. when :release
  866. if !shift && mouse_event.double?(tv)
  867. actions[ctrl(:m)].call
  868. end
  869. end
  870. end
  871. when :scroll
  872. diff, shift = val.values_at :diff, :shift
  873. actions[ctrl(:i)].call(:sclick) if shift
  874. actions[ctrl(diff > 0 ? :j : :k)].call
  875. end
  876. end
  877. }
  878. }
  879. actions[ctrl(:p)] = actions[ctrl(:k)]
  880. actions[ctrl(:n)] = actions[ctrl(:j)]
  881. actions[:stab] = actions[ctrl(:i)]
  882. actions[127] = actions[ctrl(:h)]
  883. actions[ctrl(:q)] = actions[ctrl(:g)] = actions[ctrl(:c)] = actions[:esc]
  884. while true
  885. @cursor_x.set cursor
  886. render { print_input }
  887. if key = get_input(actions)
  888. upd = actions.fetch(key, actions[:default]).call(key)
  889. # Dispatch key event
  890. emit(:key) { [@query.set(input.dup), cursor] } if upd
  891. end
  892. end
  893. ensure
  894. C.close_screen
  895. if got
  896. if @selects.empty?
  897. @stdout.puts got
  898. else
  899. @selects.each do |sel, _|
  900. @stdout.puts sel
  901. end
  902. end
  903. end
  904. end
  905. end
  906. class Matcher
  907. class MatchData
  908. def initialize n
  909. @n = n
  910. end
  911. def offset _
  912. @n
  913. end
  914. end
  915. def initialize nth, delim
  916. @nth = nth && nth.map { |n| n > 0 ? n - 1 : n }
  917. @delim = delim
  918. @tokens_cache = {}
  919. end
  920. def tokenize str
  921. @tokens_cache[str] ||=
  922. unless @delim
  923. # AWK default
  924. prefix_length = str[/^\s+/].length rescue 0
  925. [prefix_length, (str.strip.scan(/\S+\s*/) rescue [])]
  926. else
  927. prefix_length = 0
  928. [prefix_length, (str.scan(@delim) rescue [])]
  929. end
  930. end
  931. def do_match str, pat
  932. if @nth
  933. prefix_length, tokens = tokenize str
  934. @nth.each do |n|
  935. if (token = tokens[n]) && (md = token.match(pat) rescue nil)
  936. prefix_length += (tokens[0...n] || []).join.length
  937. offset = md.offset(0).map { |o| o + prefix_length }
  938. return MatchData.new(offset)
  939. end
  940. end
  941. nil
  942. else
  943. str.match(pat) rescue nil
  944. end
  945. end
  946. end
  947. class FuzzyMatcher < Matcher
  948. attr_reader :caches, :rxflag
  949. def initialize rxflag, nth = nil, delim = nil
  950. super nth, delim
  951. @caches = Hash.new { |h, k| h[k] = {} }
  952. @regexp = {}
  953. @rxflag = rxflag
  954. end
  955. def empty? q
  956. q.empty?
  957. end
  958. def rxflag_for q
  959. @rxflag || (q =~ /[A-Z]/ ? 0 : Regexp::IGNORECASE)
  960. end
  961. def fuzzy_regex q
  962. @regexp[q] ||= begin
  963. q = q.downcase if @rxflag == Regexp::IGNORECASE
  964. Regexp.new(q.split(//).inject('') { |sum, e|
  965. e = Regexp.escape e
  966. sum << (e.length > 1 ? "(?:#{e}).*?" : # FIXME: not equivalent
  967. "#{e}[^#{e}]*?")
  968. }, rxflag_for(q))
  969. end
  970. end
  971. def match list, q, prefix, suffix
  972. regexp = fuzzy_regex q
  973. cache = @caches[list.object_id]
  974. prefix_cache = nil
  975. (prefix.length - 1).downto(1) do |len|
  976. break if prefix_cache = cache[prefix[0, len]]
  977. end
  978. suffix_cache = nil
  979. 0.upto(suffix.length - 1) do |idx|
  980. break if suffix_cache = cache[suffix[idx..-1]]
  981. end unless suffix.empty?
  982. partial_cache = [prefix_cache,
  983. suffix_cache].compact.sort_by { |e| e.length }.first
  984. cache[q] ||= (partial_cache ?
  985. partial_cache.map { |e| e.first } : list).map { |line|
  986. # Ignore errors: e.g. invalid byte sequence in UTF-8
  987. md = do_match(line, regexp)
  988. md && [line, [md.offset(0)]]
  989. }.compact
  990. end
  991. end
  992. class ExtendedFuzzyMatcher < FuzzyMatcher
  993. def initialize rxflag, mode = :fuzzy, nth = nil, delim = nil
  994. super rxflag, nth, delim
  995. @regexps = {}
  996. @mode = mode
  997. end
  998. def empty? q
  999. parse(q).empty?
  1000. end
  1001. def parse q
  1002. q = q.strip
  1003. @regexps[q] ||= q.split(/\s+/).map { |w|
  1004. invert =
  1005. if w =~ /^!/
  1006. w = w[1..-1]
  1007. true
  1008. end
  1009. [ @regexp[w] ||=
  1010. case w
  1011. when ''
  1012. nil
  1013. when /^\^(.*)\$$/
  1014. Regexp.new('^' << Regexp.escape($1) << '$', rxflag_for(w))
  1015. when /^'/
  1016. if @mode == :fuzzy && w.length > 1
  1017. exact_regex w[1..-1]
  1018. elsif @mode == :exact
  1019. exact_regex w
  1020. end
  1021. when /^\^/
  1022. w.length > 1 ?
  1023. Regexp.new('^' << Regexp.escape(w[1..-1]), rxflag_for(w)) : nil
  1024. when /\$$/
  1025. w.length > 1 ?
  1026. Regexp.new(Regexp.escape(w[0..-2]) << '$', rxflag_for(w)) : nil
  1027. else
  1028. @mode == :fuzzy ? fuzzy_regex(w) : exact_regex(w)
  1029. end, invert ]
  1030. }.select { |pair| pair.first }
  1031. end
  1032. def exact_regex w
  1033. Regexp.new(Regexp.escape(w), rxflag_for(w))
  1034. end
  1035. def match list, q, prefix, suffix
  1036. regexps = parse q
  1037. # Look for prefix cache
  1038. cache = @caches[list.object_id]
  1039. prefix = prefix.strip.sub(/\$\S*$/, '').sub(/(^|\s)!\S*$/, '')
  1040. prefix_cache = nil
  1041. (prefix.length - 1).downto(1) do |len|
  1042. break if prefix_cache = cache[Set[@regexps[prefix[0, len]]]]
  1043. end
  1044. cache[Set[regexps]] ||= (prefix_cache ?
  1045. prefix_cache.map { |e| e.first } :
  1046. list).map { |line|
  1047. offsets = []
  1048. regexps.all? { |pair|
  1049. regexp, invert = pair
  1050. md = do_match(line, regexp)
  1051. if md && !invert
  1052. offsets << md.offset(0)
  1053. elsif !md && invert
  1054. true
  1055. end
  1056. } && [line, offsets]
  1057. }.select { |e| e }
  1058. end
  1059. end
  1060. end#FZF
  1061. FZF.new(ARGV, $stdin).start if ENV.fetch('FZF_EXECUTABLE', '1') == '1'