m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-15 14:53:47 -05:00
Files
fzf/src/options.go
2025-09-29 18:42:50 +09:00

3790 lines
102 KiB
Go

package fzf
import (
"errors"
"fmt"
"maps"
"os"
"regexp"
"strconv"
"strings"
"unicode"
"github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
"github.com/junegunn/go-shellwords"
"github.com/rivo/uniseg"
)
const Usage = `fzf is an interactive filter program for any kind of list.
It implements a "fuzzy" matching algorithm, so you can quickly type in patterns
with omitted characters and still get the results you want.
Project URL: https://github.com/junegunn/fzf
Author: Junegunn Choi <junegunn.c@gmail.com>
* See man page for more information: fzf --man
Usage: fzf [options]
SEARCH
-e, --exact Enable exact-match
+x, --no-extended Disable extended-search mode
-i, --ignore-case Case-insensitive match
+i, --no-ignore-case Case-sensitive match
--smart-case Smart-case match (default)
--scheme=SCHEME Scoring scheme [default|path|history]
-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
--accept-nth=N[,..] Define which fields to print on accept
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
+s, --no-sort Do not sort the result
--literal Do not normalize latin script letters
--tail=NUM Maximum number of items to keep in memory
--disabled Do not perform search
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
when the scores are tied
[length|chunk|pathname|begin|end|index] (default: length)
INPUT/OUTPUT
--read0 Read input delimited by ASCII NUL characters
--print0 Print output delimited by ASCII NUL characters
--ansi Enable processing of ANSI color codes
--sync Synchronous search for multi-staged filtering
GLOBAL STYLE
--style=PRESET Apply a style preset [default|minimal|full[:BORDER_STYLE]
--color=COLSPEC Base scheme (dark|light|base16|bw) and/or custom colors
--no-color Disable colors
--no-bold Do not use bold text
DISPLAY MODE
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given
height instead of using fullscreen.
A negative value is calculated as the terminal height
minus the given value.
If prefixed with '~', fzf will determine the height
according to the input size.
--min-height=HEIGHT[+] Minimum height when --height is given as a percentage.
Add '+' to automatically increase the value
according to the other layout options (default: 10+).
--tmux[=OPTS] Start fzf in a tmux popup (requires tmux 3.3+)
[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
[,border-native] (default: center,50%)
LAYOUT
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
--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)
--border[=STYLE] Draw border around the finder
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|line|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)
LIST SECTION
-m, --multi[=MAX] Enable multi-select with tab/shift-tab
--highlight-line Highlight the whole current line
--cycle Enable cyclic scroll
--wrap Enable line wrap
--wrap-sign=STR Indicator for wrapped lines
--no-multi-line Disable multi-line display of items when using --read0
--raw Enable raw mode (show non-matching items)
--track Track the current selection when the result is updated
--tac Reverse the order of the input
--gap[=N] Render empty lines between each item
--gap-line[=STR] Draw horizontal line on each gap using the string
(default: '┈' or '-')
--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)
--jump-labels=CHARS Label characters for jump mode
--gutter=CHAR Character used for the gutter column (default: '▌')
--gutter-raw=CHAR Character used for the gutter column in raw mode (default: '▖')
--pointer=STR Pointer to the current line (default: '▌' or '>')
--marker=STR Multi-select marker (default: '┃' or '>')
--marker-multi-line=STR Multi-select marker for multi-line entries;
3 elements for top, middle, and bottom (default: '╻┃╹')
--ellipsis=STR Ellipsis to show when line is truncated (default: '··')
--tabstop=SPACES Number of spaces for a tab character (default: 8)
--scrollbar[=C1[C2]] Scrollbar character(s)
(each for list section and preview window)
--no-scrollbar Hide scrollbar
--list-border[=STYLE] Draw border around the list section
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|none] (default: rounded)
--list-label=LABEL Label to print on the list border
--list-label-pos=COL Position of the list label
[POSITIVE_INTEGER: columns from left|
NEGATIVE_INTEGER: columns from right][:bottom]
(default: 0 or center)
INPUT SECTION
--no-input Disable and hide the input section
--prompt=STR Input prompt (default: '> ')
--info=STYLE Finder info style
[default|right|hidden|inline[-right][:PREFIX]]
--info-command=COMMAND Command to generate info line
--separator=STR Draw horizontal separator on info line using the string
(default: '─' or '-')
--no-separator Hide info line separator
--ghost=TEXT Ghost text to display when the input is empty
--filepath-word Make word-wise movements respect path separators
--input-border[=STYLE] Draw border around the input section
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|line|none] (default: rounded)
--input-label=LABEL Label to print on the input border
--input-label-pos=COL Position of the input label
[POSITIVE_INTEGER: columns from left|
NEGATIVE_INTEGER: columns from right][:bottom]
(default: 0 or center)
PREVIEW WINDOW
--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]info]
[,[no]hidden][,border-STYLE]
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
--preview-border[=STYLE] Short for --preview-window=border-STYLE
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|line|none] (default: rounded)
--preview-label=LABEL
--preview-label-pos=N Same as --border-label and --border-label-pos,
but for preview window
HEADER
--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
--header-border[=STYLE] Draw border around the header section
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|line|none] (default: rounded)
--header-lines-border[=STYLE]
Display header from --header-lines with a separate border.
Pass 'none' to still separate it but without a border.
--header-label=LABEL Label to print on the header border
--header-label-pos=COL Position of the header label
[POSITIVE_INTEGER: columns from left|
NEGATIVE_INTEGER: columns from right][:bottom]
(default: 0 or center)
FOOTER
--footer=STR String to print as footer
--footer-border[=STYLE] Draw border around the footer section
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|line|none] (default: line)
--footer-label=LABEL Label to print on the footer border
--footer-label-pos=COL Position of the footer label
[POSITIVE_INTEGER: columns from left|
NEGATIVE_INTEGER: columns from right][:bottom]
(default: 0 or center)
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 Print matches for the initial query and exit
--print-query Print query as the first line
--expect=KEYS Comma-separated list of keys to complete fzf
KEY/EVENT BINDING
--bind=BINDINGS Custom key/event bindings
ADVANCED
--with-shell=STR Shell command and flags to start child processes with
--listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
(To allow remote process execution, use --listen-unsafe)
DIRECTORY TRAVERSAL (Only used when $FZF_DEFAULT_COMMAND is not set)
--walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden)
--walker-root=DIR [...] List of directories to walk (default: .)
--walker-skip=DIRS Comma-separated list of directory names to skip
(default: .git,node_modules)
HISTORY
--history=FILE File to store fzf search history (*not* shell command history)
--history-size=N Maximum number of entries to keep in the file (default: 1000)
SHELL INTEGRATION
--bash Print script to set up Bash shell integration
--zsh Print script to set up Zsh shell integration
--fish Print script to set up Fish shell integration
HELP
--version Display version information and exit
--help Show this message
--man Show man page
ENVIRONMENT VARIABLES
FZF_DEFAULT_COMMAND Default command to use when input is tty
FZF_DEFAULT_OPTS Default options (e.g. '--layout=reverse --info=inline')
FZF_DEFAULT_OPTS_FILE Location of the file to read default options from
FZF_API_KEY X-API-Key header for HTTP server (--listen)
`
// Can be changed by --style
var defaultBorderShape tui.BorderShape = tui.DefaultBorderShape
const defaultInfoPrefix = " < "
// Case denotes case-sensitivity of search
type Case int
// Case-sensitivities
const (
CaseSmart Case = iota
CaseIgnore
CaseRespect
)
// Sort criteria
type criterion int
const (
byScore criterion = iota
byChunk
byLength
byBegin
byEnd
byPathname
)
type heightSpec struct {
size float64
percent bool
auto bool
inverse bool
index int
}
type sizeSpec struct {
size float64
percent bool
}
func (s sizeSpec) String() string {
if s.percent {
return fmt.Sprintf("%d%%", int(s.size))
}
return fmt.Sprintf("%d", int(s.size))
}
func defaultMargin() [4]sizeSpec {
return [4]sizeSpec{}
}
type trackOption int
const (
trackDisabled trackOption = iota
trackEnabled
trackCurrent
)
type windowPosition int
const (
posUp windowPosition = iota
posDown
posLeft
posRight
posCenter
)
type tmuxOptions struct {
width sizeSpec
height sizeSpec
position windowPosition
index int
border bool
}
type layoutType int
const (
layoutDefault layoutType = iota
layoutReverse
layoutReverseList
)
type infoStyle int
const (
infoDefault infoStyle = iota
infoRight
infoInline
infoInlineRight
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
info bool
border tui.BorderShape
headerLines int
threshold int
alternative *previewOpts
}
func (o *previewOpts) Visible() bool {
return o.size.size > 0 || o.alternative != nil && o.alternative.size.size > 0
}
func (o *previewOpts) Toggle() {
o.hidden = !o.hidden
}
func (o *previewOpts) Border() tui.BorderShape {
shape := o.border
if shape == tui.BorderLine {
switch o.position {
case posUp:
shape = tui.BorderBottom
case posDown:
shape = tui.BorderTop
case posLeft:
shape = tui.BorderRight
case posRight:
shape = tui.BorderLeft
}
}
return shape
}
func defaultTmuxOptions(index int) *tmuxOptions {
return &tmuxOptions{
position: posCenter,
width: sizeSpec{50, true},
height: sizeSpec{50, true},
index: index}
}
func parseTmuxOptions(arg string, index int) (*tmuxOptions, error) {
var err error
opts := defaultTmuxOptions(index)
tokens := splitRegexp.Split(arg, -1)
errorToReturn := errors.New("invalid tmux option: " + arg + " (expected: [center|top|bottom|left|right][,SIZE[%]][,SIZE[%][,border-native]])")
if len(tokens) == 0 || len(tokens) > 4 {
return nil, errorToReturn
}
for i, token := range tokens {
if token == "border-native" {
tokens = append(tokens[:i], tokens[i+1:]...) // cut the 'border-native' option
opts.border = true
break
}
}
// Defaults to 'center'
first := "center"
if len(tokens) > 0 {
first = tokens[0]
}
switch first {
case "top", "up":
opts.position = posUp
opts.width = sizeSpec{100, true}
case "bottom", "down":
opts.position = posDown
opts.width = sizeSpec{100, true}
case "left":
opts.position = posLeft
opts.height = sizeSpec{100, true}
case "right":
opts.position = posRight
opts.height = sizeSpec{100, true}
case "center":
default:
tokens = append([]string{"center"}, tokens...)
}
// One size given
var size1 sizeSpec
if len(tokens) > 1 {
if size1, err = parseSize(tokens[1], 100, "size"); err != nil {
return nil, errorToReturn
}
}
// Two sizes given
var size2 sizeSpec
if len(tokens) == 3 {
if size2, err = parseSize(tokens[2], 100, "size"); err != nil {
return nil, errorToReturn
}
opts.width = size1
opts.height = size2
} else if len(tokens) == 2 {
switch tokens[0] {
case "top", "up":
opts.height = size1
case "bottom", "down":
opts.height = size1
case "left":
opts.width = size1
case "right":
opts.width = size1
case "center":
opts.width = size1
opts.height = size1
}
}
return opts, nil
}
func parseLabelPosition(opts *labelOpts, arg string) error {
opts.column = 0
opts.bottom = false
var err error
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, err = atoi(token)
}
}
return err
}
func (a previewOpts) aboveOrBelow() bool {
return a.size.size > 0 && (a.position == posUp || a.position == posDown)
}
type previewOptsCompare int
const (
previewOptsSame previewOptsCompare = iota
previewOptsDifferentContentLayout
previewOptsDifferentLayout
)
func (o *previewOpts) compare(active *previewOpts, b *previewOpts) previewOptsCompare {
a := o
sameThreshold := o.position == b.position && o.threshold == b.threshold
// Alternative layout is being used
if o.alternative == active {
a = active
// If the other also has an alternative layout,
if b.alternative != nil {
// and if the same condition is the same, compare alt vs. alt.
if sameThreshold {
b = b.alternative
} else {
// If not, we pessimistically decide that the layouts may not be the same
return previewOptsDifferentLayout
}
}
} else if b.alternative != nil && !sameThreshold {
// We may choose the other's alternative layout, so let's be conservative.
return previewOptsDifferentLayout
}
if !(a.size == b.size && a.position == b.position && a.border == b.border && a.hidden == b.hidden) {
return previewOptsDifferentLayout
}
if a.wrap == b.wrap && a.headerLines == b.headerLines && a.info == b.info && a.scroll == b.scroll {
return previewOptsSame
}
return previewOptsDifferentContentLayout
}
func firstLine(s string) string {
return strings.SplitN(s, "\n", 2)[0]
}
type walkerOpts struct {
file bool
dir bool
hidden bool
follow bool
}
// Options stores the values of command-line options
type Options struct {
Input chan string
Output chan string
NoWinpty bool
Tmux *tmuxOptions
ForceTtyIn bool
ProxyScript string
Bash bool
Zsh bool
Fish bool
Man bool
Fuzzy bool
FuzzyAlgo algo.Algo
Scheme string
Extended bool
Phony bool
Inputless bool
Case Case
Normalize bool
Nth []Range
WithNth func(Delimiter) func([]Token, int32) string
AcceptNth func(Delimiter) func([]Token, int32) string
Delimiter Delimiter
Sort int
Raw bool
Track trackOption
Tac bool
Tail int
Criteria []criterion
Multi int
Ansi bool
Mouse bool
Theme *tui.ColorTheme
Black bool
Bold bool
Height heightSpec
MinHeight int
Layout layoutType
Cycle bool
Wrap bool
WrapSign *string
MultiLine bool
CursorLine bool
KeepRight bool
Hscroll bool
HscrollOff int
ScrollOff int
FileWord bool
InfoStyle infoStyle
InfoPrefix string
InfoCommand string
Ghost string
Separator *string
JumpLabels string
Prompt string
Gutter *string
GutterRaw *string
Pointer *string
Marker *string
MarkerMulti *[3]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
Footer []string
Gap int
GapLine *string
Ellipsis *string
Scrollbar *string
Margin [4]sizeSpec
Padding [4]sizeSpec
BorderShape tui.BorderShape
ListBorderShape tui.BorderShape
InputBorderShape tui.BorderShape
HeaderBorderShape tui.BorderShape
HeaderLinesShape tui.BorderShape
FooterBorderShape tui.BorderShape
InputLabel labelOpts
HeaderLabel labelOpts
FooterLabel labelOpts
BorderLabel labelOpts
ListLabel labelOpts
PreviewLabel labelOpts
Unicode bool
Ambidouble bool
Tabstop int
WithShell string
ListenAddr *listenAddress
Unsafe bool
ClearOnExit bool
WalkerOpts walkerOpts
WalkerRoot []string
WalkerSkip []string
Version bool
Help bool
CPUProfile string
MEMProfile string
BlockProfile string
MutexProfile string
TtyDefault string
}
func filterNonEmpty(input []string) []string {
output := make([]string, 0, len(input))
for _, str := range input {
if len(str) > 0 {
output = append(output, str)
}
}
return output
}
func defaultPreviewOpts(command string) previewOpts {
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, true, defaultBorderShape, 0, 0, nil}
}
func defaultOptions() *Options {
var theme *tui.ColorTheme
if os.Getenv("NO_COLOR") != "" {
theme = tui.NoColorTheme()
} else {
theme = tui.EmptyTheme()
}
return &Options{
Bash: false,
Zsh: false,
Fish: false,
Man: false,
Fuzzy: true,
FuzzyAlgo: algo.FuzzyMatchV2,
Scheme: "", // Unknown
Extended: true,
Phony: false,
Inputless: false,
Case: CaseSmart,
Normalize: true,
Nth: make([]Range, 0),
Delimiter: Delimiter{},
Sort: 1000,
Track: trackDisabled,
Tac: false,
Criteria: []criterion{}, // Unknown
Multi: 0,
Ansi: false,
Mouse: true,
Theme: theme,
Black: false,
Bold: true,
MinHeight: -10,
Layout: layoutDefault,
Cycle: false,
Wrap: false,
MultiLine: true,
KeepRight: false,
Hscroll: true,
HscrollOff: 10,
ScrollOff: 3,
FileWord: false,
InfoStyle: infoDefault,
Ghost: "",
Separator: nil,
JumpLabels: defaultJumpLabels,
Prompt: "> ",
Gutter: nil,
GutterRaw: nil,
Pointer: nil,
Marker: nil,
MarkerMulti: nil,
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,
Footer: make([]string, 0),
Gap: 0,
Ellipsis: nil,
Scrollbar: nil,
Margin: defaultMargin(),
Padding: defaultMargin(),
Unicode: true,
Ambidouble: os.Getenv("RUNEWIDTH_EASTASIAN") == "1",
Tabstop: 8,
BorderLabel: labelOpts{},
PreviewLabel: labelOpts{},
Unsafe: false,
ClearOnExit: true,
WalkerOpts: walkerOpts{file: true, hidden: true, follow: true},
WalkerRoot: []string{"."},
WalkerSkip: []string{".git", "node_modules"},
TtyDefault: tui.DefaultTtyDevice,
Help: false,
Version: false}
}
func isDir(path string) bool {
stat, err := os.Stat(path)
return err == nil && stat.IsDir()
}
func atoi(str string) (int, error) {
num, err := strconv.Atoi(str)
if err != nil {
return 0, errors.New("not a valid integer: " + str)
}
return num, nil
}
func atof(str string) (float64, error) {
num, err := strconv.ParseFloat(str, 64)
if err != nil {
return 0, errors.New("not a valid number: " + str)
}
return num, nil
}
func splitNth(str string) ([]Range, error) {
if match, _ := regexp.MatchString("^[0-9,-.]+$", str); !match {
return nil, errors.New("invalid format: " + str)
}
tokens := strings.Split(str, ",")
ranges := make([]Range, len(tokens))
for idx, s := range tokens {
r, ok := ParseRange(&s)
if !ok {
return nil, errors.New("invalid format: " + str)
}
ranges[idx] = r
}
return ranges, nil
}
func nthTransformer(str string) (func(Delimiter) func([]Token, int32) string, error) {
// ^[0-9,-.]+$"
if match, _ := regexp.MatchString("^[0-9,-.]+$", str); match {
nth, err := splitNth(str)
if err != nil {
return nil, err
}
return func(Delimiter) func([]Token, int32) string {
return func(tokens []Token, index int32) string {
return JoinTokens(Transform(tokens, nth))
}
}, nil
}
// {...} {...} ...
placeholder := regexp.MustCompile("{[0-9,-.]+}|{n}")
indexes := placeholder.FindAllStringIndex(str, -1)
if indexes == nil {
return nil, errors.New("template should include at least 1 placeholder: " + str)
}
type NthParts struct {
str string
index bool
nth []Range
}
parts := make([]NthParts, len(indexes))
idx := 0
for _, index := range indexes {
if idx < index[0] {
parts = append(parts, NthParts{str: str[idx:index[0]]})
}
expr := str[index[0]+1 : index[1]-1]
if expr == "n" {
parts = append(parts, NthParts{index: true})
} else if nth, err := splitNth(expr); err == nil {
parts = append(parts, NthParts{nth: nth})
}
idx = index[1]
}
if idx < len(str) {
parts = append(parts, NthParts{str: str[idx:]})
}
return func(delimiter Delimiter) func([]Token, int32) string {
return func(tokens []Token, index int32) string {
str := ""
for _, holder := range parts {
if holder.nth != nil {
str += StripLastDelimiter(JoinTokens(Transform(tokens, holder.nth)), delimiter)
} else if holder.index {
if index >= 0 {
str += strconv.Itoa(int(index))
}
} else {
str += holder.str
}
}
return str
}
}, nil
}
func delimiterRegexp(str string) Delimiter {
// Special handling of \t
str = strings.ReplaceAll(str, "\\t", "\t")
// 1. Pattern is a single character
if len([]rune(str)) == 1 {
return Delimiter{str: &str}
}
// 2. Pattern does not contain any special character
if regexp.QuoteMeta(str) == str {
return Delimiter{str: &str}
}
rx, e := regexp.Compile(str)
// 3. Pattern is not a valid regular expression
if e != nil {
return Delimiter{str: &str}
}
// 4. Pattern as regular expression. Slow.
return Delimiter{regex: rx}
}
func isAlphabet(char uint8) bool {
return char >= 'a' && char <= 'z'
}
func isNumeric(char uint8) bool {
return char >= '0' && char <= '9'
}
func parseAlgo(str string) (algo.Algo, error) {
switch str {
case "v1":
return algo.FuzzyMatchV1, nil
case "v2":
return algo.FuzzyMatchV2, nil
}
return nil, errors.New("invalid algorithm (expected: v1 or v2)")
}
func parseBorder(str string, optional bool) (tui.BorderShape, error) {
switch str {
case "line":
return tui.BorderLine, nil
case "rounded":
return tui.BorderRounded, nil
case "sharp":
return tui.BorderSharp, nil
case "bold":
return tui.BorderBold, nil
case "block":
return tui.BorderBlock, nil
case "thinblock":
return tui.BorderThinBlock, nil
case "double":
return tui.BorderDouble, nil
case "horizontal":
return tui.BorderHorizontal, nil
case "vertical":
return tui.BorderVertical, nil
case "top":
return tui.BorderTop, nil
case "bottom":
return tui.BorderBottom, nil
case "left":
return tui.BorderLeft, nil
case "right":
return tui.BorderRight, nil
case "none":
return tui.BorderNone, nil
}
if optional && str == "" {
return defaultBorderShape, nil
}
return tui.BorderNone, errors.New("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|none)")
}
func parseKeyChords(str string, message string) (map[tui.Event]string, []tui.Event, error) {
if len(str) == 0 {
return nil, nil, errors.New(message)
}
list := []tui.Event{}
str = regexp.MustCompile("(?i)(alt-),").ReplaceAllString(str, "$1"+string([]rune{escapedComma}))
tokens := strings.Split(str, ",")
if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Contains(str, ",,,") {
tokens = append(tokens, ",")
}
chords := make(map[tui.Event]string)
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
list = append(list, e.AsEvent())
}
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.Enter)
case "space":
evt := tui.Key(' ')
chords[evt] = key
list = append(list, evt)
case "backspace", "bspace", "bs":
add(tui.Backspace)
case "ctrl-space":
add(tui.CtrlSpace)
case "ctrl-^", "ctrl-6":
add(tui.CtrlCaret)
case "ctrl-/", "ctrl-_":
add(tui.CtrlSlash)
case "ctrl-\\":
add(tui.CtrlBackSlash)
case "ctrl-]":
add(tui.CtrlRightBracket)
case "change":
add(tui.Change)
case "backward-eof":
add(tui.BackwardEOF)
case "start":
add(tui.Start)
case "load":
add(tui.Load)
case "focus":
add(tui.Focus)
case "result":
add(tui.Result)
case "resize":
add(tui.Resize)
case "one":
add(tui.One)
case "zero":
add(tui.Zero)
case "jump":
add(tui.Jump)
case "jump-cancel":
add(tui.JumpCancel)
case "click-header":
add(tui.ClickHeader)
case "click-footer":
add(tui.ClickFooter)
case "multi":
add(tui.Multi)
case "alt-enter", "alt-return":
evt := tui.CtrlAltKey('m')
chords[evt] = key
list = append(list, evt)
case "alt-space":
evt := tui.AltKey(' ')
chords[evt] = key
list = append(list, evt)
case "alt-bs", "alt-bspace", "alt-backspace":
add(tui.AltBackspace)
case "ctrl-bs", "ctrl-bspace", "ctrl-backspace":
add(tui.CtrlBackspace)
case "ctrl-alt-bs", "ctrl-alt-bspace", "ctrl-alt-backspace":
add(tui.CtrlAltBackspace)
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 "alt-home":
add(tui.AltHome)
case "alt-end":
add(tui.AltEnd)
case "alt-delete":
add(tui.AltDelete)
case "alt-page-up":
add(tui.AltPageUp)
case "alt-page-down":
add(tui.AltPageDown)
case "tab":
add(tui.Tab)
case "btab", "shift-tab":
add(tui.ShiftTab)
case "esc":
add(tui.Esc)
case "delete", "del":
add(tui.Delete)
case "home":
add(tui.Home)
case "end":
add(tui.End)
case "insert":
add(tui.Insert)
case "pgup", "page-up":
add(tui.PageUp)
case "pgdn", "page-down":
add(tui.PageDown)
case "alt-shift-up", "shift-alt-up":
add(tui.AltShiftUp)
case "alt-shift-down", "shift-alt-down":
add(tui.AltShiftDown)
case "alt-shift-left", "shift-alt-left":
add(tui.AltShiftLeft)
case "alt-shift-right", "shift-alt-right":
add(tui.AltShiftRight)
case "alt-shift-home", "shift-alt-home":
add(tui.AltShiftHome)
case "alt-shift-end", "shift-alt-end":
add(tui.AltShiftEnd)
case "alt-shift-delete", "shift-alt-delete":
add(tui.AltShiftDelete)
case "alt-shift-page-up", "shift-alt-page-up":
add(tui.AltShiftPageUp)
case "alt-shift-page-down", "shift-alt-page-down":
add(tui.AltShiftPageDown)
case "ctrl-up":
add(tui.CtrlUp)
case "ctrl-down":
add(tui.CtrlDown)
case "ctrl-right":
add(tui.CtrlRight)
case "ctrl-left":
add(tui.CtrlLeft)
case "ctrl-home":
add(tui.CtrlHome)
case "ctrl-end":
add(tui.CtrlEnd)
case "ctrl-delete":
add(tui.CtrlDelete)
case "ctrl-page-up":
add(tui.CtrlPageUp)
case "ctrl-page-down":
add(tui.CtrlPageDown)
case "ctrl-alt-up", "alt-ctrl-up":
add(tui.CtrlAltUp)
case "ctrl-alt-down", "alt-ctrl-down":
add(tui.CtrlAltDown)
case "ctrl-alt-right", "alt-ctrl-right":
add(tui.CtrlAltRight)
case "ctrl-alt-left", "alt-ctrl-left":
add(tui.CtrlAltLeft)
case "ctrl-alt-home", "alt-ctrl-home":
add(tui.CtrlAltHome)
case "ctrl-alt-end", "alt-ctrl-end":
add(tui.CtrlAltEnd)
case "ctrl-alt-delete", "alt-ctrl-delete":
add(tui.CtrlAltDelete)
case "ctrl-alt-page-up", "alt-ctrl-page-up":
add(tui.CtrlAltPageUp)
case "ctrl-alt-page-down", "alt-ctrl-page-down":
add(tui.CtrlAltPageDown)
case "ctrl-shift-up", "shift-ctrl-up":
add(tui.CtrlShiftUp)
case "ctrl-shift-down", "shift-ctrl-down":
add(tui.CtrlShiftDown)
case "ctrl-shift-right", "shift-ctrl-right":
add(tui.CtrlShiftRight)
case "ctrl-shift-left", "shift-ctrl-left":
add(tui.CtrlShiftLeft)
case "ctrl-shift-home", "shift-ctrl-home":
add(tui.CtrlShiftHome)
case "ctrl-shift-end", "shift-ctrl-end":
add(tui.CtrlShiftEnd)
case "ctrl-shift-delete", "shift-ctrl-delete":
add(tui.CtrlShiftDelete)
case "ctrl-shift-page-up", "shift-ctrl-page-up":
add(tui.CtrlShiftPageUp)
case "ctrl-shift-page-down", "shift-ctrl-page-down":
add(tui.CtrlShiftPageDown)
case "ctrl-alt-shift-up":
add(tui.CtrlAltShiftUp)
case "ctrl-alt-shift-down":
add(tui.CtrlAltShiftDown)
case "ctrl-alt-shift-right":
add(tui.CtrlAltShiftRight)
case "ctrl-alt-shift-left":
add(tui.CtrlAltShiftLeft)
case "ctrl-alt-shift-home":
add(tui.CtrlAltShiftHome)
case "ctrl-alt-shift-end":
add(tui.CtrlAltShiftEnd)
case "ctrl-alt-shift-delete":
add(tui.CtrlAltShiftDelete)
case "ctrl-alt-shift-page-up":
add(tui.CtrlAltShiftPageUp)
case "ctrl-alt-shift-page-down":
add(tui.CtrlAltShiftPageDown)
case "shift-up":
add(tui.ShiftUp)
case "shift-down":
add(tui.ShiftDown)
case "shift-left":
add(tui.ShiftLeft)
case "shift-right":
add(tui.ShiftRight)
case "shift-home":
add(tui.ShiftHome)
case "shift-end":
add(tui.ShiftEnd)
case "shift-delete":
add(tui.ShiftDelete)
case "shift-page-up":
add(tui.ShiftPageUp)
case "shift-page-down":
add(tui.ShiftPageDown)
case "left-click":
add(tui.LeftClick)
case "right-click":
add(tui.RightClick)
case "shift-left-click":
add(tui.SLeftClick)
case "shift-right-click":
add(tui.SRightClick)
case "double-click":
add(tui.DoubleClick)
case "scroll-up":
add(tui.ScrollUp)
case "scroll-down":
add(tui.ScrollDown)
case "shift-scroll-up":
add(tui.SScrollUp)
case "shift-scroll-down":
add(tui.SScrollDown)
case "preview-scroll-up":
add(tui.PreviewScrollUp)
case "preview-scroll-down":
add(tui.PreviewScrollDown)
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]) {
evt := tui.CtrlAltKey(rune(key[9]))
chords[evt] = key
list = append(list, evt)
} 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 = '+'
}
evt := tui.AltKey(r)
chords[evt] = key
list = append(list, evt)
} 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 {
evt := tui.Key(runes[0])
chords[evt] = key
list = append(list, evt)
} else {
return nil, list, errors.New("unsupported key: " + key)
}
}
}
return chords, list, nil
}
func parseScheme(str string) (string, []criterion, error) {
str = strings.ToLower(str)
switch str {
case "history":
return str, []criterion{byScore}, nil
case "path":
return str, []criterion{byScore, byPathname, byLength}, nil
case "default":
return str, []criterion{byScore, byLength}, nil
}
return str, nil, errors.New("invalid scoring scheme: " + str + " (expected: default|path|history)")
}
func parseTiebreak(str string) ([]criterion, error) {
criteria := []criterion{byScore}
hasIndex := false
hasChunk := false
hasLength := false
hasBegin := false
hasEnd := false
hasPathname := false
check := func(notExpected *bool, name string) error {
if *notExpected {
return errors.New("duplicate sort criteria: " + name)
}
if hasIndex {
return errors.New("index should be the last criterion")
}
*notExpected = true
return nil
}
for _, str := range strings.Split(strings.ToLower(str), ",") {
switch str {
case "index":
if err := check(&hasIndex, "index"); err != nil {
return nil, err
}
case "chunk":
if err := check(&hasChunk, "chunk"); err != nil {
return nil, err
}
criteria = append(criteria, byChunk)
case "pathname":
if err := check(&hasPathname, "pathname"); err != nil {
return nil, err
}
criteria = append(criteria, byPathname)
case "length":
if err := check(&hasLength, "length"); err != nil {
return nil, err
}
criteria = append(criteria, byLength)
case "begin":
if err := check(&hasBegin, "begin"); err != nil {
return nil, err
}
criteria = append(criteria, byBegin)
case "end":
if err := check(&hasEnd, "end"); err != nil {
return nil, err
}
criteria = append(criteria, byEnd)
default:
return nil, errors.New("invalid sort criterion: " + str)
}
}
if len(criteria) > 4 {
return nil, errors.New("at most 3 tiebreaks are allowed: " + str)
}
return criteria, nil
}
func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme {
dupe := *theme
return &dupe
}
func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, error) {
var err error
theme := dupeTheme(defaultTheme)
rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$")
comma := regexp.MustCompile(`[\s,]+`)
for _, str := range comma.Split(strings.ToLower(str), -1) {
str = strings.TrimSpace(str)
if len(str) == 0 {
continue
}
switch str {
case "dark":
theme = dupeTheme(tui.Dark256)
case "light":
theme = dupeTheme(tui.Light256)
case "base16", "16":
theme = dupeTheme(tui.Default16)
case "bw", "no":
theme = tui.NoColorTheme()
default:
fail := func() {
// Let the code proceed to simplify the error handling
err = errors.New("invalid color specification: " + str)
}
// Color is disabled
if theme == nil {
continue
}
components := strings.Split(str, ":")
if len(components) < 2 {
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)
}
}
}
}
switch components[0] {
case "query", "input", "input-fg":
mergeAttr(&theme.Input)
case "ghost":
mergeAttr(&theme.Ghost)
case "disabled":
mergeAttr(&theme.Disabled)
case "fg":
mergeAttr(&theme.Fg)
case "bg":
mergeAttr(&theme.Bg)
case "list-fg":
mergeAttr(&theme.ListFg)
case "list-bg":
mergeAttr(&theme.ListBg)
case "preview-fg":
mergeAttr(&theme.PreviewFg)
case "preview-bg":
mergeAttr(&theme.PreviewBg)
case "current-fg", "fg+":
mergeAttr(&theme.Current)
case "current-bg", "bg+":
mergeAttr(&theme.DarkBg)
case "alt-bg":
mergeAttr(&theme.AltBg)
case "selected-fg":
mergeAttr(&theme.SelectedFg)
case "selected-bg":
mergeAttr(&theme.SelectedBg)
case "nth":
mergeAttr(&theme.Nth)
case "hidden":
mergeAttr(&theme.Hidden)
case "gutter":
mergeAttr(&theme.Gutter)
case "hl":
mergeAttr(&theme.Match)
case "current-hl", "hl+":
mergeAttr(&theme.CurrentMatch)
case "selected-hl":
mergeAttr(&theme.SelectedMatch)
case "border":
mergeAttr(&theme.Border)
case "preview-border":
mergeAttr(&theme.PreviewBorder)
case "separator":
mergeAttr(&theme.Separator)
case "scrollbar":
mergeAttr(&theme.Scrollbar)
case "preview-scrollbar":
mergeAttr(&theme.PreviewScrollbar)
case "label":
mergeAttr(&theme.BorderLabel)
case "list-label":
mergeAttr(&theme.ListLabel)
case "list-border":
mergeAttr(&theme.ListBorder)
case "preview-label":
mergeAttr(&theme.PreviewLabel)
case "prompt":
mergeAttr(&theme.Prompt)
case "input-bg":
mergeAttr(&theme.InputBg)
case "input-border":
mergeAttr(&theme.InputBorder)
case "input-label":
mergeAttr(&theme.InputLabel)
case "header-border":
mergeAttr(&theme.HeaderBorder)
case "header-label":
mergeAttr(&theme.HeaderLabel)
case "footer-border":
mergeAttr(&theme.FooterBorder)
case "footer-label":
mergeAttr(&theme.FooterLabel)
case "spinner":
mergeAttr(&theme.Spinner)
case "info":
mergeAttr(&theme.Info)
case "pointer":
mergeAttr(&theme.Cursor)
case "marker":
mergeAttr(&theme.Marker)
case "header", "header-fg":
mergeAttr(&theme.Header)
case "header-bg":
mergeAttr(&theme.HeaderBg)
case "footer", "footer-fg":
mergeAttr(&theme.Footer)
case "footer-bg":
mergeAttr(&theme.FooterBg)
case "gap-line":
mergeAttr(&theme.GapLine)
default:
fail()
}
}
}
return theme, err
}
func parseWalkerOpts(str string) (walkerOpts, error) {
opts := walkerOpts{}
for _, str := range strings.Split(strings.ToLower(str), ",") {
switch str {
case "file":
opts.file = true
case "dir":
opts.dir = true
case "hidden":
opts.hidden = true
case "follow":
opts.follow = true
case "":
// Ignored
default:
return opts, errors.New("invalid walker option: " + str)
}
}
if !opts.file && !opts.dir {
return opts, errors.New("at least one of 'file' or 'dir' should be specified")
}
return opts, nil
}
var (
executeRegexp *regexp.Regexp
splitRegexp *regexp.Regexp
actionNameRegexp *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
)
func init() {
executeRegexp = regexp.MustCompile(
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|bg-transform|transform)-(?:query|prompt|(?:border|list|preview|input|header|footer)-label|header|footer|search|nth|pointer|ghost)|bg-transform|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search|trigger)`)
splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
}
func maskActionContents(action string) string {
masked := ""
Loop:
for len(action) > 0 {
loc := executeRegexp.FindStringIndex(action)
if loc == nil {
masked += action
break
}
masked += action[:loc[1]]
action = action[loc[1]:]
if len(action) == 0 {
break
}
cs := string(action[0])
var ce string
switch action[0] {
case ':':
masked += strings.Repeat(" ", len(action))
break Loop
case '(':
ce = ")"
case '{':
ce = "}"
case '[':
ce = "]"
case '<':
ce = ">"
case '~', '!', '@', '#', '$', '%', '^', '&', '*', ';', '/', '|':
ce = string(cs)
default:
continue
}
cs = regexp.QuoteMeta(cs)
ce = regexp.QuoteMeta(ce)
// @$ or @+
loc = regexp.MustCompile(fmt.Sprintf(`(?s)^%s.*?(%s[+,]|%s$)`, cs, ce, ce)).FindStringIndex(action)
if loc == nil {
masked += action
break
}
// Keep + or , at the end
lastChar := action[loc[1]-1]
if lastChar == '+' || lastChar == ',' {
loc[1]--
}
masked += strings.Repeat(" ", loc[1])
action = action[loc[1]:]
}
masked = strings.ReplaceAll(masked, ",,,", string([]rune{',', escapedComma, ','}))
masked = strings.ReplaceAll(masked, ",:,", string([]rune{',', escapedColon, ','}))
masked = strings.ReplaceAll(masked, "::", string([]rune{escapedColon, ':'}))
masked = strings.ReplaceAll(masked, ",:", string([]rune{escapedComma, ':'}))
masked = strings.ReplaceAll(masked, "+:", string([]rune{escapedPlus, ':'}))
return masked
}
func parseSingleActionList(str string) ([]*action, error) {
// We prepend a colon to satisfy executeRegexp and remove it later
masked := maskActionContents(":" + str)[1:]
return parseActionList(masked, str, []*action{}, false)
}
func parseActionList(masked string, original string, prevActions []*action, putAllowed bool) ([]*action, error) {
maskedStrings := strings.Split(masked, "+")
originalStrings := make([]string, len(maskedStrings))
idx := 0
for i, maskedString := range maskedStrings {
originalStrings[i] = original[idx : idx+len(maskedString)]
idx += len(maskedString) + 1
}
actions := make([]*action, 0, len(maskedStrings))
appendAction := func(types ...actionType) {
actions = append(actions, toActions(types...)...)
}
prevSpec := ""
for specIndex, spec := range originalStrings {
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 "accept-or-print-query":
appendAction(actAcceptOrPrintQuery)
case "print-query":
appendAction(actPrintQuery)
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 "backward-subword":
appendAction(actBackwardSubWord)
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-multi", "clear-selection":
appendAction(actClearSelection)
case "forward-char":
appendAction(actForwardChar)
case "forward-word":
appendAction(actForwardWord)
case "forward-subword":
appendAction(actForwardSubWord)
case "jump":
appendAction(actJump)
case "jump-accept":
appendAction(actJumpAccept)
case "kill-line":
appendAction(actKillLine)
case "kill-word":
appendAction(actKillWord)
case "kill-subword":
appendAction(actKillSubWord)
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 "backward-kill-subword":
appendAction(actBackwardKillSubWord)
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 "toggle-track":
appendAction(actToggleTrack)
case "toggle-track-current":
appendAction(actToggleTrackCurrent)
case "toggle-input":
appendAction(actToggleInput)
case "hide-input":
appendAction(actHideInput)
case "show-input":
appendAction(actShowInput)
case "toggle-header":
appendAction(actToggleHeader)
case "toggle-wrap":
appendAction(actToggleWrap)
case "toggle-multi-line":
appendAction(actToggleMultiLine)
case "toggle-hscroll":
appendAction(actToggleHscroll)
case "toggle-raw":
appendAction(actToggleRaw)
case "enable-raw":
appendAction(actEnableRaw)
case "disable-raw":
appendAction(actDisableRaw)
case "show-header":
appendAction(actShowHeader)
case "hide-header":
appendAction(actHideHeader)
case "track", "track-current":
appendAction(actTrackCurrent)
case "untrack-current":
appendAction(actUntrackCurrent)
case "select":
appendAction(actSelect)
case "select-all":
appendAction(actSelectAll)
case "deselect-all":
appendAction(actDeselectAll)
case "close":
appendAction(actClose)
case "toggle":
appendAction(actToggle)
case "down":
appendAction(actDown)
case "down-match":
appendAction(actDownMatch)
case "up":
appendAction(actUp)
case "up-match":
appendAction(actUpMatch)
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 "prev-history", "previous-history":
appendAction(actPrevHistory)
case "next-history":
appendAction(actNextHistory)
case "up-selected", "prev-selected":
appendAction(actPrevSelected)
case "down-selected", "next-selected":
appendAction(actNextSelected)
case "show-preview":
appendAction(actShowPreview)
case "hide-preview":
appendAction(actHidePreview)
case "toggle-preview":
appendAction(actTogglePreview)
case "toggle-preview-wrap":
appendAction(actTogglePreviewWrap)
case "toggle-sort":
appendAction(actToggleSort)
case "offset-up":
appendAction(actOffsetUp)
case "offset-down":
appendAction(actOffsetDown)
case "offset-middle":
appendAction(actOffsetMiddle)
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 putAllowed {
appendAction(actChar)
} else {
return nil, errors.New("unable to put non-printable character")
}
case "bell":
appendAction(actBell)
case "exclude":
appendAction(actExclude)
case "exclude-multi":
appendAction(actExcludeMulti)
case "bg-cancel":
appendAction(actBgCancel)
default:
t := isExecuteAction(specLower)
if t == actIgnore {
if specIndex == 0 && specLower == "" {
actions = append(prevActions, actions...)
} else if specLower == "change-multi" {
appendAction(actChangeMulti)
} else {
return nil, errors.New("unknown action: " + spec)
}
} else {
offset := len(actionNameRegexp.FindString(spec))
var actionArg string
if spec[offset] == ':' {
if specIndex == len(originalStrings)-1 {
actionArg = spec[offset+1:]
actions = append(actions, &action{t: t, a: actionArg})
} else {
prevSpec = spec + "+"
continue
}
} else {
actionArg = spec[offset+1 : len(spec)-1]
actions = append(actions, &action{t: t, a: actionArg})
}
switch t {
case actUnbind, actRebind, actToggleBind:
if _, _, err := parseKeyChords(actionArg, spec[0:offset]+" target required"); err != nil {
return nil, err
}
case actChangePreviewWindow:
opts := previewOpts{}
for _, arg := range strings.Split(actionArg, "|") {
// Make sure that each expression is valid
if err := parsePreviewWindowImpl(&opts, arg); err != nil {
return nil, err
}
}
}
}
}
prevSpec = ""
}
return actions, nil
}
func parseKeymap(keymap map[tui.Event][]*action, str string) error {
var err error
masked := maskActionContents(str)
idx := 0
keys := []string{}
for _, pairStr := range strings.Split(masked, ",") {
origPairStr := str[idx : idx+len(pairStr)]
idx += len(pairStr) + 1
pair := strings.SplitN(pairStr, ":", 2)
if len(pair[0]) == 0 {
return errors.New("key name required")
}
keys = append(keys, pair[0])
if len(pair) < 2 {
continue
}
for _, keyName := range keys {
var key tui.Event
if len(keyName) == 1 && keyName[0] == escapedColon {
key = tui.Key(':')
} else if len(keyName) == 1 && keyName[0] == escapedComma {
key = tui.Key(',')
} else if len(keyName) == 1 && keyName[0] == escapedPlus {
key = tui.Key('+')
} else {
keys, _, err := parseKeyChords(keyName, "key name required")
if err != nil {
return err
}
key = firstKey(keys)
}
putAllowed := key.Type == tui.Rune && unicode.IsGraphic(key.Char)
keymap[key], err = parseActionList(pair[1], origPairStr[len(pair[0])+1:], keymap[key], putAllowed)
if err != nil {
return err
}
}
keys = keys[:0]
}
if len(keys) > 0 {
return errors.New("bind action not specified: " + strings.Join(keys, ", "))
}
return nil
}
func isExecuteAction(str string) actionType {
masked := maskActionContents(":" + str)[1:]
if masked == str {
// Not masked
return actIgnore
}
prefix := actionNameRegexp.FindString(str)
switch prefix {
case "become":
return actBecome
case "reload":
return actReload
case "reload-sync":
return actReloadSync
case "unbind":
return actUnbind
case "rebind":
return actRebind
case "toggle-bind":
return actToggleBind
case "preview":
return actPreview
case "change-header":
return actChangeHeader
case "change-footer":
return actChangeFooter
case "change-list-label":
return actChangeListLabel
case "change-border-label":
return actChangeBorderLabel
case "change-preview-label":
return actChangePreviewLabel
case "change-input-label":
return actChangeInputLabel
case "change-header-label":
return actChangeHeaderLabel
case "change-footer-label":
return actChangeFooterLabel
case "change-ghost":
return actChangeGhost
case "change-pointer":
return actChangePointer
case "change-preview-window":
return actChangePreviewWindow
case "change-preview":
return actChangePreview
case "change-prompt":
return actChangePrompt
case "change-query":
return actChangeQuery
case "change-multi":
return actChangeMulti
case "change-nth":
return actChangeNth
case "pos":
return actPosition
case "execute":
return actExecute
case "execute-silent":
return actExecuteSilent
case "execute-multi":
return actExecuteMulti
case "print":
return actPrint
case "put":
return actPut
case "transform":
return actTransform
case "transform-list-label":
return actTransformListLabel
case "transform-border-label":
return actTransformBorderLabel
case "transform-preview-label":
return actTransformPreviewLabel
case "transform-input-label":
return actTransformInputLabel
case "transform-header-label":
return actTransformHeaderLabel
case "transform-footer-label":
return actTransformFooterLabel
case "transform-footer":
return actTransformFooter
case "transform-header":
return actTransformHeader
case "transform-ghost":
return actTransformGhost
case "transform-nth":
return actTransformNth
case "transform-pointer":
return actTransformPointer
case "transform-prompt":
return actTransformPrompt
case "transform-query":
return actTransformQuery
case "transform-search":
return actTransformSearch
case "bg-transform":
return actBgTransform
case "bg-transform-list-label":
return actBgTransformListLabel
case "bg-transform-border-label":
return actBgTransformBorderLabel
case "bg-transform-preview-label":
return actBgTransformPreviewLabel
case "bg-transform-input-label":
return actBgTransformInputLabel
case "bg-transform-header-label":
return actBgTransformHeaderLabel
case "bg-transform-footer-label":
return actBgTransformFooterLabel
case "bg-transform-footer":
return actBgTransformFooter
case "bg-transform-header":
return actBgTransformHeader
case "bg-transform-ghost":
return actBgTransformGhost
case "bg-transform-nth":
return actBgTransformNth
case "bg-transform-pointer":
return actBgTransformPointer
case "bg-transform-prompt":
return actBgTransformPrompt
case "bg-transform-query":
return actBgTransformQuery
case "bg-transform-search":
return actBgTransformSearch
case "trigger":
return actTrigger
case "search":
return actSearch
}
return actIgnore
}
func parseToggleSort(keymap map[tui.Event][]*action, str string) error {
keys, _, err := parseKeyChords(str, "key name required")
if err != nil {
return err
}
if len(keys) != 1 {
return errors.New("multiple keys specified")
}
keymap[firstKey(keys)] = toActions(actToggleSort)
return nil
}
func strLines(str string) []string {
return strings.Split(strings.TrimSuffix(str, "\n"), "\n")
}
func parseSize(str string, maxPercent float64, label string) (sizeSpec, error) {
var spec = sizeSpec{}
var val float64
var err error
percent := strings.HasSuffix(str, "%")
if percent {
if val, err = atof(str[:len(str)-1]); err != nil {
return spec, err
}
if val < 0 {
return spec, errors.New(label + " must be non-negative")
}
if val > maxPercent {
return spec, fmt.Errorf("%s too large (max: %d%%)", label, int(maxPercent))
}
} else {
if strings.Contains(str, ".") {
return spec, errors.New(label + " (without %) must be a non-negative integer")
}
i, err := atoi(str)
if err != nil {
return spec, err
}
val = float64(i)
if val < 0 {
return spec, errors.New(label + " must be non-negative")
}
}
return sizeSpec{val, percent}, nil
}
func parseHeight(str string, index int) (heightSpec, error) {
heightSpec := heightSpec{index: index}
if strings.HasPrefix(str, "~") {
heightSpec.auto = true
str = str[1:]
}
if strings.HasPrefix(str, "-") {
if heightSpec.auto {
return heightSpec, errors.New("negative(-) height is not compatible with adaptive(~) height")
}
heightSpec.inverse = true
str = str[1:]
}
size, err := parseSize(str, 100, "height")
if err != nil {
return heightSpec, err
}
heightSpec.size = size.size
heightSpec.percent = size.percent
return heightSpec, nil
}
func parseLayout(str string) (layoutType, error) {
switch str {
case "default":
return layoutDefault, nil
case "reverse":
return layoutReverse, nil
case "reverse-list":
return layoutReverseList, nil
}
return layoutDefault, errors.New("invalid layout (expected: default / reverse / reverse-list)")
}
func parseInfoStyle(str string) (infoStyle, string, error) {
switch str {
case "default":
return infoDefault, "", nil
case "right":
return infoRight, "", nil
case "inline":
return infoInline, defaultInfoPrefix, nil
case "inline-right":
return infoInlineRight, "", nil
case "hidden":
return infoHidden, "", nil
}
type infoSpec struct {
name string
style infoStyle
}
for _, spec := range []infoSpec{
{"inline", infoInline},
{"inline-right", infoInlineRight}} {
if strings.HasPrefix(str, spec.name+":") {
return spec.style, strings.ReplaceAll(str[len(spec.name)+1:], "\n", " "), nil
}
}
return infoDefault, "", errors.New("invalid info style (expected: default|right|hidden|inline[-right][:PREFIX])")
}
func parsePreviewWindow(opts *previewOpts, input string) error {
return parsePreviewWindowImpl(opts, input)
}
func parsePreviewWindowImpl(opts *previewOpts, input string) error {
var err error
tokenRegex := regexp.MustCompile(`[:,]*(<([1-9][0-9]*)\(([^)<]+)\)|[^,:]+)`)
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
offsetRegex := regexp.MustCompile(`^(\+{(-?[0-9]+|n)})?([+-][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 {
if opts.threshold, err = atoi(match[2]); err != nil {
return err
}
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 "border-line":
opts.border = tui.BorderLine
case "sharp", "border-sharp":
opts.border = tui.BorderSharp
case "border-bold":
opts.border = tui.BorderBold
case "border-block":
opts.border = tui.BorderBlock
case "border-thinblock":
opts.border = tui.BorderThinBlock
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
case "follow":
opts.follow = true
case "nofollow":
opts.follow = false
case "info":
opts.info = true
case "noinfo":
opts.info = false
default:
if headerRegex.MatchString(token) {
if opts.headerLines, err = atoi(token[1:]); err != nil {
return err
}
} else if sizeRegex.MatchString(token) {
if opts.size, err = parseSize(token, 99, "window size"); err != nil {
return err
}
} else if offsetRegex.MatchString(token) {
opts.scroll = token
} else {
return errors.New("invalid preview window option: " + token)
}
}
}
if len(alternative) > 0 {
alternativeOpts := *opts
opts.alternative = &alternativeOpts
opts.alternative.hidden = false
opts.alternative.alternative = nil
err = parsePreviewWindowImpl(opts.alternative, alternative)
}
return err
}
func parseMargin(opt string, margin string) ([4]sizeSpec, error) {
margins := strings.Split(margin, ",")
checked := func(str string) (sizeSpec, error) {
return parseSize(str, 49, opt)
}
switch len(margins) {
case 1:
m, e := checked(margins[0])
return [4]sizeSpec{m, m, m, m}, e
case 2:
tb, e := checked(margins[0])
if e != nil {
return defaultMargin(), e
}
rl, e := checked(margins[1])
if e != nil {
return defaultMargin(), e
}
return [4]sizeSpec{tb, rl, tb, rl}, nil
case 3:
t, e := checked(margins[0])
if e != nil {
return defaultMargin(), e
}
rl, e := checked(margins[1])
if e != nil {
return defaultMargin(), e
}
b, e := checked(margins[2])
if e != nil {
return defaultMargin(), e
}
return [4]sizeSpec{t, rl, b, rl}, nil
case 4:
t, e := checked(margins[0])
if e != nil {
return defaultMargin(), e
}
r, e := checked(margins[1])
if e != nil {
return defaultMargin(), e
}
b, e := checked(margins[2])
if e != nil {
return defaultMargin(), e
}
l, e := checked(margins[3])
if e != nil {
return defaultMargin(), e
}
return [4]sizeSpec{t, r, b, l}, nil
}
return [4]sizeSpec{}, errors.New("invalid " + opt + ": " + margin)
}
func parseMarkerMultiLine(str string) (*[3]string, error) {
if str == "" {
return &[3]string{}, nil
}
gr := uniseg.NewGraphemes(str)
parts := []string{}
totalWidth := 0
for gr.Next() {
s := string(gr.Runes())
totalWidth += uniseg.StringWidth(s)
parts = append(parts, s)
}
result := [3]string{}
if totalWidth != 3 && totalWidth != 6 {
return &result, fmt.Errorf("invalid total marker width: %d (expected: 0, 3 or 6)", totalWidth)
}
expected := totalWidth / 3
idx := 0
for _, part := range parts {
expected -= uniseg.StringWidth(part)
result[idx] += part
if expected <= 0 {
idx++
expected = totalWidth / 3
}
if idx == 3 {
break
}
}
return &result, nil
}
func optString(arg string, prefix string) (bool, string) {
if strings.HasPrefix(arg, prefix) {
return true, arg[len(prefix):]
}
return false, ""
}
func parseOptions(index *int, opts *Options, allArgs []string) error {
var err error
var historyMax int
if opts.History == nil {
historyMax = defaultHistoryMax
} else {
historyMax = opts.History.maxSize
}
setHistory := func(path string) error {
h, e := NewHistory(path, historyMax)
if e != nil {
return e
}
opts.History = h
return nil
}
setHistoryMax := func(max int) error {
historyMax = max
if historyMax < 1 {
return errors.New("history max must be a positive integer")
}
if opts.History != nil {
opts.History.maxSize = historyMax
}
return nil
}
validateJumpLabels := false
clearExitingOpts := func() {
// Last-one-wins strategy
opts.Bash = false
opts.Zsh = false
opts.Fish = false
opts.Help = false
opts.Version = false
opts.Man = false
}
startIndex := *index
var i int
var val *string = nil
nextString := func(message string) (string, error) {
defer func() { val = nil }()
if val != nil {
return *val, nil
}
if len(allArgs) > i+1 {
i++
} else {
return "", errors.New(message)
}
return allArgs[i], nil
}
optionalNextString := func() (bool, string) {
defer func() { val = nil }()
if val != nil {
return true, *val
}
if len(allArgs) > i+1 && !strings.HasPrefix(allArgs[i+1], "-") && !strings.HasPrefix(allArgs[i+1], "+") {
i++
return true, allArgs[i]
}
return false, ""
}
nextDirs := func() ([]string, error) {
defer func() { val = nil }()
dirs := []string{}
if val != nil {
dirs = append(dirs, *val)
}
for i < len(allArgs)-1 {
arg := allArgs[i+1]
if isDir(arg) {
dirs = append(dirs, arg)
i++
} else {
break
}
}
if len(dirs) == 0 {
return nil, errors.New("no directory specified")
}
return dirs, nil
}
nextInt := func(message string) (int, error) {
defer func() { val = nil }()
var str string
if val != nil {
str = *val
} else if len(allArgs) > i+1 {
i++
str = allArgs[i]
} else {
return 0, errors.New(message)
}
n, err := atoi(str)
if err != nil {
return 0, errors.New(message)
}
return n, nil
}
optionalNumeric := func(defaultValue int) (int, error) {
defer func() { val = nil }()
var str string
if val != nil {
str = *val
} else if len(allArgs) > i+1 && strings.IndexAny(allArgs[i+1], "0123456789") == 0 {
i++
str = allArgs[i]
} else {
return defaultValue, nil
}
n, err := atoi(str)
if err != nil {
return 0, err
}
return n, nil
}
for ; i < len(allArgs); i++ {
arg := allArgs[i]
index := i + startIndex
if strings.HasPrefix(arg, "--") && strings.IndexRune(arg, '=') > 0 {
tokens := strings.SplitN(arg, "=", 2)
arg = tokens[0]
val = &tokens[1]
}
switch arg {
case "--man":
clearExitingOpts()
opts.Man = true
case "--bash":
clearExitingOpts()
opts.Bash = true
case "--zsh":
clearExitingOpts()
opts.Zsh = true
case "--fish":
clearExitingOpts()
opts.Fish = true
case "-h", "--help":
clearExitingOpts()
opts.Help = true
case "--version":
clearExitingOpts()
opts.Version = true
case "--no-winpty":
opts.NoWinpty = true
case "--tmux":
given, str := optionalNextString()
if given {
if opts.Tmux, err = parseTmuxOptions(str, index); err != nil {
return err
}
} else {
opts.Tmux = defaultTmuxOptions(index)
}
case "--no-tmux":
opts.Tmux = nil
case "--tty-default":
if opts.TtyDefault, err = nextString("tty device name required"); err != nil {
return err
}
case "--no-tty-default":
opts.TtyDefault = ""
case "--force-tty-in":
// NOTE: We need this because `system('fzf --tmux < /dev/tty')` doesn't
// work on Neovim. Same as '-' option of fzf-tmux.
opts.ForceTtyIn = true
case "--no-force-tty-in":
opts.ForceTtyIn = false
case "--proxy-script":
if opts.ProxyScript, err = nextString(""); err != nil {
return err
}
case "-x", "--extended":
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
case "-q", "--query":
if opts.Query, err = nextString("query string required"); err != nil {
return err
}
case "-f", "--filter":
filter, err := nextString("query string required")
if err != nil {
return err
}
opts.Filter = &filter
case "--literal":
opts.Normalize = false
case "--no-literal":
opts.Normalize = true
case "--algo":
str, err := nextString("algorithm required (v1|v2)")
if err != nil {
return err
}
if opts.FuzzyAlgo, err = parseAlgo(str); err != nil {
return err
}
case "--scheme":
str, err := nextString("scoring scheme required (default|path|history)")
if err != nil {
return err
}
if opts.Scheme, opts.Criteria, err = parseScheme(str); err != nil {
return err
}
case "--expect":
str, err := nextString("key names required")
if err != nil {
return err
}
chords, _, err := parseKeyChords(str, "key names required")
if err != nil {
return err
}
maps.Copy(opts.Expect, chords)
case "--no-expect":
opts.Expect = make(map[tui.Event]string)
case "--enabled", "--no-phony":
opts.Phony = false
case "--disabled", "--phony":
opts.Phony = true
case "--no-input":
opts.Inputless = true
case "--tiebreak":
str, err := nextString("sort criterion required")
if err != nil {
return err
}
if opts.Criteria, err = parseTiebreak(str); err != nil {
return err
}
case "--bind":
str, err := nextString("bind expression required")
if err != nil {
return err
}
if err := parseKeymap(opts.Keymap, str); err != nil {
return err
}
case "--color":
_, spec := optionalNextString()
if len(spec) == 0 {
opts.Theme = tui.EmptyTheme()
} else {
if opts.Theme, err = parseTheme(opts.Theme, spec); err != nil {
return err
}
}
case "--toggle-sort":
str, err := nextString("key name required")
if err != nil {
return err
}
if err := parseToggleSort(opts.Keymap, str); err != nil {
return err
}
case "-d", "--delimiter":
str, err := nextString("delimiter required")
if err != nil {
return err
}
opts.Delimiter = delimiterRegexp(str)
case "-n", "--nth":
str, err := nextString("nth expression required")
if err != nil {
return err
}
if opts.Nth, err = splitNth(str); err != nil {
return err
}
case "--with-nth":
str, err := nextString("nth expression required")
if err != nil {
return err
}
if opts.WithNth, err = nthTransformer(str); err != nil {
return err
}
case "--accept-nth":
str, err := nextString("nth expression required")
if err != nil {
return err
}
if opts.AcceptNth, err = nthTransformer(str); err != nil {
return err
}
case "-s", "--sort":
if opts.Sort, err = optionalNumeric(1); err != nil {
return err
}
case "+s", "--no-sort":
opts.Sort = 0
case "--raw":
opts.Raw = true
case "--no-raw":
opts.Raw = false
case "--track":
opts.Track = trackEnabled
case "--no-track":
opts.Track = trackDisabled
case "--tac":
opts.Tac = true
case "--no-tac":
opts.Tac = false
case "--tail":
if opts.Tail, err = nextInt("number of items to keep required"); err != nil {
return err
}
if opts.Tail <= 0 {
return errors.New("number of items to keep must be a positive integer")
}
case "--no-tail":
opts.Tail = 0
case "--smart-case":
opts.Case = CaseSmart
case "-i", "--ignore-case":
opts.Case = CaseIgnore
case "+i", "--no-ignore-case":
opts.Case = CaseRespect
case "-m", "--multi":
if opts.Multi, err = optionalNumeric(maxMulti); err != nil {
return err
}
case "+m", "--no-multi":
opts.Multi = 0
case "--ansi":
opts.Ansi = true
case "--no-ansi":
opts.Ansi = false
case "--no-mouse":
opts.Mouse = false
case "+c", "--no-color":
opts.Theme = tui.NoColorTheme()
case "+2", "--no-256":
opts.Theme = tui.Default16
case "--black":
opts.Black = true
case "--no-black":
opts.Black = false
case "--bold":
opts.Bold = true
case "--no-bold":
opts.Bold = false
case "--layout":
str, err := nextString("layout required (default / reverse / reverse-list)")
if err != nil {
return err
}
if opts.Layout, err = parseLayout(str); err != nil {
return err
}
case "--reverse":
opts.Layout = layoutReverse
case "--no-reverse":
opts.Layout = layoutDefault
case "--cycle":
opts.Cycle = true
case "--highlight-line":
opts.CursorLine = true
case "--no-highlight-line":
opts.CursorLine = false
case "--no-cycle":
opts.Cycle = false
case "--wrap":
opts.Wrap = true
case "--no-wrap":
opts.Wrap = false
case "--wrap-sign":
str, err := nextString("wrap sign required")
if err != nil {
return err
}
opts.WrapSign = &str
case "--multi-line":
opts.MultiLine = true
case "--no-multi-line":
opts.MultiLine = 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":
if opts.HscrollOff, err = nextInt("hscroll offset required"); err != nil {
return err
}
case "--scroll-off":
if opts.ScrollOff, err = nextInt("scroll offset required"); err != nil {
return err
}
case "--filepath-word":
opts.FileWord = true
case "--no-filepath-word":
opts.FileWord = false
case "--info":
str, err := nextString("info style required")
if err != nil {
return err
}
if opts.InfoStyle, opts.InfoPrefix, err = parseInfoStyle(str); err != nil {
return err
}
case "--info-command":
if opts.InfoCommand, err = nextString("info command required"); err != nil {
return err
}
case "--no-info-command":
opts.InfoCommand = ""
case "--no-info":
opts.InfoStyle = infoHidden
case "--inline-info":
opts.InfoStyle = infoInline
opts.InfoPrefix = defaultInfoPrefix
case "--no-inline-info":
opts.InfoStyle = infoDefault
case "--separator":
separator, err := nextString("separator character required")
if err != nil {
return err
}
opts.Separator = &separator
case "--no-separator":
nosep := ""
opts.Separator = &nosep
case "--ghost":
if opts.Ghost, err = nextString("ghost text required"); err != nil {
return err
}
case "--scrollbar":
given, bar := optionalNextString()
if given {
opts.Scrollbar = &bar
} else {
opts.Scrollbar = nil
}
case "--no-scrollbar":
noBar := ""
opts.Scrollbar = &noBar
case "--jump-labels":
if opts.JumpLabels, err = nextString("label characters required"); err != nil {
return err
}
validateJumpLabels = true
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
case "--print0":
opts.Printer = func(str string) { fmt.Print(str, "\x00") }
opts.PrintSep = "\x00"
case "--no-print0":
opts.Printer = func(str string) { fmt.Println(str) }
opts.PrintSep = "\n"
case "--print-query":
opts.PrintQuery = true
case "--no-print-query":
opts.PrintQuery = false
case "--prompt":
opts.Prompt, err = nextString("prompt string required")
if err != nil {
return err
}
case "--gutter":
str, err := nextString("gutter character required")
if err != nil {
return err
}
str = firstLine(str)
opts.Gutter = &str
case "--gutter-raw":
str, err := nextString("gutter character for raw mode required")
if err != nil {
return err
}
str = firstLine(str)
opts.GutterRaw = &str
case "--pointer":
str, err := nextString("pointer sign required")
if err != nil {
return err
}
str = firstLine(str)
opts.Pointer = &str
case "--marker":
str, err := nextString("marker sign required")
if err != nil {
return err
}
str = firstLine(str)
opts.Marker = &str
case "--marker-multi-line":
str, err := nextString("marker sign for multi-line entries required")
if err != nil {
return err
}
if opts.MarkerMulti, err = parseMarkerMultiLine(firstLine(str)); err != nil {
return err
}
case "--sync":
opts.Sync = true
case "--no-sync", "--async":
opts.Sync = false
case "--no-history":
opts.History = nil
case "--history":
str, err := nextString("history file path required")
if err != nil {
return err
}
if err := setHistory(str); err != nil {
return err
}
case "--history-size":
n, err := nextInt("history max size required")
if err != nil {
return err
}
if err := setHistoryMax(n); err != nil {
return err
}
case "--no-header":
opts.Header = []string{}
case "--no-header-lines":
opts.HeaderLines = 0
case "--header":
str, err := nextString("header string required")
if err != nil {
return err
}
opts.Header = strLines(str)
case "--header-lines":
if opts.HeaderLines, err = nextInt("number of header lines required"); err != nil {
return err
}
case "--no-footer":
opts.Footer = []string{}
case "--footer":
str, err := nextString("footer string required")
if err != nil {
return err
}
opts.Footer = strLines(str)
case "--header-first":
opts.HeaderFirst = true
case "--no-header-first":
opts.HeaderFirst = false
case "--gap":
if opts.Gap, err = optionalNumeric(1); err != nil {
return err
}
case "--no-gap":
opts.Gap = 0
case "--gap-line":
if given, bar := optionalNextString(); given {
opts.GapLine = &bar
} else {
opts.GapLine = nil
}
case "--no-gap-line":
empty := ""
opts.GapLine = &empty
case "--ellipsis":
str, err := nextString("ellipsis string required")
if err != nil {
return err
}
str = firstLine(str)
opts.Ellipsis = &str
case "--preview":
if opts.Preview.command, err = nextString("preview command required"); err != nil {
return err
}
case "--no-preview":
opts.Preview.command = ""
case "--preview-window":
str, err := nextString("preview window layout required: [up|down|left|right][,SIZE[%]][,border-STYLE][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]")
if err != nil {
return err
}
if err := parsePreviewWindow(&opts.Preview, str); err != nil {
return err
}
case "--no-preview-border":
opts.Preview.border = tui.BorderNone
case "--preview-border":
hasArg, arg := optionalNextString()
if opts.Preview.border, err = parseBorder(arg, !hasArg); err != nil {
return err
}
case "--height":
str, err := nextString("height required: [~]HEIGHT[%]")
if err != nil {
return err
}
if opts.Height, err = parseHeight(str, index); err != nil {
return err
}
case "--min-height":
expr, err := nextString("minimum height required: HEIGHT[+]")
if err != nil {
return err
}
auto := false
if strings.HasSuffix(expr, "+") {
expr = expr[:len(expr)-1]
auto = true
}
num, err := atoi(expr)
if err != nil || num < 0 {
return errors.New("minimum height must be a non-negative integer")
}
if auto {
num *= -1
}
opts.MinHeight = num
case "--no-height":
opts.Height = heightSpec{}
case "--no-margin":
opts.Margin = defaultMargin()
case "--no-padding":
opts.Padding = defaultMargin()
case "--no-border":
opts.BorderShape = tui.BorderNone
case "--border":
hasArg, arg := optionalNextString()
if opts.BorderShape, err = parseBorder(arg, !hasArg); err != nil {
return err
}
case "--list-border":
hasArg, arg := optionalNextString()
if opts.ListBorderShape, err = parseBorder(arg, !hasArg); err != nil {
return err
}
if opts.ListBorderShape == tui.BorderLine {
if hasArg {
// '--list-border line' is not allowed
return errors.New("list border cannot be 'line'")
}
// This is when '--style full:line' is previously specified and
// '--list-border' is specified without an argument.
opts.ListBorderShape = tui.BorderRounded
}
case "--no-list-border":
opts.ListBorderShape = tui.BorderNone
case "--no-list-label":
opts.ListLabel.label = ""
case "--list-label":
opts.ListLabel.label, err = nextString("label required")
if err != nil {
return err
}
case "--list-label-pos":
pos, err := nextString("label position required (positive or negative integer or 'center')")
if err != nil {
return err
}
if err := parseLabelPosition(&opts.ListLabel, pos); err != nil {
return err
}
case "--no-header-border":
opts.HeaderBorderShape = tui.BorderNone
case "--header-border":
hasArg, arg := optionalNextString()
if opts.HeaderBorderShape, err = parseBorder(arg, !hasArg); err != nil {
return err
}
case "--no-header-lines-border":
opts.HeaderLinesShape = tui.BorderUndefined
case "--header-lines-border":
hasArg, arg := optionalNextString()
if opts.HeaderLinesShape, err = parseBorder(arg, !hasArg); err != nil {
return err
}
case "--no-header-label":
opts.HeaderLabel.label = ""
case "--header-label":
if opts.HeaderLabel.label, err = nextString("header label required"); err != nil {
return err
}
case "--header-label-pos":
pos, err := nextString("header label position required (positive or negative integer or 'center')")
if err != nil {
return err
}
if err := parseLabelPosition(&opts.HeaderLabel, pos); err != nil {
return err
}
case "--no-footer-border":
opts.FooterBorderShape = tui.BorderNone
case "--footer-border":
hasArg, arg := optionalNextString()
if opts.FooterBorderShape, err = parseBorder(arg, !hasArg); err != nil {
return err
}
case "--no-footer-label":
opts.FooterLabel.label = ""
case "--footer-label":
if opts.FooterLabel.label, err = nextString("footer label required"); err != nil {
return err
}
case "--footer-label-pos":
pos, err := nextString("footer label position required (positive or negative integer or 'center')")
if err != nil {
return err
}
if err := parseLabelPosition(&opts.FooterLabel, pos); err != nil {
return err
}
case "--no-input-border":
opts.InputBorderShape = tui.BorderNone
case "--input-border":
hasArg, arg := optionalNextString()
if opts.InputBorderShape, err = parseBorder(arg, !hasArg); err != nil {
return err
}
case "--no-input-label":
opts.InputLabel.label = ""
case "--input-label":
if opts.InputLabel.label, err = nextString("input label required"); err != nil {
return err
}
case "--input-label-pos":
pos, err := nextString("input label position required (positive or negative integer or 'center')")
if err != nil {
return err
}
if err := parseLabelPosition(&opts.InputLabel, pos); err != nil {
return err
}
case "--no-border-label":
opts.BorderLabel.label = ""
case "--border-label":
opts.BorderLabel.label, err = nextString("label required")
if err != nil {
return err
}
case "--border-label-pos":
pos, err := nextString("label position required (positive or negative integer or 'center')")
if err != nil {
return err
}
if err := parseLabelPosition(&opts.BorderLabel, pos); err != nil {
return err
}
case "--no-preview-label":
opts.PreviewLabel.label = ""
case "--preview-label":
if opts.PreviewLabel.label, err = nextString("preview label required"); err != nil {
return err
}
case "--preview-label-pos":
pos, err := nextString("preview label position required (positive or negative integer or 'center')")
if err != nil {
return err
}
if err := parseLabelPosition(&opts.PreviewLabel, pos); err != nil {
return err
}
case "--style":
preset, err := nextString("preset name required: [default|minimal|full[:BORDER_STYLE]]")
if err != nil {
return err
}
if err := applyPreset(opts, preset); err != nil {
return err
}
case "--no-unicode":
opts.Unicode = false
case "--unicode":
opts.Unicode = true
case "--ambidouble":
opts.Ambidouble = true
case "--no-ambidouble":
opts.Ambidouble = false
case "--margin":
str, err := nextString("margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)")
if err != nil {
return err
}
if opts.Margin, err = parseMargin("margin", str); err != nil {
return err
}
case "--padding":
str, err := nextString("padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)")
if err != nil {
return err
}
if opts.Padding, err = parseMargin("padding", str); err != nil {
return err
}
case "--tabstop":
if opts.Tabstop, err = nextInt("tab stop required"); err != nil {
return err
}
case "--with-shell":
if opts.WithShell, err = nextString("shell command and flags required"); err != nil {
return err
}
case "--listen", "--listen-unsafe":
given, str := optionalNextString()
addr := defaultListenAddr
if given {
var err error
addr, err = parseListenAddress(str)
if err != nil {
return err
}
}
opts.ListenAddr = &addr
opts.Unsafe = arg == "--listen-unsafe"
case "--no-listen", "--no-listen-unsafe":
opts.ListenAddr = nil
opts.Unsafe = false
case "--clear":
opts.ClearOnExit = true
case "--no-clear":
opts.ClearOnExit = false
case "--walker":
str, err := nextString("walker options required [file][,dir][,follow][,hidden]")
if err != nil {
return err
}
if opts.WalkerOpts, err = parseWalkerOpts(str); err != nil {
return err
}
case "--walker-root":
if opts.WalkerRoot, err = nextDirs(); err != nil {
return err
}
case "--walker-skip":
str, err := nextString("directory names to ignore required")
if err != nil {
return err
}
opts.WalkerSkip = filterNonEmpty(strings.Split(str, ","))
case "--profile-cpu":
if opts.CPUProfile, err = nextString("file path required: cpu"); err != nil {
return err
}
case "--profile-mem":
if opts.MEMProfile, err = nextString("file path required: mem"); err != nil {
return err
}
case "--profile-block":
if opts.BlockProfile, err = nextString("file path required: block"); err != nil {
return err
}
case "--profile-mutex":
if opts.MutexProfile, err = nextString("file path required: mutex"); err != nil {
return err
}
case "--":
// Ignored
default:
if match, value := optString(arg, "-q"); match {
opts.Query = value
} else if match, value := optString(arg, "-f"); match {
opts.Filter = &value
} else if match, value := optString(arg, "-d"); match {
opts.Delimiter = delimiterRegexp(value)
} else if match, value := optString(arg, "-n"); match {
if opts.Nth, err = splitNth(value); err != nil {
return err
}
} else if match, _ := optString(arg, "-s"); match {
opts.Sort = 1 // Don't care
} else if match, value := optString(arg, "-m"); match {
if opts.Multi, err = atoi(value); err != nil {
return err
}
} else {
return errors.New("unknown option: " + arg)
}
}
if val != nil {
return errors.New("unexpected value for " + arg + ": " + *val)
}
}
*index += len(allArgs)
if opts.HeaderLines < 0 {
return errors.New("header lines must be a non-negative integer")
}
if opts.HscrollOff < 0 {
return errors.New("hscroll offset must be a non-negative integer")
}
if opts.ScrollOff < 0 {
return errors.New("scroll offset must be a non-negative integer")
}
if opts.Tabstop < 1 {
return errors.New("tab stop must be a positive integer")
}
if len(opts.JumpLabels) == 0 {
return errors.New("empty jump labels")
}
if validateJumpLabels {
for _, r := range opts.JumpLabels {
if r < 32 || r > 126 {
return errors.New("non-ascii jump labels are not allowed")
}
}
}
return err
}
func applyPreset(opts *Options, preset string) error {
// Reset to the platform default
defaultBorderShape = tui.DefaultBorderShape
switch strings.ToLower(preset) {
case "default":
opts.ListBorderShape = tui.BorderUndefined
opts.InputBorderShape = tui.BorderUndefined
opts.HeaderBorderShape = tui.BorderUndefined
opts.FooterBorderShape = tui.BorderUndefined
opts.Preview.border = defaultBorderShape
opts.Preview.info = true
opts.InfoStyle = infoDefault
opts.Theme.Gutter = tui.NewColorAttr()
opts.Separator = nil
opts.Scrollbar = nil
opts.CursorLine = false
case "minimal":
opts.ListBorderShape = tui.BorderUndefined
opts.InputBorderShape = tui.BorderUndefined
opts.HeaderBorderShape = tui.BorderUndefined
opts.FooterBorderShape = tui.BorderLine
opts.Preview.border = tui.BorderLine
opts.Preview.info = false
opts.InfoStyle = infoDefault
opts.Theme.Gutter = tui.ColorAttr{Color: -1, Attr: 0}
empty := ""
opts.Separator = &empty
opts.Scrollbar = &empty
opts.CursorLine = false
default:
tokens := strings.SplitN(preset, ":", 2)
if tokens[0] != "full" {
return errors.New("unsupported style preset: " + preset)
}
if len(tokens) == 2 && len(tokens[1]) > 0 {
var err error
defaultBorderShape, err = parseBorder(tokens[1], false)
if err != nil {
return err
}
}
if defaultBorderShape != tui.BorderLine {
opts.ListBorderShape = defaultBorderShape
}
opts.InputBorderShape = defaultBorderShape
opts.HeaderBorderShape = defaultBorderShape
opts.FooterBorderShape = defaultBorderShape
opts.Preview.border = defaultBorderShape
if defaultBorderShape == tui.BorderLine {
opts.BorderShape = defaultBorderShape
}
opts.Preview.info = true
opts.InfoStyle = infoInlineRight
opts.Theme.Gutter = tui.NewColorAttr()
opts.Separator = nil
opts.Scrollbar = nil
opts.CursorLine = true
}
return nil
}
func validateSign(sign string, signOptName string, maxWidth int) error {
if uniseg.StringWidth(sign) > maxWidth {
return fmt.Errorf("%v display width should be up to %d", signOptName, maxWidth)
}
return nil
}
func validateOptions(opts *Options) error {
if opts.Pointer != nil {
if err := validateSign(*opts.Pointer, "pointer", 2); err != nil {
return err
}
}
if opts.Marker != nil {
if err := validateSign(*opts.Marker, "marker", 2); err != nil {
return err
}
}
if opts.Gutter != nil {
if err := validateSign(*opts.Gutter, "gutter", 1); err != nil {
return err
}
}
if opts.GutterRaw != nil {
if err := validateSign(*opts.GutterRaw, "gutter", 1); err != nil {
return err
}
}
if opts.Scrollbar != nil {
runes := []rune(*opts.Scrollbar)
if len(runes) > 2 {
return errors.New("--scrollbar should be given one or two characters")
}
for _, r := range runes {
if uniseg.StringWidth(string(r)) != 1 {
return errors.New("scrollbar display width should be 1")
}
}
}
if opts.Height.auto {
for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} {
if s.percent {
return errors.New("adaptive height is not compatible with top/bottom percent margin")
}
}
for _, s := range []sizeSpec{opts.Padding[0], opts.Padding[2]} {
if s.percent {
return errors.New("adaptive height is not compatible with top/bottom percent padding")
}
}
}
if opts.Theme.Nth.IsColorDefined() {
return errors.New("only ANSI attributes are allowed for 'nth' (regular, bold, underline, reverse, dim, italic, strikethrough)")
}
return nil
}
func noSeparatorLine(style infoStyle, separator bool) bool {
switch style {
case infoInline:
return true
case infoHidden, infoInlineRight:
return !separator
}
return false
}
func (opts *Options) useTmux() bool {
return opts.Tmux != nil && len(os.Getenv("TMUX")) > 0 && opts.Tmux.index >= opts.Height.index
}
func (opts *Options) noSeparatorLine() bool {
if opts.Inputless {
return true
}
sep := opts.Separator == nil && !opts.InputBorderShape.Visible() || opts.Separator != nil && len(*opts.Separator) > 0
return noSeparatorLine(opts.InfoStyle, sep)
}
// This function can have side-effects and alter some global states.
// So we run it on fzf.Run and not on ParseOptions.
func postProcessOptions(opts *Options) error {
if opts.Ambidouble {
uniseg.EastAsianAmbiguousWidth = 2
}
if opts.BorderShape == tui.BorderUndefined {
opts.BorderShape = tui.BorderNone
}
if opts.ListBorderShape == tui.BorderUndefined {
opts.ListBorderShape = tui.BorderNone
}
if opts.InputBorderShape == tui.BorderUndefined {
opts.InputBorderShape = tui.BorderNone
}
if opts.HeaderBorderShape == tui.BorderUndefined {
opts.HeaderBorderShape = tui.BorderNone
}
if opts.FooterBorderShape == tui.BorderUndefined {
opts.FooterBorderShape = tui.BorderLine
}
if opts.HeaderLinesShape == tui.BorderNone {
opts.HeaderLinesShape = tui.BorderPhantom
}
if opts.Pointer == nil {
defaultPointer := "▌"
if !opts.Unicode {
defaultPointer = ">"
}
opts.Pointer = &defaultPointer
}
if opts.GapLine == nil {
defaultGapLine := "┈"
if !opts.Unicode {
defaultGapLine = "-"
}
opts.GapLine = &defaultGapLine
}
markerLen := 1
if opts.Marker == nil {
if opts.MarkerMulti != nil && opts.MarkerMulti[0] == "" {
empty := ""
opts.Marker = &empty
markerLen = 0
} else {
// "▎" looks better, but not all terminals render it correctly
defaultMarker := "┃"
if !opts.Unicode {
defaultMarker = ">"
}
opts.Marker = &defaultMarker
}
} else {
markerLen = uniseg.StringWidth(*opts.Marker)
}
markerMultiLen := 1
if opts.MarkerMulti == nil {
if *opts.Marker == "" {
opts.MarkerMulti = &[3]string{}
markerMultiLen = 0
} else if opts.Unicode {
opts.MarkerMulti = &[3]string{"╻", "┃", "╹"}
} else {
opts.MarkerMulti = &[3]string{".", "|", "'"}
}
} else {
markerMultiLen = uniseg.StringWidth(opts.MarkerMulti[0])
}
diff := markerMultiLen - markerLen
if diff > 0 {
padded := *opts.Marker + strings.Repeat(" ", diff)
opts.Marker = &padded
} else if diff < 0 {
for idx := range opts.MarkerMulti {
opts.MarkerMulti[idx] += strings.Repeat(" ", -diff)
}
}
// 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(actPrevHistory)
}
if _, prs := opts.Keymap[tui.CtrlN.AsEvent()]; !prs {
opts.Keymap[tui.CtrlN.AsEvent()] = toActions(actNextHistory)
}
}
// Extend the default key map
keymap := defaultKeymap()
for key, actions := range opts.Keymap {
reordered := []*action{}
for _, act := range actions {
switch act.t {
case actToggleSort:
// To display "+S"/"-S" on info line
opts.ToggleSort = true
case actTogglePreview, actShowPreview, actHidePreview, actChangePreviewWindow:
reordered = append(reordered, act)
}
}
// Re-organize actions so that we put actions that change the preview window first in the list.
// * change-preview-window(up,+10)+preview(sleep 3; cat {})+change-preview-window(up,+20)
// -> change-preview-window(up,+10)+change-preview-window(up,+20)+preview(sleep 3; cat {})
if len(reordered) > 0 {
for _, act := range actions {
switch act.t {
case actTogglePreview, actShowPreview, actHidePreview, actChangePreviewWindow:
default:
reordered = append(reordered, act)
}
}
actions = reordered
}
keymap[key] = actions
}
opts.Keymap = keymap
// If 'double-click' is left unbound, bind it to the action bound to 'enter'
if _, prs := opts.Keymap[tui.DoubleClick.AsEvent()]; !prs {
opts.Keymap[tui.DoubleClick.AsEvent()] = opts.Keymap[tui.Enter.AsEvent()]
}
// If we're not using extended search mode, --nth option becomes irrelevant
// if it contains the whole range
if !opts.Extended || len(opts.Nth) == 1 {
for _, r := range opts.Nth {
if r.begin == rangeEllipsis && r.end == rangeEllipsis {
opts.Nth = make([]Range, 0)
break
}
}
}
if opts.Bold {
theme := opts.Theme
boldify := func(c tui.ColorAttr) tui.ColorAttr {
dup := c
if (c.Attr & tui.AttrRegular) == 0 {
dup.Attr |= tui.BoldForce
}
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 --height option is not supported on the platform, just ignore it
if !tui.IsLightRendererSupported() && opts.Height.size > 0 {
opts.Height = heightSpec{}
}
// Sets --min-height automatically
if opts.Height.size > 0 && opts.Height.percent && opts.MinHeight < 0 {
opts.MinHeight = -opts.MinHeight + borderLines(opts.BorderShape) + borderLines(opts.ListBorderShape)
if !opts.Inputless {
opts.MinHeight += 1 + borderLines(opts.InputBorderShape)
if !opts.noSeparatorLine() {
opts.MinHeight++
}
}
if len(opts.Header) > 0 {
opts.MinHeight += borderLines(opts.HeaderBorderShape) + len(opts.Header)
}
if opts.HeaderLines > 0 {
borderShape := opts.HeaderBorderShape
if opts.HeaderLinesShape.Visible() {
borderShape = opts.HeaderLinesShape
}
opts.MinHeight += borderLines(borderShape) + opts.HeaderLines
}
if len(opts.Preview.command) > 0 && (opts.Preview.position == posUp || opts.Preview.position == posDown) && opts.Preview.Visible() && opts.Preview.position == posUp {
borderShape := opts.Preview.border
if opts.Preview.border == tui.BorderLine {
borderShape = tui.BorderTop
}
opts.MinHeight += borderLines(borderShape) + 10
}
for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2], opts.Padding[0], opts.Padding[2]} {
if !s.percent {
opts.MinHeight += int(s.size)
}
}
}
if err := opts.initProfiling(); err != nil {
return errors.New("failed to start pprof profiles: " + err.Error())
}
algo.Init(opts.Scheme)
return nil
}
func parseShellWords(str string) ([]string, error) {
parser := shellwords.NewParser()
parser.ParseComment = true
return parser.Parse(str)
}
// ParseOptions parses command-line options
func ParseOptions(useDefaults bool, args []string) (*Options, error) {
opts := defaultOptions()
index := 0
if useDefaults {
// 1. Options from $FZF_DEFAULT_OPTS_FILE
if path := os.Getenv("FZF_DEFAULT_OPTS_FILE"); path != "" {
bytes, err := os.ReadFile(path)
if err != nil {
return nil, errors.New("$FZF_DEFAULT_OPTS_FILE: " + err.Error())
}
words, parseErr := parseShellWords(string(bytes))
if parseErr != nil {
return nil, errors.New(path + ": " + parseErr.Error())
}
if len(words) > 0 {
if err := parseOptions(&index, opts, words); err != nil {
return nil, errors.New(path + ": " + err.Error())
}
}
}
// 2. Options from $FZF_DEFAULT_OPTS string
words, parseErr := parseShellWords(os.Getenv("FZF_DEFAULT_OPTS"))
if parseErr != nil {
return nil, errors.New("$FZF_DEFAULT_OPTS: " + parseErr.Error())
}
if len(words) > 0 {
if err := parseOptions(&index, opts, words); err != nil {
return nil, errors.New("$FZF_DEFAULT_OPTS: " + err.Error())
}
}
}
// 3. Options from command-line arguments
if err := parseOptions(&index, opts, args); err != nil {
return nil, err
}
// 4. Change default scheme when built-in walker is used
if len(opts.Scheme) == 0 {
opts.Scheme = "default"
if len(opts.Criteria) == 0 {
// NOTE: Let's assume $FZF_DEFAULT_COMMAND generates a list of file paths.
// But it is possible that it is set to a command that doesn't generate
// file paths.
//
// In that case, you can either
// 1. explicitly set --scheme=default,
// 2. or replace $FZF_DEFAULT_COMMAND with an equivalent 'start:reload'
// binding, which is the new preferred way.
if !opts.hasReloadOrTransformOnStart() && util.IsTty(os.Stdin) {
opts.Scheme = "path"
}
_, opts.Criteria, _ = parseScheme(opts.Scheme)
}
}
// 5. Final validation of merged options
if err := validateOptions(opts); err != nil {
return nil, err
}
return opts, nil
}
func (opts *Options) hasReloadOrTransformOnStart() bool {
if actions, prs := opts.Keymap[tui.Start.AsEvent()]; prs {
for _, action := range actions {
if action.t == actReload || action.t == actReloadSync || action.t == actTransform {
return true
}
}
}
return false
}
func (opts *Options) extractReloadOnStart() string {
cmd := ""
if actions, prs := opts.Keymap[tui.Start.AsEvent()]; prs {
filtered := []*action{}
for _, action := range actions {
if action.t == actReload || action.t == actReloadSync {
cmd = action.a
} else {
filtered = append(filtered, action)
}
}
opts.Keymap[tui.Start.AsEvent()] = filtered
}
return cmd
}