fzf/src/options.go

1934 lines
54 KiB
Go
Raw Normal View History

2015-01-01 19:49:30 +00:00
package fzf
import (
"fmt"
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"
"unicode"
2016-09-07 00:58:18 +00:00
"github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/tui"
2015-01-12 03:56:17 +00:00
"github.com/mattn/go-runewidth"
2017-06-01 23:27:17 +00:00
"github.com/mattn/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
-x, --extended Extended-search mode
(enabled by default; +x or --no-extended to disable)
-e, --exact Enable Exact-match
-i Case-insensitive match (default: smart-case match)
+i Case-sensitive match
--scheme=SCHEME Scoring scheme [default|path|history]
--literal Do not normalize latin script letters before matching
-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 presentation of each line using
field index expressions
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
+s, --no-sort Do not sort the result
--tac Reverse the order of the input
--disabled Do not perform search
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
when the scores are tied [length|chunk|begin|end|index]
(default: length)
2015-01-01 19:49:30 +00:00
Interface
-m, --multi[=MAX] Enable multi-select with tab/shift-tab
--no-mouse Disable mouse
--bind=KEYBINDS Custom key bindings. Refer to the man page.
--cycle Enable cyclic scroll
--keep-right Keep the right end of the line visible on overflow
--scroll-off=LINES Number of screen lines to keep above or below when
scrolling to the top or to the bottom (default: 0)
--no-hscroll Disable horizontal scroll
--hscroll-off=COLS Number of screen columns to keep to the right of the
highlighted substring (default: 10)
--filepath-word Make word-wise movements respect path separators
--jump-labels=CHARS Label characters for jump and jump-accept
Layout
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given
height instead of using fullscreen.
If prefixed with '~', fzf will determine the height
according to the input size.
--min-height=HEIGHT Minimum height when --height is given in percent
(default: 10)
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
--border[=STYLE] Draw border around the finder
[rounded|sharp|horizontal|vertical|
top|bottom|left|right|none] (default: rounded)
--border-label=LABEL Label to print on the border
--border-label-pos=COL Position of the border label
[POSITIVE_INTEGER: columns from left|
NEGATIVE_INTEGER: columns from right][:bottom]
(default: 0 or center)
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
--info=STYLE Finder info style [default|inline|hidden[:nosep]]
--prompt=STR Input prompt (default: '> ')
--pointer=STR Pointer to the current line (default: '>')
--marker=STR Multi-select marker (default: '>')
--header=STR String to print as header
--header-lines=N The first N lines of the input are treated as header
--header-first Print header before the prompt line
--ellipsis=STR Ellipsis to show when line is truncated (default: '..')
2015-01-01 19:49:30 +00:00
Display
--ansi Enable processing of ANSI color codes
--tabstop=SPACES Number of spaces for a tab character (default: 8)
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
--no-bold Do not use bold text
History
--history=FILE History file
--history-size=N Maximum number of history entries (default: 1000)
Preview
--preview=COMMAND Command to preview highlighted line ({})
--preview-window=OPT Preview window layout (default: right:50%)
[up|down|left|right][,SIZE[%]]
[,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden]
[,border-BORDER_OPT]
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
--preview-label=LABEL
--preview-label-pos=N Same as --border-label and --border-label-pos,
but for preview window
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.
--print-query Print query as the first line
--expect=KEYS Comma-separated list of keys to complete fzf
--read0 Read input delimited by ASCII NUL characters
--print0 Print output delimited by ASCII NUL characters
--sync Synchronous search for multi-staged filtering
--version Display version information and exit
2015-01-01 19:49:30 +00:00
Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty
FZF_DEFAULT_OPTS Default options
(e.g. '--layout=reverse --inline-info')
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
)
// Sort criteria
type criterion int
const (
2016-09-07 00:58:18 +00:00
byScore criterion = iota
byChunk
byLength
byBegin
byEnd
)
type heightSpec struct {
size float64
percent bool
auto bool
}
type sizeSpec struct {
size float64
percent bool
}
func defaultMargin() [4]sizeSpec {
return [4]sizeSpec{}
}
type windowPosition int
const (
posUp windowPosition = iota
posDown
posLeft
posRight
)
type layoutType int
const (
layoutDefault layoutType = iota
layoutReverse
layoutReverseList
)
type infoLayout int
type infoStyle struct {
layout infoLayout
separator bool
}
const (
infoDefault infoLayout = iota
infoInline
infoHidden
)
type labelOpts struct {
label string
column int
bottom bool
}
type previewOpts struct {
command string
position windowPosition
size sizeSpec
scroll string
hidden bool
wrap bool
cycle bool
follow bool
border tui.BorderShape
headerLines int
threshold int
alternative *previewOpts
2015-07-26 14:02:04 +00:00
}
func parseLabelPosition(opts *labelOpts, arg string) {
opts.column = 0
opts.bottom = false
for _, token := range splitRegexp.Split(strings.ToLower(arg), -1) {
switch token {
case "center":
opts.column = 0
case "bottom":
opts.bottom = true
case "top":
opts.bottom = false
default:
opts.column = atoi(token)
}
}
}
func (a previewOpts) aboveOrBelow() bool {
return a.size.size > 0 && (a.position == posUp || a.position == posDown)
}
func (a previewOpts) sameLayout(b previewOpts) bool {
return a.size == b.size && a.position == b.position && a.border == b.border && a.hidden == b.hidden && a.threshold == b.threshold &&
(a.alternative != nil && b.alternative != nil && a.alternative.sameLayout(*b.alternative) ||
a.alternative == nil && b.alternative == nil)
}
func (a previewOpts) sameContentLayout(b previewOpts) bool {
return a.wrap == b.wrap && a.headerLines == b.headerLines
}
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 {
Fuzzy bool
FuzzyAlgo algo.Algo
Scheme string
Extended bool
Phony bool
Case Case
Normalize bool
Nth []Range
WithNth []Range
Delimiter Delimiter
Sort int
Tac bool
Criteria []criterion
Multi int
Ansi bool
Mouse bool
Theme *tui.ColorTheme
Black bool
Bold bool
Height heightSpec
MinHeight int
Layout layoutType
Cycle bool
KeepRight bool
Hscroll bool
HscrollOff int
ScrollOff int
FileWord bool
InfoStyle infoStyle
JumpLabels string
Prompt string
Pointer string
Marker string
Query string
Select1 bool
Exit0 bool
Filter *string
ToggleSort bool
Expect map[tui.Event]string
Keymap map[tui.Event][]*action
Preview previewOpts
PrintQuery bool
ReadZero bool
Printer func(string)
PrintSep string
Sync bool
History *History
Header []string
HeaderLines int
HeaderFirst bool
Ellipsis string
Margin [4]sizeSpec
Padding [4]sizeSpec
BorderShape tui.BorderShape
BorderLabel labelOpts
PreviewLabel labelOpts
Unicode bool
Tabstop int
ClearOnExit bool
Version bool
2015-01-01 19:49:30 +00:00
}
func defaultPreviewOpts(command string) previewOpts {
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded, 0, 0, nil}
}
2015-05-31 07:46:54 +00:00
func defaultOptions() *Options {
2015-01-01 19:49:30 +00:00
return &Options{
Fuzzy: true,
FuzzyAlgo: algo.FuzzyMatchV2,
Scheme: "default",
Extended: true,
Phony: false,
Case: CaseSmart,
Normalize: true,
Nth: make([]Range, 0),
WithNth: make([]Range, 0),
Delimiter: Delimiter{},
Sort: 1000,
Tac: false,
Criteria: []criterion{byScore, byLength},
Multi: 0,
Ansi: false,
Mouse: true,
Theme: tui.EmptyTheme(),
Black: false,
Bold: true,
MinHeight: 10,
Layout: layoutDefault,
Cycle: false,
KeepRight: false,
Hscroll: true,
HscrollOff: 10,
ScrollOff: 0,
FileWord: false,
InfoStyle: infoStyle{layout: infoDefault, separator: true},
JumpLabels: defaultJumpLabels,
Prompt: "> ",
Pointer: ">",
Marker: ">",
Query: "",
Select1: false,
Exit0: false,
Filter: nil,
ToggleSort: false,
Expect: make(map[tui.Event]string),
Keymap: make(map[tui.Event][]*action),
Preview: defaultPreviewOpts(""),
PrintQuery: false,
ReadZero: false,
Printer: func(str string) { fmt.Println(str) },
PrintSep: "\n",
Sync: false,
History: nil,
Header: make([]string, 0),
HeaderLines: 0,
HeaderFirst: false,
Ellipsis: "..",
Margin: defaultMargin(),
Padding: defaultMargin(),
Unicode: true,
Tabstop: 8,
BorderLabel: labelOpts{},
PreviewLabel: labelOpts{},
ClearOnExit: true,
Version: false}
2015-01-01 19:49:30 +00:00
}
func help(code int) {
os.Stdout.WriteString(usage)
os.Exit(code)
2015-01-01 19:49:30 +00:00
}
func errorExit(msg string) {
os.Stderr.WriteString(msg + "\n")
os.Exit(exitError)
2015-01-01 19:49:30 +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]
}
func optionalNextString(args []string, i *int) (bool, string) {
if len(args) > *i+1 && !strings.HasPrefix(args[*i+1], "-") && !strings.HasPrefix(args[*i+1], "+") {
2015-05-31 07:46:54 +00:00
*i++
return true, args[*i]
2015-05-31 07:46:54 +00:00
}
return false, ""
2015-05-31 07:46:54 +00:00
}
func atoi(str string) int {
num, err := strconv.Atoi(str)
if err != nil {
errorExit("not a valid integer: " + str)
}
return num
}
2015-07-26 14:02:04 +00:00
func atof(str string) float64 {
num, err := strconv.ParseFloat(str, 64)
if err != nil {
errorExit("not a valid number: " + 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])
}
func optionalNumeric(args []string, i *int, defaultValue int) int {
2015-01-01 19:49:30 +00:00
if len(args) > *i+1 {
if strings.IndexAny(args[*i+1], "0123456789") == 0 {
*i++
return atoi(args[*i])
2015-01-01 19:49:30 +00:00
}
}
return defaultValue
2015-01-01 19:49:30 +00:00
}
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) Delimiter {
// Special handling of \t
str = strings.Replace(str, "\\t", "\t", -1)
// 1. Pattern does not contain any special character
if regexp.QuoteMeta(str) == str {
return Delimiter{str: &str}
}
2015-01-01 19:49:30 +00:00
rx, e := regexp.Compile(str)
// 2. Pattern is not a valid regular expression
2015-01-01 19:49:30 +00:00
if e != nil {
return Delimiter{str: &str}
2015-01-01 19:49:30 +00:00
}
// 3. Pattern as regular expression. Slow.
return Delimiter{regex: rx}
2015-01-01 19:49:30 +00:00
}
func isAlphabet(char uint8) bool {
return char >= 'a' && char <= 'z'
}
func isNumeric(char uint8) bool {
return char >= '0' && char <= '9'
}
2016-09-07 00:58:18 +00:00
func parseAlgo(str string) algo.Algo {
switch str {
case "v1":
return algo.FuzzyMatchV1
case "v2":
return algo.FuzzyMatchV2
default:
errorExit("invalid algorithm (expected: v1 or v2)")
}
return algo.FuzzyMatchV2
}
func processScheme(opts *Options) {
if !algo.Init(opts.Scheme) {
errorExit("invalid scoring scheme (expected: default|path|history)")
}
if opts.Scheme == "history" {
opts.Criteria = []criterion{byScore}
}
}
func parseBorder(str string, optional bool) tui.BorderShape {
switch str {
case "rounded":
return tui.BorderRounded
case "sharp":
return tui.BorderSharp
case "bold":
return tui.BorderBold
case "double":
return tui.BorderDouble
case "horizontal":
return tui.BorderHorizontal
case "vertical":
return tui.BorderVertical
case "top":
return tui.BorderTop
case "bottom":
return tui.BorderBottom
case "left":
return tui.BorderLeft
case "right":
return tui.BorderRight
case "none":
return tui.BorderNone
default:
if optional && str == "" {
return tui.BorderRounded
}
errorExit("invalid border style (expected: rounded|sharp|bold|double|horizontal|vertical|top|bottom|left|right|none)")
}
return tui.BorderNone
}
func parseKeyChords(str string, message string) map[tui.Event]string {
2015-03-31 13:05:02 +00:00
if len(str) == 0 {
errorExit(message)
}
str = regexp.MustCompile("(?i)(alt-),").ReplaceAllString(str, "$1"+string([]rune{escapedComma}))
2015-03-31 13:05:02 +00:00
tokens := strings.Split(str, ",")
Improvements to code quality and readability (#1737) * Remove 1 unused field and 3 unused functions unused elements fount by running golangci-lint run --disable-all --enable unused src/result.go:19:2: field `index` is unused (unused) index int32 ^ src/tui/light.go:716:23: func `(*LightWindow).stderr` is unused (unused) func (w *LightWindow) stderr(str string) { ^ src/terminal.go:1015:6: func `numLinesMax` is unused (unused) func numLinesMax(str string, max int) int { ^ src/tui/tui.go:167:20: func `ColorPair.is24` is unused (unused) func (p ColorPair) is24() bool { ^ * Address warnings from "gosimple" linter src/options.go:389:83: S1003: should use strings.Contains(str, ",,,") instead (gosimple) if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Index(str, ",,,") >= 0 { ^ src/options.go:630:18: S1007: should use raw string (`...`) with regexp.MustCompile to avoid having to escape twice (gosimple) executeRegexp = regexp.MustCompile( ^ src/terminal.go:29:16: S1007: should use raw string (`...`) with regexp.MustCompile to avoid having to escape twice (gosimple) placeholder = regexp.MustCompile("\\\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\\+?f?nf?})") ^ src/terminal_test.go:92:10: S1007: should use raw string (`...`) with regexp.MustCompile to avoid having to escape twice (gosimple) regex = regexp.MustCompile("\\w+") ^ * Address warnings from "staticcheck" linter src/algo/algo.go:374:2: SA4006: this value of `offset32` is never used (staticcheck) offset32, T := alloc32(offset32, slab, N) ^ src/algo/algo.go:456:2: SA4006: this value of `offset16` is never used (staticcheck) offset16, C := alloc16(offset16, slab, width*M) ^ src/tui/tui.go:119:2: SA9004: only the first constant in this group has an explicit type (staticcheck) colUndefined Color = -2 ^
2019-11-05 00:46:51 +00:00
if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Contains(str, ",,,") {
2015-03-31 13:05:02 +00:00
tokens = append(tokens, ",")
}
chords := make(map[tui.Event]string)
2015-03-31 13:05:02 +00:00
for _, key := range tokens {
if len(key) == 0 {
continue // ignore
}
key = strings.ReplaceAll(key, string([]rune{escapedComma}), ",")
lkey := strings.ToLower(key)
add := func(e tui.EventType) {
chords[e.AsEvent()] = key
}
switch lkey {
case "up":
add(tui.Up)
case "down":
add(tui.Down)
case "left":
add(tui.Left)
case "right":
add(tui.Right)
case "enter", "return":
add(tui.CtrlM)
case "space":
chords[tui.Key(' ')] = key
case "bspace", "bs":
add(tui.BSpace)
case "ctrl-space":
add(tui.CtrlSpace)
2019-11-14 13:39:25 +00:00
case "ctrl-^", "ctrl-6":
add(tui.CtrlCaret)
2019-11-14 13:39:25 +00:00
case "ctrl-/", "ctrl-_":
add(tui.CtrlSlash)
2019-11-14 13:39:25 +00:00
case "ctrl-\\":
add(tui.CtrlBackSlash)
2019-11-14 13:39:25 +00:00
case "ctrl-]":
add(tui.CtrlRightBracket)
case "change":
add(tui.Change)
2020-06-07 14:07:03 +00:00
case "backward-eof":
add(tui.BackwardEOF)
case "start":
add(tui.Start)
case "alt-enter", "alt-return":
chords[tui.CtrlAltKey('m')] = key
case "alt-space":
chords[tui.AltKey(' ')] = key
case "alt-bs", "alt-bspace":
add(tui.AltBS)
case "alt-up":
add(tui.AltUp)
case "alt-down":
add(tui.AltDown)
case "alt-left":
add(tui.AltLeft)
case "alt-right":
add(tui.AltRight)
case "tab":
add(tui.Tab)
case "btab", "shift-tab":
add(tui.BTab)
case "esc":
add(tui.ESC)
case "del":
add(tui.Del)
case "home":
add(tui.Home)
case "end":
add(tui.End)
case "insert":
add(tui.Insert)
case "pgup", "page-up":
add(tui.PgUp)
case "pgdn", "page-down":
add(tui.PgDn)
case "alt-shift-up", "shift-alt-up":
add(tui.AltSUp)
case "alt-shift-down", "shift-alt-down":
add(tui.AltSDown)
case "alt-shift-left", "shift-alt-left":
add(tui.AltSLeft)
case "alt-shift-right", "shift-alt-right":
add(tui.AltSRight)
case "shift-up":
add(tui.SUp)
case "shift-down":
add(tui.SDown)
case "shift-left":
add(tui.SLeft)
case "shift-right":
add(tui.SRight)
case "left-click":
add(tui.LeftClick)
case "right-click":
add(tui.RightClick)
2015-10-12 17:24:38 +00:00
case "double-click":
add(tui.DoubleClick)
2016-05-18 13:25:09 +00:00
case "f10":
add(tui.F10)
case "f11":
add(tui.F11)
case "f12":
add(tui.F12)
default:
runes := []rune(key)
if len(key) == 10 && strings.HasPrefix(lkey, "ctrl-alt-") && isAlphabet(lkey[9]) {
chords[tui.CtrlAltKey(rune(key[9]))] = key
} else if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
add(tui.EventType(tui.CtrlA.Int() + int(lkey[5]) - 'a'))
} else if len(runes) == 5 && strings.HasPrefix(lkey, "alt-") {
r := runes[4]
switch r {
case escapedColon:
r = ':'
case escapedComma:
r = ','
case escapedPlus:
r = '+'
}
chords[tui.AltKey(r)] = key
2016-05-18 13:25:09 +00:00
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' {
add(tui.EventType(tui.F1.Int() + int(key[1]) - '1'))
} else if len(runes) == 1 {
chords[tui.Key(runes[0])] = key
} else {
errorExit("unsupported key: " + key)
}
}
}
return chords
}
func parseTiebreak(str string) []criterion {
2016-09-07 00:58:18 +00:00
criteria := []criterion{byScore}
hasIndex := false
hasChunk := false
hasLength := false
hasBegin := false
hasEnd := false
check := func(notExpected *bool, name string) {
if *notExpected {
errorExit("duplicate sort criteria: " + name)
}
if hasIndex {
errorExit("index should be the last criterion")
}
*notExpected = true
}
for _, str := range strings.Split(strings.ToLower(str), ",") {
switch str {
case "index":
check(&hasIndex, "index")
case "chunk":
check(&hasChunk, "chunk")
criteria = append(criteria, byChunk)
case "length":
check(&hasLength, "length")
criteria = append(criteria, byLength)
case "begin":
check(&hasBegin, "begin")
criteria = append(criteria, byBegin)
case "end":
check(&hasEnd, "end")
criteria = append(criteria, byEnd)
default:
errorExit("invalid sort criterion: " + str)
}
}
if len(criteria) > 4 {
errorExit("at most 3 tiebreaks are allowed: " + str)
}
return criteria
}
func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme {
dupe := *theme
return &dupe
2015-05-31 07:46:54 +00:00
}
func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
2015-05-31 07:46:54 +00:00
theme := dupeTheme(defaultTheme)
rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$")
2015-05-31 07:46:54 +00:00
for _, str := range strings.Split(strings.ToLower(str), ",") {
switch str {
case "dark":
theme = dupeTheme(tui.Dark256)
2015-05-31 07:46:54 +00:00
case "light":
theme = dupeTheme(tui.Light256)
2015-05-31 07:46:54 +00:00
case "16":
theme = dupeTheme(tui.Default16)
2015-05-31 07:46:54 +00:00
case "bw", "no":
2020-10-28 16:27:08 +00:00
theme = tui.NoColorTheme()
2015-05-31 07:46:54 +00:00
default:
fail := func() {
errorExit("invalid color specification: " + str)
}
// Color is disabled
if theme == nil {
continue
2015-05-31 07:46:54 +00:00
}
components := strings.Split(str, ":")
if len(components) < 2 {
2015-05-31 07:46:54 +00:00
fail()
}
mergeAttr := func(cattr *tui.ColorAttr) {
for _, component := range components[1:] {
switch component {
case "regular":
cattr.Attr = tui.AttrRegular
case "bold", "strong":
cattr.Attr |= tui.Bold
case "dim":
cattr.Attr |= tui.Dim
case "italic":
cattr.Attr |= tui.Italic
case "underline":
cattr.Attr |= tui.Underline
case "blink":
cattr.Attr |= tui.Blink
case "reverse":
cattr.Attr |= tui.Reverse
case "strikethrough":
cattr.Attr |= tui.StrikeThrough
case "black":
cattr.Color = tui.Color(0)
case "red":
cattr.Color = tui.Color(1)
case "green":
cattr.Color = tui.Color(2)
case "yellow":
cattr.Color = tui.Color(3)
case "blue":
cattr.Color = tui.Color(4)
case "magenta":
cattr.Color = tui.Color(5)
case "cyan":
cattr.Color = tui.Color(6)
case "white":
cattr.Color = tui.Color(7)
case "bright-black", "gray", "grey":
cattr.Color = tui.Color(8)
case "bright-red":
cattr.Color = tui.Color(9)
case "bright-green":
cattr.Color = tui.Color(10)
case "bright-yellow":
cattr.Color = tui.Color(11)
case "bright-blue":
cattr.Color = tui.Color(12)
case "bright-magenta":
cattr.Color = tui.Color(13)
case "bright-cyan":
cattr.Color = tui.Color(14)
case "bright-white":
cattr.Color = tui.Color(15)
case "":
default:
if rrggbb.MatchString(component) {
cattr.Color = tui.HexToColor(component)
} else {
ansi32, err := strconv.Atoi(component)
if err != nil || ansi32 < -1 || ansi32 > 255 {
fail()
}
cattr.Color = tui.Color(ansi32)
}
}
}
2015-05-31 07:46:54 +00:00
}
switch components[0] {
case "query", "input":
mergeAttr(&theme.Input)
case "disabled":
mergeAttr(&theme.Disabled)
2015-05-31 07:46:54 +00:00
case "fg":
mergeAttr(&theme.Fg)
2015-05-31 07:46:54 +00:00
case "bg":
mergeAttr(&theme.Bg)
case "preview-fg":
mergeAttr(&theme.PreviewFg)
case "preview-bg":
mergeAttr(&theme.PreviewBg)
2015-05-31 07:46:54 +00:00
case "fg+":
mergeAttr(&theme.Current)
2015-05-31 07:46:54 +00:00
case "bg+":
mergeAttr(&theme.DarkBg)
case "gutter":
mergeAttr(&theme.Gutter)
2015-05-31 07:46:54 +00:00
case "hl":
mergeAttr(&theme.Match)
2015-05-31 07:46:54 +00:00
case "hl+":
mergeAttr(&theme.CurrentMatch)
case "border":
mergeAttr(&theme.Border)
case "separator":
mergeAttr(&theme.Separator)
case "label":
mergeAttr(&theme.BorderLabel)
2015-05-31 07:46:54 +00:00
case "prompt":
mergeAttr(&theme.Prompt)
2015-05-31 07:46:54 +00:00
case "spinner":
mergeAttr(&theme.Spinner)
2015-05-31 07:46:54 +00:00
case "info":
mergeAttr(&theme.Info)
2015-05-31 07:46:54 +00:00
case "pointer":
mergeAttr(&theme.Cursor)
2015-05-31 07:46:54 +00:00
case "marker":
mergeAttr(&theme.Selected)
2015-07-21 15:19:37 +00:00
case "header":
mergeAttr(&theme.Header)
2015-05-31 07:46:54 +00:00
default:
fail()
}
}
}
2015-05-31 07:46:54 +00:00
return theme
}
var (
executeRegexp *regexp.Regexp
splitRegexp *regexp.Regexp
)
func firstKey(keymap map[tui.Event]string) tui.Event {
for k := range keymap {
return k
}
return tui.EventType(0).AsEvent()
}
const (
escapedColon = 0
escapedComma = 1
escapedPlus = 2
)
2017-01-27 08:46:56 +00:00
func init() {
// Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile(
`(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|(?:re|un)bind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|(?:re|un)bind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
splitRegexp = regexp.MustCompile("[,:]+")
2017-01-27 08:46:56 +00:00
}
func parseKeymap(keymap map[tui.Event][]*action, str string) {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
2019-11-21 14:06:13 +00:00
symbol := ":"
if strings.HasPrefix(src, "+") {
symbol = "+"
}
prefix := symbol + "execute"
if strings.HasPrefix(src[1:], "reload") {
prefix = symbol + "reload"
} else if strings.HasPrefix(src[1:], "change-preview-window") {
prefix = symbol + "change-preview-window"
} else if strings.HasPrefix(src[1:], "change-preview") {
prefix = symbol + "change-preview"
} else if strings.HasPrefix(src[1:], "preview") {
prefix = symbol + "preview"
2021-05-22 04:13:55 +00:00
} else if strings.HasPrefix(src[1:], "unbind") {
prefix = symbol + "unbind"
} else if strings.HasPrefix(src[1:], "rebind") {
prefix = symbol + "rebind"
2020-12-04 11:34:41 +00:00
} else if strings.HasPrefix(src[1:], "change-prompt") {
prefix = symbol + "change-prompt"
} else if src[len(prefix)] == '-' {
2017-01-27 08:46:56 +00:00
c := src[len(prefix)+1]
if c == 's' || c == 'S' {
prefix += "-silent"
} else {
prefix += "-multi"
}
2015-11-08 16:42:01 +00:00
}
2017-01-27 08:46:56 +00:00
return prefix + "(" + strings.Repeat(" ", len(src)-len(prefix)-2) + ")"
})
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1)
idx := 0
for _, pairStr := range strings.Split(masked, ",") {
origPairStr := str[idx : idx+len(pairStr)]
idx += len(pairStr) + 1
pair := strings.SplitN(pairStr, ":", 2)
if len(pair) < 2 {
errorExit("bind action not specified: " + origPairStr)
2015-05-20 12:25:15 +00:00
}
var key tui.Event
if len(pair[0]) == 1 && pair[0][0] == escapedColon {
key = tui.Key(':')
} else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
key = tui.Key(',')
} else if len(pair[0]) == 1 && pair[0][0] == escapedPlus {
key = tui.Key('+')
} else {
keys := parseKeyChords(pair[0], "key name required")
key = firstKey(keys)
2015-05-20 12:25:15 +00:00
}
idx2 := len(pair[0]) + 1
specs := strings.Split(pair[1], "+")
actions := make([]*action, 0, len(specs))
appendAction := func(types ...actionType) {
actions = append(actions, toActions(types...)...)
}
prevSpec := ""
for specIndex, maskedSpec := range specs {
spec := origPairStr[idx2 : idx2+len(maskedSpec)]
idx2 += len(maskedSpec) + 1
spec = prevSpec + spec
specLower := strings.ToLower(spec)
switch specLower {
case "ignore":
appendAction(actIgnore)
case "beginning-of-line":
appendAction(actBeginningOfLine)
case "abort":
appendAction(actAbort)
case "accept":
appendAction(actAccept)
case "accept-non-empty":
appendAction(actAcceptNonEmpty)
case "print-query":
appendAction(actPrintQuery)
2020-06-20 13:04:09 +00:00
case "refresh-preview":
appendAction(actRefreshPreview)
case "replace-query":
appendAction(actReplaceQuery)
case "backward-char":
appendAction(actBackwardChar)
case "backward-delete-char":
appendAction(actBackwardDeleteChar)
case "backward-delete-char/eof":
appendAction(actBackwardDeleteCharEOF)
case "backward-word":
appendAction(actBackwardWord)
case "clear-screen":
appendAction(actClearScreen)
case "delete-char":
appendAction(actDeleteChar)
case "delete-char/eof":
appendAction(actDeleteCharEOF)
case "deselect":
appendAction(actDeselect)
case "end-of-line":
appendAction(actEndOfLine)
case "cancel":
appendAction(actCancel)
case "clear-query":
appendAction(actClearQuery)
case "clear-selection":
appendAction(actClearSelection)
case "forward-char":
appendAction(actForwardChar)
case "forward-word":
appendAction(actForwardWord)
case "jump":
appendAction(actJump)
case "jump-accept":
appendAction(actJumpAccept)
case "kill-line":
appendAction(actKillLine)
case "kill-word":
appendAction(actKillWord)
case "unix-line-discard", "line-discard":
appendAction(actUnixLineDiscard)
case "unix-word-rubout", "word-rubout":
appendAction(actUnixWordRubout)
case "yank":
appendAction(actYank)
case "backward-kill-word":
appendAction(actBackwardKillWord)
case "toggle-down":
appendAction(actToggle, actDown)
case "toggle-up":
appendAction(actToggle, actUp)
case "toggle-in":
appendAction(actToggleIn)
case "toggle-out":
appendAction(actToggleOut)
case "toggle-all":
appendAction(actToggleAll)
case "toggle-search":
appendAction(actToggleSearch)
case "select":
appendAction(actSelect)
case "select-all":
appendAction(actSelectAll)
case "deselect-all":
appendAction(actDeselectAll)
2021-02-01 15:08:54 +00:00
case "close":
appendAction(actClose)
case "toggle":
appendAction(actToggle)
case "down":
appendAction(actDown)
case "up":
appendAction(actUp)
case "first", "top":
appendAction(actFirst)
case "last":
appendAction(actLast)
case "page-up":
appendAction(actPageUp)
case "page-down":
appendAction(actPageDown)
case "half-page-up":
appendAction(actHalfPageUp)
case "half-page-down":
appendAction(actHalfPageDown)
case "previous-history":
appendAction(actPreviousHistory)
case "next-history":
appendAction(actNextHistory)
case "toggle-preview":
appendAction(actTogglePreview)
2017-02-18 14:49:00 +00:00
case "toggle-preview-wrap":
appendAction(actTogglePreviewWrap)
case "toggle-sort":
appendAction(actToggleSort)
case "preview-top":
appendAction(actPreviewTop)
case "preview-bottom":
appendAction(actPreviewBottom)
case "preview-up":
appendAction(actPreviewUp)
case "preview-down":
appendAction(actPreviewDown)
case "preview-page-up":
appendAction(actPreviewPageUp)
case "preview-page-down":
appendAction(actPreviewPageDown)
case "preview-half-page-up":
appendAction(actPreviewHalfPageUp)
case "preview-half-page-down":
appendAction(actPreviewHalfPageDown)
case "enable-search":
appendAction(actEnableSearch)
case "disable-search":
appendAction(actDisableSearch)
case "put":
if key.Type == tui.Rune && unicode.IsGraphic(key.Char) {
appendAction(actRune)
} else {
errorExit("unable to put non-printable character: " + pair[0])
}
default:
t := isExecuteAction(specLower)
if t == actIgnore {
if specIndex == 0 && specLower == "" {
actions = append(keymap[key], actions...)
} else {
errorExit("unknown action: " + spec)
}
} else {
var offset int
2017-01-27 08:46:56 +00:00
switch t {
case actReload:
offset = len("reload")
case actPreview:
offset = len("preview")
case actChangePreviewWindow:
offset = len("change-preview-window")
case actChangePreview:
offset = len("change-preview")
2020-12-04 11:34:41 +00:00
case actChangePrompt:
offset = len("change-prompt")
2021-05-22 04:13:55 +00:00
case actUnbind:
offset = len("unbind")
case actRebind:
offset = len("rebind")
2017-01-27 08:46:56 +00:00
case actExecuteSilent:
offset = len("execute-silent")
case actExecuteMulti:
offset = len("execute-multi")
2017-01-27 08:46:56 +00:00
default:
offset = len("execute")
}
2021-05-22 04:13:55 +00:00
var actionArg string
if spec[offset] == ':' {
if specIndex == len(specs)-1 {
2021-05-22 04:13:55 +00:00
actionArg = spec[offset+1:]
actions = append(actions, &action{t: t, a: actionArg})
} else {
prevSpec = spec + "+"
continue
}
} else {
2021-05-22 04:13:55 +00:00
actionArg = spec[offset+1 : len(spec)-1]
actions = append(actions, &action{t: t, a: actionArg})
2021-05-22 04:13:55 +00:00
}
if t == actUnbind || t == actRebind {
parseKeyChords(actionArg, spec[0:offset]+" target required")
}
}
}
prevSpec = ""
2015-05-20 12:25:15 +00:00
}
keymap[key] = actions
2015-05-20 12:25:15 +00:00
}
}
func isExecuteAction(str string) actionType {
2017-01-27 08:46:56 +00:00
matches := executeRegexp.FindAllStringSubmatch(":"+str, -1)
if matches == nil || len(matches) != 1 {
return actIgnore
}
2017-01-27 08:46:56 +00:00
prefix := matches[0][1]
if len(prefix) == 0 {
prefix = matches[0][2]
2015-11-08 16:42:01 +00:00
}
2017-01-27 08:46:56 +00:00
switch prefix {
case "reload":
return actReload
2021-05-22 04:13:55 +00:00
case "unbind":
return actUnbind
case "rebind":
return actRebind
case "preview":
return actPreview
case "change-preview-window":
return actChangePreviewWindow
case "change-preview":
return actChangePreview
2020-12-04 11:34:41 +00:00
case "change-prompt":
return actChangePrompt
2017-01-27 08:46:56 +00:00
case "execute":
return actExecute
case "execute-silent":
return actExecuteSilent
case "execute-multi":
return actExecuteMulti
}
return actIgnore
2015-05-20 12:25:15 +00:00
}
func parseToggleSort(keymap map[tui.Event][]*action, str string) {
keys := parseKeyChords(str, "key name required")
2015-03-31 13:05:02 +00:00
if len(keys) != 1 {
errorExit("multiple keys specified")
}
keymap[firstKey(keys)] = toActions(actToggleSort)
2015-03-31 13:05:02 +00:00
}
func strLines(str string) []string {
return strings.Split(strings.TrimSuffix(str, "\n"), "\n")
2015-07-21 15:19:37 +00:00
}
func parseSize(str string, maxPercent float64, label string) sizeSpec {
var val float64
percent := strings.HasSuffix(str, "%")
if percent {
val = atof(str[:len(str)-1])
if val < 0 {
errorExit(label + " must be non-negative")
2015-07-26 14:02:04 +00:00
}
if val > maxPercent {
errorExit(fmt.Sprintf("%s too large (max: %d%%)", label, int(maxPercent)))
}
} else {
if strings.Contains(str, ".") {
errorExit(label + " (without %) must be a non-negative integer")
}
val = float64(atoi(str))
if val < 0 {
errorExit(label + " must be non-negative")
}
}
return sizeSpec{val, percent}
}
func parseHeight(str string) heightSpec {
heightSpec := heightSpec{}
if strings.HasPrefix(str, "~") {
heightSpec.auto = true
str = str[1:]
}
2017-01-07 16:30:31 +00:00
size := parseSize(str, 100, "height")
heightSpec.size = size.size
heightSpec.percent = size.percent
return heightSpec
2017-01-07 16:30:31 +00:00
}
func parseLayout(str string) layoutType {
switch str {
case "default":
return layoutDefault
case "reverse":
return layoutReverse
case "reverse-list":
return layoutReverseList
default:
errorExit("invalid layout (expected: default / reverse / reverse-list)")
}
return layoutDefault
}
func parseInfoStyle(str string) infoStyle {
layout := infoDefault
separator := true
for _, token := range splitRegexp.Split(strings.ToLower(str), -1) {
switch token {
case "default":
layout = infoDefault
case "inline":
layout = infoInline
case "hidden":
layout = infoHidden
case "nosep":
separator = false
case "sep":
separator = true
default:
errorExit("invalid info style (expected: default|inline|hidden[:nosep])")
}
}
return infoStyle{layout: layout, separator: separator}
}
func parsePreviewWindow(opts *previewOpts, input string) {
tokenRegex := regexp.MustCompile(`[:,]*(<([1-9][0-9]*)\(([^)<]+)\)|[^,:]+)`)
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`)
headerRegex := regexp.MustCompile("^~(0|[1-9][0-9]*)$")
tokens := tokenRegex.FindAllStringSubmatch(input, -1)
var alternative string
for _, match := range tokens {
if len(match[2]) > 0 {
opts.threshold = atoi(match[2])
alternative = match[3]
continue
}
token := match[1]
switch token {
case "":
case "default":
*opts = defaultPreviewOpts(opts.command)
case "hidden":
opts.hidden = true
case "nohidden":
opts.hidden = false
case "wrap":
opts.wrap = true
case "nowrap":
opts.wrap = false
case "cycle":
opts.cycle = true
case "nocycle":
opts.cycle = false
case "up", "top":
opts.position = posUp
case "down", "bottom":
opts.position = posDown
case "left":
opts.position = posLeft
case "right":
opts.position = posRight
case "rounded", "border", "border-rounded":
opts.border = tui.BorderRounded
case "sharp", "border-sharp":
opts.border = tui.BorderSharp
case "border-bold":
opts.border = tui.BorderBold
case "border-double":
opts.border = tui.BorderDouble
case "noborder", "border-none":
opts.border = tui.BorderNone
case "border-horizontal":
opts.border = tui.BorderHorizontal
case "border-vertical":
opts.border = tui.BorderVertical
case "border-up", "border-top":
opts.border = tui.BorderTop
case "border-down", "border-bottom":
opts.border = tui.BorderBottom
case "border-left":
opts.border = tui.BorderLeft
case "border-right":
opts.border = tui.BorderRight
2020-12-05 12:16:35 +00:00
case "follow":
opts.follow = true
case "nofollow":
opts.follow = false
default:
if headerRegex.MatchString(token) {
opts.headerLines = atoi(token[1:])
} else if sizeRegex.MatchString(token) {
opts.size = parseSize(token, 99, "window size")
} else if offsetRegex.MatchString(token) {
opts.scroll = token
} else {
errorExit("invalid preview window option: " + token)
}
}
}
if len(alternative) > 0 {
alternativeOpts := *opts
opts.alternative = &alternativeOpts
2022-07-20 03:29:45 +00:00
opts.alternative.hidden = false
opts.alternative.alternative = nil
parsePreviewWindow(opts.alternative, alternative)
}
}
2020-11-09 11:34:08 +00:00
func parseMargin(opt string, margin string) [4]sizeSpec {
margins := strings.Split(margin, ",")
checked := func(str string) sizeSpec {
2020-11-09 11:34:08 +00:00
return parseSize(str, 49, opt)
2015-07-26 14:02:04 +00:00
}
switch len(margins) {
case 1:
m := checked(margins[0])
return [4]sizeSpec{m, m, m, m}
2015-07-26 14:02:04 +00:00
case 2:
tb := checked(margins[0])
rl := checked(margins[1])
return [4]sizeSpec{tb, rl, tb, rl}
2015-07-26 14:02:04 +00:00
case 3:
t := checked(margins[0])
rl := checked(margins[1])
b := checked(margins[2])
return [4]sizeSpec{t, rl, b, rl}
2015-07-26 14:02:04 +00:00
case 4:
return [4]sizeSpec{
2015-07-26 14:02:04 +00:00
checked(margins[0]), checked(margins[1]),
checked(margins[2]), checked(margins[3])}
default:
2020-11-09 11:34:08 +00:00
errorExit("invalid " + opt + ": " + margin)
2015-07-26 14:02:04 +00:00
}
return defaultMargin()
}
2015-01-01 19:49:30 +00:00
func parseOptions(opts *Options, allArgs []string) {
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
}
}
validateJumpLabels := false
validatePointer := false
validateMarker := false
2015-01-01 19:49:30 +00:00
for i := 0; i < len(allArgs); i++ {
arg := allArgs[i]
switch arg {
case "-h", "--help":
help(exitOk)
2015-01-01 19:49:30 +00:00
case "-x", "--extended":
2015-11-03 13:49:32 +00:00
opts.Extended = true
case "-e", "--exact":
opts.Fuzzy = false
case "--extended-exact":
// Note that we now don't have --no-extended-exact
opts.Fuzzy = false
opts.Extended = true
case "+x", "--no-extended":
opts.Extended = false
case "+e", "--no-exact":
opts.Fuzzy = true
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
case "--literal":
opts.Normalize = false
case "--no-literal":
opts.Normalize = true
2016-09-07 00:58:18 +00:00
case "--algo":
opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)"))
case "--scheme":
opts.Scheme = strings.ToLower(nextString(allArgs, &i, "scoring scheme required (default|path|history)"))
case "--expect":
for k, v := range parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") {
opts.Expect[k] = v
}
2017-07-04 14:02:08 +00:00
case "--no-expect":
opts.Expect = make(map[tui.Event]string)
case "--enabled", "--no-phony":
opts.Phony = false
case "--disabled", "--phony":
opts.Phony = true
case "--tiebreak":
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
2015-05-20 12:25:15 +00:00
case "--bind":
parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"))
case "--color":
_, spec := optionalNextString(allArgs, &i)
2015-05-31 07:46:54 +00:00
if len(spec) == 0 {
opts.Theme = tui.EmptyTheme()
2015-05-31 07:46:54 +00:00
} else {
opts.Theme = parseTheme(opts.Theme, spec)
}
2015-03-31 13:05:02 +00:00
case "--toggle-sort":
parseToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required"))
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, 1)
2015-01-01 19:49:30 +00:00
case "+s", "--no-sort":
opts.Sort = 0
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 = optionalNumeric(allArgs, &i, maxMulti)
2015-01-01 19:49:30 +00:00
case "+m", "--no-multi":
opts.Multi = 0
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":
opts.Theme = tui.NoColorTheme()
2015-01-01 19:49:30 +00:00
case "+2", "--no-256":
opts.Theme = tui.Default16
2015-01-01 19:49:30 +00:00
case "--black":
opts.Black = true
case "--no-black":
opts.Black = false
2016-11-15 14:57:32 +00:00
case "--bold":
opts.Bold = true
case "--no-bold":
opts.Bold = false
case "--layout":
opts.Layout = parseLayout(
nextString(allArgs, &i, "layout required (default / reverse / reverse-list)"))
2015-01-01 19:49:30 +00:00
case "--reverse":
opts.Layout = layoutReverse
2015-01-01 19:49:30 +00:00
case "--no-reverse":
opts.Layout = layoutDefault
case "--cycle":
opts.Cycle = true
case "--no-cycle":
opts.Cycle = false
case "--keep-right":
opts.KeepRight = true
case "--no-keep-right":
opts.KeepRight = false
case "--hscroll":
opts.Hscroll = true
case "--no-hscroll":
opts.Hscroll = false
case "--hscroll-off":
opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required")
case "--scroll-off":
opts.ScrollOff = nextInt(allArgs, &i, "scroll offset required")
2017-01-15 10:42:28 +00:00
case "--filepath-word":
opts.FileWord = true
case "--no-filepath-word":
opts.FileWord = false
case "--info":
opts.InfoStyle = parseInfoStyle(
nextString(allArgs, &i, "info style required"))
case "--no-info":
opts.InfoStyle.layout = infoHidden
2015-04-21 14:50:53 +00:00
case "--inline-info":
opts.InfoStyle.layout = infoInline
2015-04-21 14:50:53 +00:00
case "--no-inline-info":
opts.InfoStyle.layout = infoDefault
case "--jump-labels":
opts.JumpLabels = nextString(allArgs, &i, "label characters required")
validateJumpLabels = true
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
case "--read0":
opts.ReadZero = true
case "--no-read0":
opts.ReadZero = false
2016-09-17 19:52:47 +00:00
case "--print0":
opts.Printer = func(str string) { fmt.Print(str, "\x00") }
opts.PrintSep = "\x00"
2016-09-17 19:52:47 +00:00
case "--no-print0":
opts.Printer = func(str string) { fmt.Println(str) }
opts.PrintSep = "\n"
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")
case "--pointer":
opts.Pointer = nextString(allArgs, &i, "pointer sign string required")
validatePointer = true
case "--marker":
opts.Marker = nextString(allArgs, &i, "selected sign string required")
validateMarker = true
2015-02-13 03:25:19 +00:00
case "--sync":
opts.Sync = true
case "--no-sync":
opts.Sync = false
case "--async":
opts.Sync = false
case "--no-history":
opts.History = nil
case "--history":
setHistory(nextString(allArgs, &i, "history file path required"))
case "--history-size":
setHistoryMax(nextInt(allArgs, &i, "history max size required"))
case "--no-header":
2015-07-21 18:21:20 +00:00
opts.Header = []string{}
case "--no-header-lines":
opts.HeaderLines = 0
case "--header":
opts.Header = strLines(nextString(allArgs, &i, "header string required"))
2015-07-21 18:21:20 +00:00
case "--header-lines":
opts.HeaderLines = atoi(
nextString(allArgs, &i, "number of header lines required"))
case "--header-first":
opts.HeaderFirst = true
case "--no-header-first":
opts.HeaderFirst = false
case "--ellipsis":
opts.Ellipsis = nextString(allArgs, &i, "ellipsis string required")
case "--preview":
opts.Preview.command = nextString(allArgs, &i, "preview command required")
case "--no-preview":
opts.Preview.command = ""
case "--preview-window":
parsePreviewWindow(&opts.Preview,
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][,SIZE[%]][,border-BORDER_OPT][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]"))
2017-01-07 16:30:31 +00:00
case "--height":
opts.Height = parseHeight(nextString(allArgs, &i, "height required: [~]HEIGHT[%]"))
case "--min-height":
opts.MinHeight = nextInt(allArgs, &i, "height required: HEIGHT")
2017-01-07 16:30:31 +00:00
case "--no-height":
opts.Height = heightSpec{}
2015-07-26 14:02:04 +00:00
case "--no-margin":
opts.Margin = defaultMargin()
2020-11-09 11:34:08 +00:00
case "--no-padding":
opts.Padding = defaultMargin()
case "--no-border":
opts.BorderShape = tui.BorderNone
case "--border":
hasArg, arg := optionalNextString(allArgs, &i)
opts.BorderShape = parseBorder(arg, !hasArg)
case "--no-border-label":
opts.BorderLabel.label = ""
case "--border-label":
opts.BorderLabel.label = nextString(allArgs, &i, "label required")
case "--border-label-pos":
pos := nextString(allArgs, &i, "label position required (positive or negative integer or 'center')")
parseLabelPosition(&opts.BorderLabel, pos)
case "--no-preview-label":
opts.PreviewLabel.label = ""
case "--preview-label":
opts.PreviewLabel.label = nextString(allArgs, &i, "preview label required")
case "--preview-label-pos":
pos := nextString(allArgs, &i, "preview label position required (positive or negative integer or 'center')")
parseLabelPosition(&opts.PreviewLabel, pos)
case "--no-unicode":
opts.Unicode = false
case "--unicode":
opts.Unicode = true
2015-07-26 14:02:04 +00:00
case "--margin":
opts.Margin = parseMargin(
2020-11-09 11:34:08 +00:00
"margin",
2015-07-26 14:02:04 +00:00
nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
2020-11-09 11:34:08 +00:00
case "--padding":
opts.Padding = parseMargin(
"padding",
nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
case "--tabstop":
opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
2017-03-04 02:29:31 +00:00
case "--clear":
opts.ClearOnExit = true
case "--no-clear":
opts.ClearOnExit = false
2015-01-01 19:49:30 +00:00
case "--version":
opts.Version = true
case "--":
// Ignored
2015-01-01 19:49:30 +00:00
default:
2016-09-07 00:58:18 +00:00
if match, value := optString(arg, "--algo="); match {
opts.FuzzyAlgo = parseAlgo(value)
} else if match, value := optString(arg, "--scheme="); match {
opts.Scheme = strings.ToLower(value)
2016-09-07 00:58:18 +00:00
} else if match, value := optString(arg, "-q", "--query="); match {
2015-01-01 19:49:30 +00:00
opts.Query = value
} else if match, value := optString(arg, "-f", "--filter="); match {
2015-01-01 19:49:30 +00:00
opts.Filter = &value
} 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, "--border="); match {
opts.BorderShape = parseBorder(value, false)
} else if match, value := optString(arg, "--border-label="); match {
opts.BorderLabel.label = value
} else if match, value := optString(arg, "--border-label-pos="); match {
parseLabelPosition(&opts.BorderLabel, value)
} else if match, value := optString(arg, "--preview-label="); match {
opts.PreviewLabel.label = value
} else if match, value := optString(arg, "--preview-label-pos="); match {
parseLabelPosition(&opts.PreviewLabel, value)
2015-01-01 19:49:30 +00:00
} else if match, value := optString(arg, "--prompt="); match {
opts.Prompt = value
} else if match, value := optString(arg, "--pointer="); match {
opts.Pointer = value
validatePointer = true
} else if match, value := optString(arg, "--marker="); match {
opts.Marker = value
validateMarker = true
} 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)
} else if match, _ := optString(arg, "-s", "--sort="); match {
2015-01-01 19:49:30 +00:00
opts.Sort = 1 // Don't care
2019-11-02 11:44:21 +00:00
} else if match, value := optString(arg, "-m", "--multi="); match {
opts.Multi = atoi(value)
2017-01-07 16:30:31 +00:00
} else if match, value := optString(arg, "--height="); match {
opts.Height = parseHeight(value)
} else if match, value := optString(arg, "--min-height="); match {
opts.MinHeight = atoi(value)
} else if match, value := optString(arg, "--layout="); match {
opts.Layout = parseLayout(value)
} else if match, value := optString(arg, "--info="); match {
opts.InfoStyle = parseInfoStyle(value)
2015-03-31 13:05:02 +00:00
} else if match, value := optString(arg, "--toggle-sort="); match {
parseToggleSort(opts.Keymap, value)
} else if match, value := optString(arg, "--expect="); match {
for k, v := range parseKeyChords(value, "key names required") {
opts.Expect[k] = v
}
} else if match, value := optString(arg, "--tiebreak="); match {
opts.Criteria = parseTiebreak(value)
} 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 {
parseKeymap(opts.Keymap, value)
} else if match, value := optString(arg, "--history="); match {
setHistory(value)
} else if match, value := optString(arg, "--history-size="); match {
setHistoryMax(atoi(value))
} else if match, value := optString(arg, "--header="); match {
opts.Header = strLines(value)
2015-07-21 18:21:20 +00:00
} else if match, value := optString(arg, "--header-lines="); match {
opts.HeaderLines = atoi(value)
} else if match, value := optString(arg, "--ellipsis="); match {
opts.Ellipsis = value
} else if match, value := optString(arg, "--preview="); match {
opts.Preview.command = value
} else if match, value := optString(arg, "--preview-window="); match {
parsePreviewWindow(&opts.Preview, value)
2015-07-26 14:02:04 +00:00
} else if match, value := optString(arg, "--margin="); match {
2020-11-09 11:34:08 +00:00
opts.Margin = parseMargin("margin", value)
} else if match, value := optString(arg, "--padding="); match {
opts.Padding = parseMargin("padding", value)
} else if match, value := optString(arg, "--tabstop="); match {
opts.Tabstop = atoi(value)
} else if match, value := optString(arg, "--hscroll-off="); match {
opts.HscrollOff = atoi(value)
} else if match, value := optString(arg, "--scroll-off="); match {
opts.ScrollOff = atoi(value)
} else if match, value := optString(arg, "--jump-labels="); match {
opts.JumpLabels = value
validateJumpLabels = true
2015-01-01 19:49:30 +00:00
} else {
errorExit("unknown option: " + arg)
}
}
}
if opts.HeaderLines < 0 {
errorExit("header lines must be a non-negative integer")
}
if opts.HscrollOff < 0 {
errorExit("hscroll offset must be a non-negative integer")
}
if opts.ScrollOff < 0 {
errorExit("scroll offset must be a non-negative integer")
}
if opts.Tabstop < 1 {
errorExit("tab stop must be a positive integer")
}
if len(opts.JumpLabels) == 0 {
errorExit("empty jump labels")
}
if validateJumpLabels {
for _, r := range opts.JumpLabels {
if r < 32 || r > 126 {
errorExit("non-ascii jump labels are not allowed")
}
}
}
if validatePointer {
if err := validateSign(opts.Pointer, "pointer"); err != nil {
errorExit(err.Error())
}
}
if validateMarker {
if err := validateSign(opts.Marker, "marker"); err != nil {
errorExit(err.Error())
}
}
}
func validateSign(sign string, signOptName string) error {
if sign == "" {
return fmt.Errorf("%v cannot be empty", signOptName)
}
if runewidth.StringWidth(sign) > 2 {
return fmt.Errorf("%v display width should be up to 2", signOptName)
}
return nil
}
func postProcessOptions(opts *Options) {
if !opts.Version && !tui.IsLightRendererSupported() && opts.Height.size > 0 {
errorExit("--height option is currently not supported on this platform")
}
// Default actions for CTRL-N / CTRL-P when --history is set
if opts.History != nil {
if _, prs := opts.Keymap[tui.CtrlP.AsEvent()]; !prs {
opts.Keymap[tui.CtrlP.AsEvent()] = toActions(actPreviousHistory)
}
if _, prs := opts.Keymap[tui.CtrlN.AsEvent()]; !prs {
opts.Keymap[tui.CtrlN.AsEvent()] = toActions(actNextHistory)
}
}
// Extend the default key map
previewEnabled := len(opts.Preview.command) > 0 || hasPreviewAction(opts)
keymap := defaultKeymap()
for key, actions := range opts.Keymap {
var lastChangePreviewWindow *action
for _, act := range actions {
switch act.t {
case actToggleSort:
// To display "+S"/"-S" on info line
opts.ToggleSort = true
case actChangePreviewWindow:
lastChangePreviewWindow = act
if !previewEnabled {
// Doesn't matter
continue
}
opts := previewOpts{}
for _, arg := range strings.Split(act.a, "|") {
// Make sure that each expression is valid
parsePreviewWindow(&opts, arg)
}
}
}
// Re-organize actions so that we only keep the last change-preview-window
// and it comes first in the list.
// * change-preview-window(up,+10)+preview(sleep 3; cat {})+change-preview-window(up,+20)
// -> change-preview-window(up,+20)+preview(sleep 3; cat {})
if lastChangePreviewWindow != nil {
reordered := []*action{lastChangePreviewWindow}
for _, act := range actions {
if act.t != actChangePreviewWindow {
reordered = append(reordered, act)
}
}
actions = reordered
}
keymap[key] = actions
}
opts.Keymap = keymap
if opts.Height.auto {
for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} {
if s.percent {
errorExit("adaptive height is not compatible with top/bottom percent margin")
}
}
for _, s := range []sizeSpec{opts.Padding[0], opts.Padding[2]} {
if s.percent {
errorExit("adaptive height is not compatible with top/bottom percent padding")
}
}
}
// If we're not using extended search mode, --nth option becomes irrelevant
// if it contains the whole range
2015-11-03 13:49:32 +00:00
if !opts.Extended || len(opts.Nth) == 1 {
for _, r := range opts.Nth {
if r.begin == rangeEllipsis && r.end == rangeEllipsis {
opts.Nth = make([]Range, 0)
return
}
}
}
if opts.Bold {
theme := opts.Theme
boldify := func(c tui.ColorAttr) tui.ColorAttr {
dup := c
if !theme.Colored {
dup.Attr |= tui.Bold
} else if (c.Attr & tui.AttrRegular) == 0 {
dup.Attr |= tui.Bold
}
return dup
}
theme.Current = boldify(theme.Current)
theme.CurrentMatch = boldify(theme.CurrentMatch)
theme.Prompt = boldify(theme.Prompt)
theme.Input = boldify(theme.Input)
theme.Cursor = boldify(theme.Cursor)
theme.Spinner = boldify(theme.Spinner)
}
if opts.Scheme != "default" {
processScheme(opts)
}
2015-01-01 19:49:30 +00:00
}
func expectsArbitraryString(opt string) bool {
switch opt {
case "-q", "--query", "-f", "--filter", "--header", "--prompt":
return true
}
return false
}
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
for idx, arg := range os.Args[1:] {
if arg == "--version" && (idx == 0 || idx > 0 && !expectsArbitraryString(os.Args[idx])) {
opts.Version = true
return opts
}
}
2015-01-01 19:49:30 +00:00
// Options from Env var
words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS"))
if len(words) > 0 {
parseOptions(opts, words)
}
2015-01-01 19:49:30 +00:00
// Options from command-line arguments
parseOptions(opts, os.Args[1:])
postProcessOptions(opts)
2015-01-01 19:49:30 +00:00
return opts
}