2015-01-01 19:49:30 +00:00
|
|
|
package fzf
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2015-07-21 15:19:37 +00:00
|
|
|
"io/ioutil"
|
2015-01-01 19:49:30 +00:00
|
|
|
"os"
|
|
|
|
"regexp"
|
2015-05-31 07:46:54 +00:00
|
|
|
"strconv"
|
2015-01-01 19:49:30 +00:00
|
|
|
"strings"
|
2015-03-28 17:59:32 +00:00
|
|
|
"unicode/utf8"
|
|
|
|
|
|
|
|
"github.com/junegunn/fzf/src/curses"
|
2015-01-12 03:56:17 +00:00
|
|
|
|
|
|
|
"github.com/junegunn/go-shellwords"
|
2015-01-01 19:49:30 +00:00
|
|
|
)
|
|
|
|
|
2015-01-11 18:01:24 +00:00
|
|
|
const usage = `usage: fzf [options]
|
2015-01-01 19:49:30 +00:00
|
|
|
|
2015-06-08 14:28:41 +00:00
|
|
|
Search
|
2015-01-01 19:49:30 +00:00
|
|
|
-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])
|
2015-07-21 16:12:50 +00:00
|
|
|
--with-nth=N[,..] Transform item using index expressions within finder
|
2015-01-01 19:49:30 +00:00
|
|
|
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
|
2015-02-25 16:42:15 +00:00
|
|
|
+s, --no-sort Do not sort the result
|
2015-07-21 16:12:50 +00:00
|
|
|
--tac Reverse the order of the input
|
|
|
|
--tiebreak=CRITERION Sort criterion when the scores are tied;
|
2015-04-16 05:19:28 +00:00
|
|
|
[length|begin|end|index] (default: length)
|
2015-01-01 19:49:30 +00:00
|
|
|
|
|
|
|
Interface
|
|
|
|
-m, --multi Enable multi-select with tab/shift-tab
|
2015-07-21 16:12:50 +00:00
|
|
|
--ansi Enable processing of ANSI color codes
|
|
|
|
--no-mouse Disable mouse
|
|
|
|
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
|
|
|
|
--black Use black background
|
|
|
|
--reverse Reverse orientation
|
|
|
|
--cycle Enable cyclic scroll
|
|
|
|
--no-hscroll Disable horizontal scroll
|
|
|
|
--inline-info Display finder info inline with the query
|
|
|
|
--prompt=STR Input prompt (default: '> ')
|
|
|
|
--bind=KEYBINDS Custom key bindings. Refer to the man page.
|
|
|
|
--history=FILE History file
|
|
|
|
--history-size=N Maximum number of history entries (default: 1000)
|
|
|
|
--header-file=FILE The file whose content to be printed as header
|
2015-07-21 18:21:20 +00:00
|
|
|
--header-lines=N The first N lines of the input are treated as header
|
2015-01-01 19:49:30 +00:00
|
|
|
|
|
|
|
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.
|
2015-07-21 16:12:50 +00:00
|
|
|
--print-query Print query as the first line
|
|
|
|
--expect=KEYS Comma-separated list of keys to complete fzf
|
|
|
|
--sync Synchronous search for multi-staged filtering
|
2015-01-01 19:49:30 +00:00
|
|
|
|
|
|
|
Environment variables
|
|
|
|
FZF_DEFAULT_COMMAND Default command to use when input is tty
|
2015-02-13 03:25:19 +00:00
|
|
|
FZF_DEFAULT_OPTS Defaults options. (e.g. '-x -m')
|
2015-01-01 19:49:30 +00:00
|
|
|
|
|
|
|
`
|
|
|
|
|
2015-01-11 18:01:24 +00:00
|
|
|
// Mode denotes the current search mode
|
2015-01-01 19:49:30 +00:00
|
|
|
type Mode int
|
|
|
|
|
2015-01-11 18:01:24 +00:00
|
|
|
// Search modes
|
2015-01-01 19:49:30 +00:00
|
|
|
const (
|
2015-01-11 18:01:24 +00:00
|
|
|
ModeFuzzy Mode = iota
|
|
|
|
ModeExtended
|
|
|
|
ModeExtendedExact
|
2015-01-01 19:49:30 +00:00
|
|
|
)
|
|
|
|
|
2015-01-11 18:01:24 +00:00
|
|
|
// Case denotes case-sensitivity of search
|
2015-01-01 19:49:30 +00:00
|
|
|
type Case int
|
|
|
|
|
2015-01-11 18:01:24 +00:00
|
|
|
// Case-sensitivities
|
2015-01-01 19:49:30 +00:00
|
|
|
const (
|
2015-01-11 18:01:24 +00:00
|
|
|
CaseSmart Case = iota
|
|
|
|
CaseIgnore
|
|
|
|
CaseRespect
|
2015-01-01 19:49:30 +00:00
|
|
|
)
|
|
|
|
|
2015-04-16 05:19:28 +00:00
|
|
|
// Sort criteria
|
|
|
|
type tiebreak int
|
|
|
|
|
|
|
|
const (
|
|
|
|
byLength tiebreak = iota
|
|
|
|
byBegin
|
|
|
|
byEnd
|
|
|
|
byIndex
|
|
|
|
)
|
|
|
|
|
2015-01-11 18:01:24 +00:00
|
|
|
// Options stores the values of command-line options
|
2015-01-01 19:49:30 +00:00
|
|
|
type Options struct {
|
2015-07-21 18:21:20 +00:00
|
|
|
Mode Mode
|
|
|
|
Case Case
|
|
|
|
Nth []Range
|
|
|
|
WithNth []Range
|
|
|
|
Delimiter *regexp.Regexp
|
|
|
|
Sort int
|
|
|
|
Tac bool
|
|
|
|
Tiebreak tiebreak
|
|
|
|
Multi bool
|
|
|
|
Ansi bool
|
|
|
|
Mouse bool
|
|
|
|
Theme *curses.ColorTheme
|
|
|
|
Black bool
|
|
|
|
Reverse bool
|
|
|
|
Cycle bool
|
|
|
|
Hscroll bool
|
|
|
|
InlineInfo bool
|
|
|
|
Prompt string
|
|
|
|
Query string
|
|
|
|
Select1 bool
|
|
|
|
Exit0 bool
|
|
|
|
Filter *string
|
|
|
|
ToggleSort bool
|
|
|
|
Expect map[int]string
|
|
|
|
Keymap map[int]actionType
|
|
|
|
Execmap map[int]string
|
|
|
|
PrintQuery bool
|
|
|
|
ReadZero bool
|
|
|
|
Sync bool
|
|
|
|
History *History
|
|
|
|
Header []string
|
|
|
|
HeaderLines int
|
|
|
|
Version bool
|
2015-01-01 19:49:30 +00:00
|
|
|
}
|
|
|
|
|
2015-05-31 07:46:54 +00:00
|
|
|
func defaultTheme() *curses.ColorTheme {
|
2015-04-17 17:52:30 +00:00
|
|
|
if strings.Contains(os.Getenv("TERM"), "256") {
|
2015-05-31 07:46:54 +00:00
|
|
|
return curses.Dark256
|
2015-04-17 17:52:30 +00:00
|
|
|
}
|
2015-05-31 07:46:54 +00:00
|
|
|
return curses.Default16
|
|
|
|
}
|
2015-04-17 17:52:30 +00:00
|
|
|
|
2015-05-31 07:46:54 +00:00
|
|
|
func defaultOptions() *Options {
|
2015-01-01 19:49:30 +00:00
|
|
|
return &Options{
|
2015-07-21 18:21:20 +00:00
|
|
|
Mode: ModeFuzzy,
|
|
|
|
Case: CaseSmart,
|
|
|
|
Nth: make([]Range, 0),
|
|
|
|
WithNth: make([]Range, 0),
|
|
|
|
Delimiter: nil,
|
|
|
|
Sort: 1000,
|
|
|
|
Tac: false,
|
|
|
|
Tiebreak: byLength,
|
|
|
|
Multi: false,
|
|
|
|
Ansi: false,
|
|
|
|
Mouse: true,
|
|
|
|
Theme: defaultTheme(),
|
|
|
|
Black: false,
|
|
|
|
Reverse: false,
|
|
|
|
Cycle: false,
|
|
|
|
Hscroll: true,
|
|
|
|
InlineInfo: false,
|
|
|
|
Prompt: "> ",
|
|
|
|
Query: "",
|
|
|
|
Select1: false,
|
|
|
|
Exit0: false,
|
|
|
|
Filter: nil,
|
|
|
|
ToggleSort: false,
|
|
|
|
Expect: make(map[int]string),
|
|
|
|
Keymap: defaultKeymap(),
|
|
|
|
Execmap: make(map[int]string),
|
|
|
|
PrintQuery: false,
|
|
|
|
ReadZero: false,
|
|
|
|
Sync: false,
|
|
|
|
History: nil,
|
|
|
|
Header: make([]string, 0),
|
|
|
|
HeaderLines: 0,
|
|
|
|
Version: false}
|
2015-01-01 19:49:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func help(ok int) {
|
2015-01-11 18:01:24 +00:00
|
|
|
os.Stderr.WriteString(usage)
|
2015-01-01 19:49:30 +00:00
|
|
|
os.Exit(ok)
|
|
|
|
}
|
|
|
|
|
|
|
|
func errorExit(msg string) {
|
|
|
|
os.Stderr.WriteString(msg + "\n")
|
|
|
|
help(1)
|
|
|
|
}
|
|
|
|
|
2015-06-14 02:23:07 +00:00
|
|
|
func optString(arg string, prefixes ...string) (bool, string) {
|
|
|
|
for _, prefix := range prefixes {
|
|
|
|
if strings.HasPrefix(arg, prefix) {
|
|
|
|
return true, arg[len(prefix):]
|
|
|
|
}
|
2015-01-01 19:49:30 +00:00
|
|
|
}
|
2015-01-11 18:01:24 +00:00
|
|
|
return false, ""
|
2015-01-01 19:49:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func nextString(args []string, i *int, message string) string {
|
|
|
|
if len(args) > *i+1 {
|
|
|
|
*i++
|
|
|
|
} else {
|
|
|
|
errorExit(message)
|
|
|
|
}
|
|
|
|
return args[*i]
|
|
|
|
}
|
|
|
|
|
2015-05-31 07:46:54 +00:00
|
|
|
func optionalNextString(args []string, i *int) string {
|
|
|
|
if len(args) > *i+1 {
|
|
|
|
*i++
|
|
|
|
return args[*i]
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2015-06-13 15:43:44 +00:00
|
|
|
func atoi(str string) int {
|
|
|
|
num, err := strconv.Atoi(str)
|
|
|
|
if err != nil {
|
|
|
|
errorExit("not a valid integer: " + str)
|
|
|
|
}
|
|
|
|
return num
|
|
|
|
}
|
|
|
|
|
|
|
|
func nextInt(args []string, i *int, message string) int {
|
|
|
|
if len(args) > *i+1 {
|
|
|
|
*i++
|
|
|
|
} else {
|
|
|
|
errorExit(message)
|
|
|
|
}
|
|
|
|
return atoi(args[*i])
|
|
|
|
}
|
|
|
|
|
2015-01-01 19:49:30 +00:00
|
|
|
func optionalNumeric(args []string, i *int) int {
|
|
|
|
if len(args) > *i+1 {
|
|
|
|
if strings.IndexAny(args[*i+1], "0123456789") == 0 {
|
|
|
|
*i++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 1 // Don't care
|
|
|
|
}
|
|
|
|
|
|
|
|
func splitNth(str string) []Range {
|
|
|
|
if match, _ := regexp.MatchString("^[0-9,-.]+$", str); !match {
|
|
|
|
errorExit("invalid format: " + str)
|
|
|
|
}
|
|
|
|
|
|
|
|
tokens := strings.Split(str, ",")
|
|
|
|
ranges := make([]Range, len(tokens))
|
|
|
|
for idx, s := range tokens {
|
|
|
|
r, ok := ParseRange(&s)
|
|
|
|
if !ok {
|
|
|
|
errorExit("invalid format: " + str)
|
|
|
|
}
|
|
|
|
ranges[idx] = r
|
|
|
|
}
|
|
|
|
return ranges
|
|
|
|
}
|
|
|
|
|
|
|
|
func delimiterRegexp(str string) *regexp.Regexp {
|
|
|
|
rx, e := regexp.Compile(str)
|
|
|
|
if e != nil {
|
|
|
|
str = regexp.QuoteMeta(str)
|
|
|
|
}
|
|
|
|
|
|
|
|
rx, e = regexp.Compile(fmt.Sprintf("(?:.*?%s)|(?:.+?$)", str))
|
|
|
|
if e != nil {
|
|
|
|
errorExit("invalid regular expression: " + e.Error())
|
|
|
|
}
|
|
|
|
return rx
|
|
|
|
}
|
|
|
|
|
2015-03-28 17:59:32 +00:00
|
|
|
func isAlphabet(char uint8) bool {
|
|
|
|
return char >= 'a' && char <= 'z'
|
|
|
|
}
|
|
|
|
|
2015-06-18 15:31:48 +00:00
|
|
|
func parseKeyChords(str string, message string) map[int]string {
|
2015-03-31 13:05:02 +00:00
|
|
|
if len(str) == 0 {
|
|
|
|
errorExit(message)
|
|
|
|
}
|
|
|
|
|
|
|
|
tokens := strings.Split(str, ",")
|
|
|
|
if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Index(str, ",,,") >= 0 {
|
|
|
|
tokens = append(tokens, ",")
|
|
|
|
}
|
|
|
|
|
2015-06-18 15:31:48 +00:00
|
|
|
chords := make(map[int]string)
|
2015-03-31 13:05:02 +00:00
|
|
|
for _, key := range tokens {
|
|
|
|
if len(key) == 0 {
|
|
|
|
continue // ignore
|
|
|
|
}
|
2015-03-28 17:59:32 +00:00
|
|
|
lkey := strings.ToLower(key)
|
2015-06-14 16:26:18 +00:00
|
|
|
chord := 0
|
2015-06-18 15:31:48 +00:00
|
|
|
switch lkey {
|
|
|
|
case "up":
|
|
|
|
chord = curses.Up
|
|
|
|
case "down":
|
|
|
|
chord = curses.Down
|
|
|
|
case "left":
|
|
|
|
chord = curses.Left
|
|
|
|
case "right":
|
|
|
|
chord = curses.Right
|
|
|
|
case "enter", "return":
|
|
|
|
chord = curses.CtrlM
|
|
|
|
case "space":
|
|
|
|
chord = curses.AltZ + int(' ')
|
|
|
|
case "bspace", "bs":
|
|
|
|
chord = curses.BSpace
|
|
|
|
case "alt-bs", "alt-bspace":
|
|
|
|
chord = curses.AltBS
|
|
|
|
case "tab":
|
|
|
|
chord = curses.Tab
|
|
|
|
case "btab", "shift-tab":
|
|
|
|
chord = curses.BTab
|
|
|
|
case "esc":
|
|
|
|
chord = curses.ESC
|
|
|
|
case "del":
|
|
|
|
chord = curses.Del
|
|
|
|
case "home":
|
|
|
|
chord = curses.Home
|
|
|
|
case "end":
|
|
|
|
chord = curses.End
|
|
|
|
case "pgup", "page-up":
|
|
|
|
chord = curses.PgUp
|
|
|
|
case "pgdn", "page-down":
|
|
|
|
chord = curses.PgDn
|
|
|
|
case "shift-left":
|
|
|
|
chord = curses.SLeft
|
|
|
|
case "shift-right":
|
|
|
|
chord = curses.SRight
|
|
|
|
default:
|
2015-06-14 16:26:18 +00:00
|
|
|
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
|
|
|
|
chord = curses.CtrlA + int(lkey[5]) - 'a'
|
|
|
|
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
|
|
|
|
chord = curses.AltA + int(lkey[4]) - 'a'
|
|
|
|
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' {
|
|
|
|
chord = curses.F1 + int(key[1]) - '1'
|
|
|
|
} else if utf8.RuneCountInString(key) == 1 {
|
|
|
|
chord = curses.AltZ + int([]rune(key)[0])
|
|
|
|
} else {
|
|
|
|
errorExit("unsupported key: " + key)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if chord > 0 {
|
2015-06-18 15:31:48 +00:00
|
|
|
chords[chord] = key
|
2015-03-28 17:59:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return chords
|
|
|
|
}
|
|
|
|
|
2015-04-16 05:19:28 +00:00
|
|
|
func parseTiebreak(str string) tiebreak {
|
|
|
|
switch strings.ToLower(str) {
|
|
|
|
case "length":
|
|
|
|
return byLength
|
|
|
|
case "index":
|
|
|
|
return byIndex
|
|
|
|
case "begin":
|
|
|
|
return byBegin
|
|
|
|
case "end":
|
|
|
|
return byEnd
|
|
|
|
default:
|
|
|
|
errorExit("invalid sort criterion: " + str)
|
|
|
|
}
|
|
|
|
return byLength
|
|
|
|
}
|
|
|
|
|
2015-05-31 07:46:54 +00:00
|
|
|
func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
|
|
|
|
dupe := *theme
|
|
|
|
return &dupe
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme {
|
|
|
|
theme := dupeTheme(defaultTheme)
|
|
|
|
for _, str := range strings.Split(strings.ToLower(str), ",") {
|
|
|
|
switch str {
|
|
|
|
case "dark":
|
|
|
|
theme = dupeTheme(curses.Dark256)
|
|
|
|
case "light":
|
|
|
|
theme = dupeTheme(curses.Light256)
|
|
|
|
case "16":
|
|
|
|
theme = dupeTheme(curses.Default16)
|
|
|
|
case "bw", "no":
|
|
|
|
theme = nil
|
|
|
|
default:
|
|
|
|
fail := func() {
|
|
|
|
errorExit("invalid color specification: " + str)
|
|
|
|
}
|
|
|
|
// Color is disabled
|
|
|
|
if theme == nil {
|
|
|
|
errorExit("colors disabled; cannot customize colors")
|
|
|
|
}
|
|
|
|
|
|
|
|
pair := strings.Split(str, ":")
|
|
|
|
if len(pair) != 2 {
|
|
|
|
fail()
|
|
|
|
}
|
|
|
|
ansi32, err := strconv.Atoi(pair[1])
|
|
|
|
if err != nil || ansi32 < -1 || ansi32 > 255 {
|
|
|
|
fail()
|
|
|
|
}
|
|
|
|
ansi := int16(ansi32)
|
|
|
|
switch pair[0] {
|
|
|
|
case "fg":
|
|
|
|
theme.Fg = ansi
|
|
|
|
theme.UseDefault = theme.UseDefault && ansi < 0
|
|
|
|
case "bg":
|
|
|
|
theme.Bg = ansi
|
|
|
|
theme.UseDefault = theme.UseDefault && ansi < 0
|
|
|
|
case "fg+":
|
|
|
|
theme.Current = ansi
|
|
|
|
case "bg+":
|
|
|
|
theme.DarkBg = ansi
|
|
|
|
case "hl":
|
|
|
|
theme.Match = ansi
|
|
|
|
case "hl+":
|
|
|
|
theme.CurrentMatch = ansi
|
|
|
|
case "prompt":
|
|
|
|
theme.Prompt = ansi
|
|
|
|
case "spinner":
|
|
|
|
theme.Spinner = ansi
|
|
|
|
case "info":
|
|
|
|
theme.Info = ansi
|
|
|
|
case "pointer":
|
|
|
|
theme.Cursor = ansi
|
|
|
|
case "marker":
|
|
|
|
theme.Selected = ansi
|
2015-07-21 15:19:37 +00:00
|
|
|
case "header":
|
|
|
|
theme.Header = ansi
|
2015-05-31 07:46:54 +00:00
|
|
|
default:
|
|
|
|
fail()
|
|
|
|
}
|
|
|
|
}
|
2015-04-17 17:52:30 +00:00
|
|
|
}
|
2015-05-31 07:46:54 +00:00
|
|
|
return theme
|
2015-04-17 17:52:30 +00:00
|
|
|
}
|
|
|
|
|
2015-06-14 14:36:49 +00:00
|
|
|
var executeRegexp *regexp.Regexp
|
|
|
|
|
2015-06-18 15:31:48 +00:00
|
|
|
func firstKey(keymap map[int]string) int {
|
|
|
|
for k := range keymap {
|
|
|
|
return k
|
|
|
|
}
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
2015-07-16 10:18:23 +00:00
|
|
|
const (
|
|
|
|
escapedColon = 0
|
|
|
|
escapedComma = 1
|
|
|
|
)
|
|
|
|
|
2015-06-14 03:25:08 +00:00
|
|
|
func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) {
|
2015-06-14 14:36:49 +00:00
|
|
|
if executeRegexp == nil {
|
|
|
|
// Backreferences are not supported.
|
2015-06-15 14:25:00 +00:00
|
|
|
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
|
2015-06-14 14:36:49 +00:00
|
|
|
executeRegexp = regexp.MustCompile(
|
2015-06-15 14:25:00 +00:00
|
|
|
"(?s):execute:.*|:execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
|
2015-06-14 14:36:49 +00:00
|
|
|
}
|
|
|
|
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
|
2015-06-14 03:25:08 +00:00
|
|
|
return ":execute(" + strings.Repeat(" ", len(src)-10) + ")"
|
|
|
|
})
|
2015-07-16 10:18:23 +00:00
|
|
|
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
|
|
|
|
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
|
2015-06-14 03:25:08 +00:00
|
|
|
|
|
|
|
idx := 0
|
|
|
|
for _, pairStr := range strings.Split(masked, ",") {
|
2015-07-16 10:18:23 +00:00
|
|
|
origPairStr := str[idx : idx+len(pairStr)]
|
2015-06-14 03:25:08 +00:00
|
|
|
idx += len(pairStr) + 1
|
|
|
|
|
|
|
|
pair := strings.SplitN(pairStr, ":", 2)
|
2015-07-17 17:31:35 +00:00
|
|
|
if len(pair) < 2 {
|
|
|
|
errorExit("bind action not specified: " + origPairStr)
|
2015-05-20 12:25:15 +00:00
|
|
|
}
|
2015-07-16 10:18:23 +00:00
|
|
|
var key int
|
|
|
|
if len(pair[0]) == 1 && pair[0][0] == escapedColon {
|
|
|
|
key = ':' + curses.AltZ
|
|
|
|
} else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
|
|
|
|
key = ',' + curses.AltZ
|
|
|
|
} else {
|
|
|
|
keys := parseKeyChords(pair[0], "key name required")
|
|
|
|
key = firstKey(keys)
|
2015-05-20 12:25:15 +00:00
|
|
|
}
|
2015-07-16 10:18:23 +00:00
|
|
|
|
|
|
|
act := origPairStr[len(pair[0])+1 : len(origPairStr)]
|
|
|
|
actLower := strings.ToLower(act)
|
|
|
|
switch actLower {
|
2015-06-13 16:54:56 +00:00
|
|
|
case "ignore":
|
|
|
|
keymap[key] = actIgnore
|
2015-05-20 12:25:15 +00:00
|
|
|
case "beginning-of-line":
|
|
|
|
keymap[key] = actBeginningOfLine
|
|
|
|
case "abort":
|
|
|
|
keymap[key] = actAbort
|
|
|
|
case "accept":
|
|
|
|
keymap[key] = actAccept
|
|
|
|
case "backward-char":
|
|
|
|
keymap[key] = actBackwardChar
|
|
|
|
case "backward-delete-char":
|
|
|
|
keymap[key] = actBackwardDeleteChar
|
|
|
|
case "backward-word":
|
|
|
|
keymap[key] = actBackwardWord
|
|
|
|
case "clear-screen":
|
|
|
|
keymap[key] = actClearScreen
|
|
|
|
case "delete-char":
|
|
|
|
keymap[key] = actDeleteChar
|
|
|
|
case "end-of-line":
|
|
|
|
keymap[key] = actEndOfLine
|
|
|
|
case "forward-char":
|
|
|
|
keymap[key] = actForwardChar
|
|
|
|
case "forward-word":
|
|
|
|
keymap[key] = actForwardWord
|
|
|
|
case "kill-line":
|
|
|
|
keymap[key] = actKillLine
|
|
|
|
case "kill-word":
|
|
|
|
keymap[key] = actKillWord
|
|
|
|
case "unix-line-discard", "line-discard":
|
|
|
|
keymap[key] = actUnixLineDiscard
|
|
|
|
case "unix-word-rubout", "word-rubout":
|
|
|
|
keymap[key] = actUnixWordRubout
|
|
|
|
case "yank":
|
|
|
|
keymap[key] = actYank
|
|
|
|
case "backward-kill-word":
|
|
|
|
keymap[key] = actBackwardKillWord
|
|
|
|
case "toggle-down":
|
|
|
|
keymap[key] = actToggleDown
|
|
|
|
case "toggle-up":
|
|
|
|
keymap[key] = actToggleUp
|
2015-06-09 14:44:54 +00:00
|
|
|
case "toggle-all":
|
|
|
|
keymap[key] = actToggleAll
|
|
|
|
case "select-all":
|
|
|
|
keymap[key] = actSelectAll
|
|
|
|
case "deselect-all":
|
|
|
|
keymap[key] = actDeselectAll
|
2015-05-20 16:35:35 +00:00
|
|
|
case "toggle":
|
|
|
|
keymap[key] = actToggle
|
2015-05-20 12:25:15 +00:00
|
|
|
case "down":
|
|
|
|
keymap[key] = actDown
|
|
|
|
case "up":
|
|
|
|
keymap[key] = actUp
|
|
|
|
case "page-up":
|
|
|
|
keymap[key] = actPageUp
|
|
|
|
case "page-down":
|
|
|
|
keymap[key] = actPageDown
|
2015-06-13 15:43:44 +00:00
|
|
|
case "previous-history":
|
|
|
|
keymap[key] = actPreviousHistory
|
|
|
|
case "next-history":
|
|
|
|
keymap[key] = actNextHistory
|
2015-05-20 12:25:15 +00:00
|
|
|
case "toggle-sort":
|
|
|
|
keymap[key] = actToggleSort
|
|
|
|
toggleSort = true
|
|
|
|
default:
|
2015-07-16 10:18:23 +00:00
|
|
|
if isExecuteAction(actLower) {
|
2015-06-14 03:25:08 +00:00
|
|
|
keymap[key] = actExecute
|
2015-07-16 10:18:23 +00:00
|
|
|
if act[7] == ':' {
|
|
|
|
execmap[key] = act[8:]
|
2015-06-15 14:00:38 +00:00
|
|
|
} else {
|
2015-07-16 10:18:23 +00:00
|
|
|
execmap[key] = act[8 : len(act)-1]
|
2015-06-15 14:00:38 +00:00
|
|
|
}
|
2015-06-14 03:25:08 +00:00
|
|
|
} else {
|
|
|
|
errorExit("unknown action: " + act)
|
|
|
|
}
|
2015-05-20 12:25:15 +00:00
|
|
|
}
|
|
|
|
}
|
2015-06-14 03:25:08 +00:00
|
|
|
return keymap, execmap, toggleSort
|
|
|
|
}
|
|
|
|
|
|
|
|
func isExecuteAction(str string) bool {
|
|
|
|
if !strings.HasPrefix(str, "execute") || len(str) < 9 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
b := str[7]
|
|
|
|
e := str[len(str)-1]
|
2015-06-15 14:25:00 +00:00
|
|
|
if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' ||
|
|
|
|
b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") {
|
2015-06-14 03:25:08 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
2015-05-20 12:25:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType {
|
2015-06-18 15:31:48 +00:00
|
|
|
keys := parseKeyChords(str, "key name required")
|
2015-03-31 13:05:02 +00:00
|
|
|
if len(keys) != 1 {
|
|
|
|
errorExit("multiple keys specified")
|
|
|
|
}
|
2015-06-18 15:31:48 +00:00
|
|
|
keymap[firstKey(keys)] = actToggleSort
|
2015-05-20 12:25:15 +00:00
|
|
|
return keymap
|
2015-03-31 13:05:02 +00:00
|
|
|
}
|
|
|
|
|
2015-07-21 15:19:37 +00:00
|
|
|
func readHeaderFile(filename string) []string {
|
|
|
|
content, err := ioutil.ReadFile(filename)
|
|
|
|
if err != nil {
|
|
|
|
errorExit("failed to read header file: " + filename)
|
|
|
|
}
|
|
|
|
return strings.Split(strings.TrimSuffix(string(content), "\n"), "\n")
|
|
|
|
}
|
|
|
|
|
2015-01-01 19:49:30 +00:00
|
|
|
func parseOptions(opts *Options, allArgs []string) {
|
2015-06-13 15:43:44 +00:00
|
|
|
keymap := make(map[int]actionType)
|
|
|
|
var historyMax int
|
|
|
|
if opts.History == nil {
|
|
|
|
historyMax = defaultHistoryMax
|
|
|
|
} else {
|
|
|
|
historyMax = opts.History.maxSize
|
|
|
|
}
|
|
|
|
setHistory := func(path string) {
|
|
|
|
h, e := NewHistory(path, historyMax)
|
|
|
|
if e != nil {
|
|
|
|
errorExit(e.Error())
|
|
|
|
}
|
|
|
|
opts.History = h
|
|
|
|
}
|
|
|
|
setHistoryMax := func(max int) {
|
|
|
|
historyMax = max
|
|
|
|
if historyMax < 1 {
|
|
|
|
errorExit("history max must be a positive integer")
|
|
|
|
}
|
|
|
|
if opts.History != nil {
|
|
|
|
opts.History.maxSize = historyMax
|
|
|
|
}
|
|
|
|
}
|
2015-01-01 19:49:30 +00:00
|
|
|
for i := 0; i < len(allArgs); i++ {
|
|
|
|
arg := allArgs[i]
|
|
|
|
switch arg {
|
|
|
|
case "-h", "--help":
|
|
|
|
help(0)
|
|
|
|
case "-x", "--extended":
|
2015-01-11 18:01:24 +00:00
|
|
|
opts.Mode = ModeExtended
|
2015-01-01 19:49:30 +00:00
|
|
|
case "-e", "--extended-exact":
|
2015-01-11 18:01:24 +00:00
|
|
|
opts.Mode = ModeExtendedExact
|
2015-01-01 19:49:30 +00:00
|
|
|
case "+x", "--no-extended", "+e", "--no-extended-exact":
|
2015-01-11 18:01:24 +00:00
|
|
|
opts.Mode = ModeFuzzy
|
2015-01-01 19:49:30 +00:00
|
|
|
case "-q", "--query":
|
|
|
|
opts.Query = nextString(allArgs, &i, "query string required")
|
|
|
|
case "-f", "--filter":
|
|
|
|
filter := nextString(allArgs, &i, "query string required")
|
|
|
|
opts.Filter = &filter
|
2015-03-28 17:59:32 +00:00
|
|
|
case "--expect":
|
2015-06-18 15:31:48 +00:00
|
|
|
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required")
|
2015-04-16 05:19:28 +00:00
|
|
|
case "--tiebreak":
|
|
|
|
opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
|
2015-05-20 12:25:15 +00:00
|
|
|
case "--bind":
|
2015-06-14 03:25:08 +00:00
|
|
|
keymap, opts.Execmap, opts.ToggleSort =
|
|
|
|
parseKeymap(keymap, opts.Execmap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required"))
|
2015-04-17 17:52:30 +00:00
|
|
|
case "--color":
|
2015-05-31 07:46:54 +00:00
|
|
|
spec := optionalNextString(allArgs, &i)
|
|
|
|
if len(spec) == 0 {
|
|
|
|
opts.Theme = defaultTheme()
|
|
|
|
} else {
|
|
|
|
opts.Theme = parseTheme(opts.Theme, spec)
|
|
|
|
}
|
2015-03-31 13:05:02 +00:00
|
|
|
case "--toggle-sort":
|
2015-06-13 15:43:44 +00:00
|
|
|
keymap = checkToggleSort(keymap, nextString(allArgs, &i, "key name required"))
|
2015-05-20 12:25:15 +00:00
|
|
|
opts.ToggleSort = true
|
2015-01-01 19:49:30 +00:00
|
|
|
case "-d", "--delimiter":
|
|
|
|
opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required"))
|
|
|
|
case "-n", "--nth":
|
|
|
|
opts.Nth = splitNth(nextString(allArgs, &i, "nth expression required"))
|
|
|
|
case "--with-nth":
|
|
|
|
opts.WithNth = splitNth(nextString(allArgs, &i, "nth expression required"))
|
|
|
|
case "-s", "--sort":
|
|
|
|
opts.Sort = optionalNumeric(allArgs, &i)
|
|
|
|
case "+s", "--no-sort":
|
|
|
|
opts.Sort = 0
|
2015-02-25 16:42:15 +00:00
|
|
|
case "--tac":
|
|
|
|
opts.Tac = true
|
|
|
|
case "--no-tac":
|
|
|
|
opts.Tac = false
|
2015-01-01 19:49:30 +00:00
|
|
|
case "-i":
|
2015-01-11 18:01:24 +00:00
|
|
|
opts.Case = CaseIgnore
|
2015-01-01 19:49:30 +00:00
|
|
|
case "+i":
|
2015-01-11 18:01:24 +00:00
|
|
|
opts.Case = CaseRespect
|
2015-01-01 19:49:30 +00:00
|
|
|
case "-m", "--multi":
|
|
|
|
opts.Multi = true
|
|
|
|
case "+m", "--no-multi":
|
|
|
|
opts.Multi = false
|
2015-03-18 16:59:14 +00:00
|
|
|
case "--ansi":
|
|
|
|
opts.Ansi = true
|
|
|
|
case "--no-ansi":
|
|
|
|
opts.Ansi = false
|
2015-01-01 19:49:30 +00:00
|
|
|
case "--no-mouse":
|
|
|
|
opts.Mouse = false
|
|
|
|
case "+c", "--no-color":
|
2015-04-17 17:52:30 +00:00
|
|
|
opts.Theme = nil
|
2015-01-01 19:49:30 +00:00
|
|
|
case "+2", "--no-256":
|
2015-04-17 17:52:30 +00:00
|
|
|
opts.Theme = curses.Default16
|
2015-01-01 19:49:30 +00:00
|
|
|
case "--black":
|
|
|
|
opts.Black = true
|
|
|
|
case "--no-black":
|
|
|
|
opts.Black = false
|
|
|
|
case "--reverse":
|
|
|
|
opts.Reverse = true
|
|
|
|
case "--no-reverse":
|
|
|
|
opts.Reverse = false
|
2015-06-16 14:14:57 +00:00
|
|
|
case "--cycle":
|
|
|
|
opts.Cycle = true
|
|
|
|
case "--no-cycle":
|
|
|
|
opts.Cycle = false
|
2015-04-16 03:56:01 +00:00
|
|
|
case "--hscroll":
|
|
|
|
opts.Hscroll = true
|
|
|
|
case "--no-hscroll":
|
|
|
|
opts.Hscroll = false
|
2015-04-21 14:50:53 +00:00
|
|
|
case "--inline-info":
|
|
|
|
opts.InlineInfo = true
|
|
|
|
case "--no-inline-info":
|
|
|
|
opts.InlineInfo = false
|
2015-01-01 19:49:30 +00:00
|
|
|
case "-1", "--select-1":
|
|
|
|
opts.Select1 = true
|
|
|
|
case "+1", "--no-select-1":
|
|
|
|
opts.Select1 = false
|
|
|
|
case "-0", "--exit-0":
|
|
|
|
opts.Exit0 = true
|
|
|
|
case "+0", "--no-exit-0":
|
|
|
|
opts.Exit0 = false
|
2015-06-21 08:29:58 +00:00
|
|
|
case "--read0":
|
2015-06-08 06:36:21 +00:00
|
|
|
opts.ReadZero = true
|
2015-06-21 08:29:58 +00:00
|
|
|
case "--no-read0":
|
|
|
|
opts.ReadZero = false
|
2015-01-01 19:49:30 +00:00
|
|
|
case "--print-query":
|
|
|
|
opts.PrintQuery = true
|
|
|
|
case "--no-print-query":
|
|
|
|
opts.PrintQuery = false
|
|
|
|
case "--prompt":
|
|
|
|
opts.Prompt = nextString(allArgs, &i, "prompt string required")
|
2015-02-13 03:25:19 +00:00
|
|
|
case "--sync":
|
|
|
|
opts.Sync = true
|
|
|
|
case "--no-sync":
|
|
|
|
opts.Sync = false
|
|
|
|
case "--async":
|
|
|
|
opts.Sync = false
|
2015-06-13 15:43:44 +00:00
|
|
|
case "--no-history":
|
|
|
|
opts.History = nil
|
|
|
|
case "--history":
|
|
|
|
setHistory(nextString(allArgs, &i, "history file path required"))
|
2015-06-18 16:03:25 +00:00
|
|
|
case "--history-size":
|
2015-06-13 15:43:44 +00:00
|
|
|
setHistoryMax(nextInt(allArgs, &i, "history max size required"))
|
2015-07-21 18:21:20 +00:00
|
|
|
case "--no-header-file":
|
|
|
|
opts.Header = []string{}
|
|
|
|
case "--no-header-lines":
|
|
|
|
opts.HeaderLines = 0
|
2015-07-21 15:19:37 +00:00
|
|
|
case "--header-file":
|
|
|
|
opts.Header = readHeaderFile(
|
|
|
|
nextString(allArgs, &i, "header file name required"))
|
2015-07-21 18:21:20 +00:00
|
|
|
opts.HeaderLines = 0
|
|
|
|
case "--header-lines":
|
|
|
|
opts.Header = []string{}
|
|
|
|
opts.HeaderLines = atoi(
|
|
|
|
nextString(allArgs, &i, "number of header lines required"))
|
2015-01-01 19:49:30 +00:00
|
|
|
case "--version":
|
|
|
|
opts.Version = true
|
|
|
|
default:
|
2015-06-14 02:23:07 +00:00
|
|
|
if match, value := optString(arg, "-q", "--query="); match {
|
2015-01-01 19:49:30 +00:00
|
|
|
opts.Query = value
|
2015-06-14 02:23:07 +00:00
|
|
|
} else if match, value := optString(arg, "-f", "--filter="); match {
|
2015-01-01 19:49:30 +00:00
|
|
|
opts.Filter = &value
|
2015-06-14 02:23:07 +00:00
|
|
|
} else if match, value := optString(arg, "-d", "--delimiter="); match {
|
2015-01-01 19:49:30 +00:00
|
|
|
opts.Delimiter = delimiterRegexp(value)
|
|
|
|
} else if match, value := optString(arg, "--prompt="); match {
|
|
|
|
opts.Prompt = value
|
2015-06-14 02:23:07 +00:00
|
|
|
} else if match, value := optString(arg, "-n", "--nth="); match {
|
2015-01-01 19:49:30 +00:00
|
|
|
opts.Nth = splitNth(value)
|
|
|
|
} else if match, value := optString(arg, "--with-nth="); match {
|
|
|
|
opts.WithNth = splitNth(value)
|
2015-06-14 02:23:07 +00:00
|
|
|
} else if match, _ := optString(arg, "-s", "--sort="); match {
|
2015-01-01 19:49:30 +00:00
|
|
|
opts.Sort = 1 // Don't care
|
2015-03-31 13:05:02 +00:00
|
|
|
} else if match, value := optString(arg, "--toggle-sort="); match {
|
2015-06-13 15:43:44 +00:00
|
|
|
keymap = checkToggleSort(keymap, value)
|
2015-05-20 12:25:15 +00:00
|
|
|
opts.ToggleSort = true
|
2015-03-28 17:59:32 +00:00
|
|
|
} else if match, value := optString(arg, "--expect="); match {
|
2015-06-18 15:31:48 +00:00
|
|
|
opts.Expect = parseKeyChords(value, "key names required")
|
2015-04-16 05:19:28 +00:00
|
|
|
} else if match, value := optString(arg, "--tiebreak="); match {
|
|
|
|
opts.Tiebreak = parseTiebreak(value)
|
2015-04-17 17:52:30 +00:00
|
|
|
} else if match, value := optString(arg, "--color="); match {
|
2015-05-31 07:46:54 +00:00
|
|
|
opts.Theme = parseTheme(opts.Theme, value)
|
2015-05-20 12:25:15 +00:00
|
|
|
} else if match, value := optString(arg, "--bind="); match {
|
2015-06-14 03:25:08 +00:00
|
|
|
keymap, opts.Execmap, opts.ToggleSort =
|
|
|
|
parseKeymap(keymap, opts.Execmap, opts.ToggleSort, value)
|
2015-06-13 15:43:44 +00:00
|
|
|
} else if match, value := optString(arg, "--history="); match {
|
|
|
|
setHistory(value)
|
2015-06-18 16:03:25 +00:00
|
|
|
} else if match, value := optString(arg, "--history-size="); match {
|
2015-06-13 15:43:44 +00:00
|
|
|
setHistoryMax(atoi(value))
|
2015-07-21 15:19:37 +00:00
|
|
|
} else if match, value := optString(arg, "--header-file="); match {
|
|
|
|
opts.Header = readHeaderFile(value)
|
2015-07-21 18:21:20 +00:00
|
|
|
opts.HeaderLines = 0
|
|
|
|
} else if match, value := optString(arg, "--header-lines="); match {
|
|
|
|
opts.Header = []string{}
|
|
|
|
opts.HeaderLines = atoi(value)
|
2015-01-01 19:49:30 +00:00
|
|
|
} else {
|
|
|
|
errorExit("unknown option: " + arg)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-01-22 21:26:00 +00:00
|
|
|
|
2015-07-22 04:45:38 +00:00
|
|
|
if opts.HeaderLines < 0 {
|
|
|
|
errorExit("header lines must be a non-negative integer")
|
|
|
|
}
|
|
|
|
|
2015-06-13 15:43:44 +00:00
|
|
|
// Change default actions for CTRL-N / CTRL-P when --history is used
|
|
|
|
if opts.History != nil {
|
|
|
|
if _, prs := keymap[curses.CtrlP]; !prs {
|
|
|
|
keymap[curses.CtrlP] = actPreviousHistory
|
|
|
|
}
|
|
|
|
if _, prs := keymap[curses.CtrlN]; !prs {
|
|
|
|
keymap[curses.CtrlN] = actNextHistory
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Override default key bindings
|
|
|
|
for key, act := range keymap {
|
|
|
|
opts.Keymap[key] = act
|
|
|
|
}
|
|
|
|
|
2015-01-22 21:26:00 +00:00
|
|
|
// If we're not using extended search mode, --nth option becomes irrelevant
|
|
|
|
// if it contains the whole range
|
|
|
|
if opts.Mode == ModeFuzzy || len(opts.Nth) == 1 {
|
|
|
|
for _, r := range opts.Nth {
|
|
|
|
if r.begin == rangeEllipsis && r.end == rangeEllipsis {
|
|
|
|
opts.Nth = make([]Range, 0)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-01-01 19:49:30 +00:00
|
|
|
}
|
|
|
|
|
2015-01-11 18:01:24 +00:00
|
|
|
// ParseOptions parses command-line options
|
2015-01-01 19:49:30 +00:00
|
|
|
func ParseOptions() *Options {
|
2015-01-11 18:01:24 +00:00
|
|
|
opts := defaultOptions()
|
2015-01-01 19:49:30 +00:00
|
|
|
|
|
|
|
// Options from Env var
|
|
|
|
words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS"))
|
|
|
|
parseOptions(opts, words)
|
|
|
|
|
|
|
|
// Options from command-line arguments
|
|
|
|
parseOptions(opts, os.Args[1:])
|
|
|
|
return opts
|
|
|
|
}
|