mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2025-04-05 08:41:51 +00:00
Different marker for the first and last line of multi-line entries
Can be configured via `--marker-multi-line`
This commit is contained in:
parent
0ccbd79e10
commit
2f51eb2b41
@ -10,6 +10,12 @@ CHANGELOG
|
|||||||
```
|
```
|
||||||
- To disable multi-line display, use `--no-multi-line`
|
- To disable multi-line display, use `--no-multi-line`
|
||||||
- The default `--pointer` and `--marker` have been changed from `>` to Unicode bar characters as they look better with multi-line items
|
- The default `--pointer` and `--marker` have been changed from `>` to Unicode bar characters as they look better with multi-line items
|
||||||
|
- Added `--marker-multi-line` to customize the select marker for multi-line entries with the default set to `╻┃╹`
|
||||||
|
```
|
||||||
|
╻First line
|
||||||
|
┃...
|
||||||
|
╹Last line
|
||||||
|
```
|
||||||
- Native `--tmux` integration to replace fzf-tmux script
|
- Native `--tmux` integration to replace fzf-tmux script
|
||||||
```sh
|
```sh
|
||||||
# --tmux [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
# --tmux [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||||
|
@ -455,6 +455,10 @@ Pointer to the current line (default: '▌' or '>' depending on \fB--no-unicode\
|
|||||||
.BI "--marker=" "STR"
|
.BI "--marker=" "STR"
|
||||||
Multi-select marker (default: '┃' or '>' depending on \fB--no-unicode\fR)
|
Multi-select marker (default: '┃' or '>' depending on \fB--no-unicode\fR)
|
||||||
.TP
|
.TP
|
||||||
|
.BI "--marker-multi-line=" "STR"
|
||||||
|
Multi-select marker for multi-line entries. 3 elements for top, middle, and bottom.
|
||||||
|
(default: '╻┃╹' or '.|'' depending on \fB--no-unicode\fR)
|
||||||
|
.TP
|
||||||
.BI "--header=" "STR"
|
.BI "--header=" "STR"
|
||||||
The given string will be printed as the sticky header. The lines are displayed
|
The given string will be printed as the sticky header. The lines are displayed
|
||||||
in the given order from top to bottom regardless of \fB--layout\fR option, and
|
in the given order from top to bottom regardless of \fB--layout\fR option, and
|
||||||
|
295
src/options.go
295
src/options.go
@ -27,136 +27,137 @@ Author: Junegunn Choi <junegunn.c@gmail.com>
|
|||||||
Usage: fzf [options]
|
Usage: fzf [options]
|
||||||
|
|
||||||
Search
|
Search
|
||||||
-x, --extended Extended-search mode
|
-x, --extended Extended-search mode
|
||||||
(enabled by default; +x or --no-extended to disable)
|
(enabled by default; +x or --no-extended to disable)
|
||||||
-e, --exact Enable Exact-match
|
-e, --exact Enable Exact-match
|
||||||
-i, --ignore-case Case-insensitive match (default: smart-case match)
|
-i, --ignore-case Case-insensitive match (default: smart-case match)
|
||||||
+i, --no-ignore-case Case-sensitive match
|
+i, --no-ignore-case Case-sensitive match
|
||||||
--scheme=SCHEME Scoring scheme [default|path|history]
|
--scheme=SCHEME Scoring scheme [default|path|history]
|
||||||
--literal Do not normalize latin script letters before matching
|
--literal Do not normalize latin script letters before matching
|
||||||
-n, --nth=N[,..] Comma-separated list of field index expressions
|
-n, --nth=N[,..] Comma-separated list of field index expressions
|
||||||
for limiting search scope. Each can be a non-zero
|
for limiting search scope. Each can be a non-zero
|
||||||
integer or a range expression ([BEGIN]..[END]).
|
integer or a range expression ([BEGIN]..[END]).
|
||||||
--with-nth=N[,..] Transform the presentation of each line using
|
--with-nth=N[,..] Transform the presentation of each line using
|
||||||
field index expressions
|
field index expressions
|
||||||
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
|
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
|
||||||
+s, --no-sort Do not sort the result
|
+s, --no-sort Do not sort the result
|
||||||
--track Track the current selection when the result is updated
|
--track Track the current selection when the result is updated
|
||||||
--tac Reverse the order of the input
|
--tac Reverse the order of the input
|
||||||
--disabled Do not perform search
|
--disabled Do not perform search
|
||||||
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
|
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
|
||||||
when the scores are tied [length|chunk|begin|end|index]
|
when the scores are tied [length|chunk|begin|end|index]
|
||||||
(default: length)
|
(default: length)
|
||||||
|
|
||||||
Interface
|
Interface
|
||||||
-m, --multi[=MAX] Enable multi-select with tab/shift-tab
|
-m, --multi[=MAX] Enable multi-select with tab/shift-tab
|
||||||
--no-mouse Disable mouse
|
--no-mouse Disable mouse
|
||||||
--bind=KEYBINDS Custom key bindings. Refer to the man page.
|
--bind=KEYBINDS Custom key bindings. Refer to the man page.
|
||||||
--cycle Enable cyclic scroll
|
--cycle Enable cyclic scroll
|
||||||
--no-multi-line Disable multi-line display of items when using --read0
|
--no-multi-line Disable multi-line display of items when using --read0
|
||||||
--keep-right Keep the right end of the line visible on overflow
|
--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
|
--scroll-off=LINES Number of screen lines to keep above or below when
|
||||||
scrolling to the top or to the bottom (default: 0)
|
scrolling to the top or to the bottom (default: 0)
|
||||||
--no-hscroll Disable horizontal scroll
|
--no-hscroll Disable horizontal scroll
|
||||||
--hscroll-off=COLS Number of screen columns to keep to the right of the
|
--hscroll-off=COLS Number of screen columns to keep to the right of the
|
||||||
highlighted substring (default: 10)
|
highlighted substring (default: 10)
|
||||||
--filepath-word Make word-wise movements respect path separators
|
--filepath-word Make word-wise movements respect path separators
|
||||||
--jump-labels=CHARS Label characters for jump mode
|
--jump-labels=CHARS Label characters for jump mode
|
||||||
|
|
||||||
Layout
|
Layout
|
||||||
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given
|
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given
|
||||||
height instead of using fullscreen.
|
height instead of using fullscreen.
|
||||||
A negative value is calculated as the terminal height
|
A negative value is calculated as the terminal height
|
||||||
minus the given value.
|
minus the given value.
|
||||||
If prefixed with '~', fzf will determine the height
|
If prefixed with '~', fzf will determine the height
|
||||||
according to the input size.
|
according to the input size.
|
||||||
--min-height=HEIGHT Minimum height when --height is given in percent
|
--min-height=HEIGHT Minimum height when --height is given in percent
|
||||||
(default: 10)
|
(default: 10)
|
||||||
--tmux=OPTS Start fzf in a tmux popup (requires tmux 3.3+)
|
--tmux=OPTS Start fzf in a tmux popup (requires tmux 3.3+)
|
||||||
[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||||
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
|
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
|
||||||
--border[=STYLE] Draw border around the finder
|
--border[=STYLE] Draw border around the finder
|
||||||
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
|
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
|
||||||
top|bottom|left|right|none] (default: rounded)
|
top|bottom|left|right|none] (default: rounded)
|
||||||
--border-label=LABEL Label to print on the border
|
--border-label=LABEL Label to print on the border
|
||||||
--border-label-pos=COL Position of the border label
|
--border-label-pos=COL Position of the border label
|
||||||
[POSITIVE_INTEGER: columns from left|
|
[POSITIVE_INTEGER: columns from left|
|
||||||
NEGATIVE_INTEGER: columns from right][:bottom]
|
NEGATIVE_INTEGER: columns from right][:bottom]
|
||||||
(default: 0 or center)
|
(default: 0 or center)
|
||||||
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
|
--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)
|
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
|
||||||
--info=STYLE Finder info style
|
--info=STYLE Finder info style
|
||||||
[default|right|hidden|inline[-right][:PREFIX]]
|
[default|right|hidden|inline[-right][:PREFIX]]
|
||||||
--separator=STR String to form horizontal separator on info line
|
--separator=STR String to form horizontal separator on info line
|
||||||
--no-separator Hide info line separator
|
--no-separator Hide info line separator
|
||||||
--scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window)
|
--scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window)
|
||||||
--no-scrollbar Hide scrollbar
|
--no-scrollbar Hide scrollbar
|
||||||
--prompt=STR Input prompt (default: '> ')
|
--prompt=STR Input prompt (default: '> ')
|
||||||
--pointer=STR Pointer to the current line (default: '▌' or '>')
|
--pointer=STR Pointer to the current line (default: '▌' or '>')
|
||||||
--marker=STR Multi-select marker (default: '┃' or '>')
|
--marker=STR Multi-select marker (default: '┃' or '>')
|
||||||
--header=STR String to print as header
|
--marker-multi-line=STR Multi-select marker for multi-line entries;
|
||||||
--header-lines=N The first N lines of the input are treated as header
|
3 elements for top, middle, and bottom (default: '╻┃╹')
|
||||||
--header-first Print header before the prompt line
|
--header=STR String to print as header
|
||||||
--ellipsis=STR Ellipsis to show when line is truncated (default: '..')
|
--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: '..')
|
||||||
|
|
||||||
Display
|
Display
|
||||||
--ansi Enable processing of ANSI color codes
|
--ansi Enable processing of ANSI color codes
|
||||||
--tabstop=SPACES Number of spaces for a tab character (default: 8)
|
--tabstop=SPACES Number of spaces for a tab character (default: 8)
|
||||||
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
|
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
|
||||||
--highlight-line Highlight the whole current line
|
--highlight-line Highlight the whole current line
|
||||||
--no-bold Do not use bold text
|
--no-bold Do not use bold text
|
||||||
|
|
||||||
History
|
History
|
||||||
--history=FILE History file
|
--history=FILE History file
|
||||||
--history-size=N Maximum number of history entries (default: 1000)
|
--history-size=N Maximum number of history entries (default: 1000)
|
||||||
|
|
||||||
Preview
|
Preview
|
||||||
--preview=COMMAND Command to preview highlighted line ({})
|
--preview=COMMAND Command to preview highlighted line ({})
|
||||||
--preview-window=OPT Preview window layout (default: right:50%)
|
--preview-window=OPT Preview window layout (default: right:50%)
|
||||||
[up|down|left|right][,SIZE[%]]
|
[up|down|left|right][,SIZE[%]]
|
||||||
[,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden]
|
[,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden]
|
||||||
[,border-BORDER_OPT]
|
[,border-BORDER_OPT]
|
||||||
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
|
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
|
||||||
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
|
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
|
||||||
--preview-label=LABEL
|
--preview-label=LABEL
|
||||||
--preview-label-pos=N Same as --border-label and --border-label-pos,
|
--preview-label-pos=N Same as --border-label and --border-label-pos,
|
||||||
but for preview window
|
but for preview window
|
||||||
|
|
||||||
Scripting
|
Scripting
|
||||||
-q, --query=STR Start the finder with the given query
|
-q, --query=STR Start the finder with the given query
|
||||||
-1, --select-1 Automatically select the only match
|
-1, --select-1 Automatically select the only match
|
||||||
-0, --exit-0 Exit immediately when there's no match
|
-0, --exit-0 Exit immediately when there's no match
|
||||||
-f, --filter=STR Filter mode. Do not start interactive finder.
|
-f, --filter=STR Filter mode. Do not start interactive finder.
|
||||||
--print-query Print query as the first line
|
--print-query Print query as the first line
|
||||||
--expect=KEYS Comma-separated list of keys to complete fzf
|
--expect=KEYS Comma-separated list of keys to complete fzf
|
||||||
--read0 Read input delimited by ASCII NUL characters
|
--read0 Read input delimited by ASCII NUL characters
|
||||||
--print0 Print output delimited by ASCII NUL characters
|
--print0 Print output delimited by ASCII NUL characters
|
||||||
--sync Synchronous search for multi-staged filtering
|
--sync Synchronous search for multi-staged filtering
|
||||||
--with-shell=STR Shell command and flags to start child processes with
|
--with-shell=STR Shell command and flags to start child processes with
|
||||||
--listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
|
--listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
|
||||||
(To allow remote process execution, use --listen-unsafe)
|
(To allow remote process execution, use --listen-unsafe)
|
||||||
|
|
||||||
Directory traversal (Only used when $FZF_DEFAULT_COMMAND is not set)
|
Directory traversal (Only used when $FZF_DEFAULT_COMMAND is not set)
|
||||||
--walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden)
|
--walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden)
|
||||||
--walker-root=DIR Root directory from which to start walker (default: .)
|
--walker-root=DIR Root directory from which to start walker (default: .)
|
||||||
--walker-skip=DIRS Comma-separated list of directory names to skip
|
--walker-skip=DIRS Comma-separated list of directory names to skip
|
||||||
(default: .git,node_modules)
|
(default: .git,node_modules)
|
||||||
|
|
||||||
Shell integration
|
Shell integration
|
||||||
--bash Print script to set up Bash shell integration
|
--bash Print script to set up Bash shell integration
|
||||||
--zsh Print script to set up Zsh shell integration
|
--zsh Print script to set up Zsh shell integration
|
||||||
--fish Print script to set up Fish shell integration
|
--fish Print script to set up Fish shell integration
|
||||||
|
|
||||||
Help
|
Help
|
||||||
--version Display version information and exit
|
--version Display version information and exit
|
||||||
--help Show this message
|
--help Show this message
|
||||||
--man Show man page
|
--man Show man page
|
||||||
|
|
||||||
Environment variables
|
Environment variables
|
||||||
FZF_DEFAULT_COMMAND Default command to use when input is tty
|
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 Default options (e.g. '--layout=reverse --info=inline')
|
||||||
FZF_DEFAULT_OPTS_FILE Location of the file to read default options from
|
FZF_DEFAULT_OPTS_FILE Location of the file to read default options from
|
||||||
FZF_API_KEY X-API-Key header for HTTP server (--listen)
|
FZF_API_KEY X-API-Key header for HTTP server (--listen)
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -438,6 +439,7 @@ type Options struct {
|
|||||||
Prompt string
|
Prompt string
|
||||||
Pointer *string
|
Pointer *string
|
||||||
Marker *string
|
Marker *string
|
||||||
|
MarkerMulti [3]string
|
||||||
Query string
|
Query string
|
||||||
Select1 bool
|
Select1 bool
|
||||||
Exit0 bool
|
Exit0 bool
|
||||||
@ -1847,6 +1849,37 @@ func parseMargin(opt string, margin string) ([4]sizeSpec, error) {
|
|||||||
return [4]sizeSpec{}, errors.New("invalid " + opt + ": " + margin)
|
return [4]sizeSpec{}, errors.New("invalid " + opt + ": " + margin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseMarkerMultiLine(str string) ([3]string, error) {
|
||||||
|
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: 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 parseOptions(opts *Options, allArgs []string) error {
|
func parseOptions(opts *Options, allArgs []string) error {
|
||||||
var err error
|
var err error
|
||||||
var historyMax int
|
var historyMax int
|
||||||
@ -2186,19 +2219,27 @@ func parseOptions(opts *Options, allArgs []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "--pointer":
|
case "--pointer":
|
||||||
str, err := nextString(allArgs, &i, "pointer sign string required")
|
str, err := nextString(allArgs, &i, "pointer sign required")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
str = firstLine(str)
|
str = firstLine(str)
|
||||||
opts.Pointer = &str
|
opts.Pointer = &str
|
||||||
case "--marker":
|
case "--marker":
|
||||||
str, err := nextString(allArgs, &i, "selected sign string required")
|
str, err := nextString(allArgs, &i, "marker sign required")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
str = firstLine(str)
|
str = firstLine(str)
|
||||||
opts.Marker = &str
|
opts.Marker = &str
|
||||||
|
case "--marker-multi-line":
|
||||||
|
str, err := nextString(allArgs, &i, "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":
|
case "--sync":
|
||||||
opts.Sync = true
|
opts.Sync = true
|
||||||
case "--no-sync", "--async":
|
case "--no-sync", "--async":
|
||||||
@ -2439,6 +2480,10 @@ func parseOptions(opts *Options, allArgs []string) error {
|
|||||||
} else if match, value := optString(arg, "--marker="); match {
|
} else if match, value := optString(arg, "--marker="); match {
|
||||||
str := firstLine(value)
|
str := firstLine(value)
|
||||||
opts.Marker = &str
|
opts.Marker = &str
|
||||||
|
} else if match, value := optString(arg, "--marker-multi-line="); match {
|
||||||
|
if opts.MarkerMulti, err = parseMarkerMultiLine(firstLine(value)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} else if match, value := optString(arg, "-n", "--nth="); match {
|
} else if match, value := optString(arg, "-n", "--nth="); match {
|
||||||
if opts.Nth, err = splitNth(value); err != nil {
|
if opts.Nth, err = splitNth(value); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -2680,13 +2725,35 @@ func postProcessOptions(opts *Options) error {
|
|||||||
opts.Pointer = &defaultPointer
|
opts.Pointer = &defaultPointer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markerLen := 1
|
||||||
if opts.Marker == nil {
|
if opts.Marker == nil {
|
||||||
// "▏" looks better, but not all terminals render it correctly
|
// "▎" looks better, but not all terminals render it correctly
|
||||||
defaultMarker := "┃"
|
defaultMarker := "┃"
|
||||||
if !opts.Unicode {
|
if !opts.Unicode {
|
||||||
defaultMarker = ">"
|
defaultMarker = ">"
|
||||||
}
|
}
|
||||||
opts.Marker = &defaultMarker
|
opts.Marker = &defaultMarker
|
||||||
|
} else {
|
||||||
|
markerLen = uniseg.StringWidth(*opts.Marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
markerMultiLen := 1
|
||||||
|
if len(opts.MarkerMulti[0]) == 0 {
|
||||||
|
if opts.Unicode {
|
||||||
|
opts.MarkerMulti = [3]string{"╻", "┃", "╹"}
|
||||||
|
} else {
|
||||||
|
opts.MarkerMulti = [3]string{".", "|", "'"}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
markerMultiLen = uniseg.StringWidth(opts.MarkerMulti[0])
|
||||||
|
}
|
||||||
|
if markerMultiLen > markerLen {
|
||||||
|
padded := *opts.Marker + " "
|
||||||
|
opts.Marker = &padded
|
||||||
|
} else if markerMultiLen < markerLen {
|
||||||
|
for idx := range opts.MarkerMulti {
|
||||||
|
opts.MarkerMulti[idx] += " "
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default actions for CTRL-N / CTRL-P when --history is set
|
// Default actions for CTRL-N / CTRL-P when --history is set
|
||||||
|
@ -177,6 +177,15 @@ type fitpad struct {
|
|||||||
|
|
||||||
type labelPrinter func(tui.Window, int)
|
type labelPrinter func(tui.Window, int)
|
||||||
|
|
||||||
|
type markerClass int
|
||||||
|
|
||||||
|
const (
|
||||||
|
markerSingle markerClass = iota
|
||||||
|
markerTop
|
||||||
|
markerMiddle
|
||||||
|
markerBottom
|
||||||
|
)
|
||||||
|
|
||||||
type StatusItem struct {
|
type StatusItem struct {
|
||||||
Index int `json:"index"`
|
Index int `json:"index"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
@ -218,6 +227,7 @@ type Terminal struct {
|
|||||||
marker string
|
marker string
|
||||||
markerLen int
|
markerLen int
|
||||||
markerEmpty string
|
markerEmpty string
|
||||||
|
markerMultiLine [3]string
|
||||||
queryLen [2]int
|
queryLen [2]int
|
||||||
layout layoutType
|
layout layoutType
|
||||||
fullscreen bool
|
fullscreen bool
|
||||||
@ -755,6 +765,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
|||||||
pointerLen: uniseg.StringWidth(*opts.Pointer),
|
pointerLen: uniseg.StringWidth(*opts.Pointer),
|
||||||
marker: *opts.Marker,
|
marker: *opts.Marker,
|
||||||
markerLen: uniseg.StringWidth(*opts.Marker),
|
markerLen: uniseg.StringWidth(*opts.Marker),
|
||||||
|
markerMultiLine: opts.MarkerMulti,
|
||||||
wordRubout: wordRubout,
|
wordRubout: wordRubout,
|
||||||
wordNext: wordNext,
|
wordNext: wordNext,
|
||||||
cx: len(input),
|
cx: len(input),
|
||||||
@ -1084,7 +1095,8 @@ func (t *Terminal) avgNumLines() int {
|
|||||||
offset := util.Max(0, util.Min(t.offset, total-maxItems-1))
|
offset := util.Max(0, util.Min(t.offset, total-maxItems-1))
|
||||||
for idx := 0; idx < maxItems && idx+offset < total; idx++ {
|
for idx := 0; idx < maxItems && idx+offset < total; idx++ {
|
||||||
item := t.merger.Get(idx + offset)
|
item := t.merger.Get(idx + offset)
|
||||||
numLines += item.item.text.NumLines(maxItems)
|
lines, _ := item.item.text.NumLines(maxItems)
|
||||||
|
numLines += lines
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
@ -1884,7 +1896,7 @@ func (t *Terminal) printHeader() {
|
|||||||
|
|
||||||
t.printHighlighted(Result{item: item},
|
t.printHighlighted(Result{item: item},
|
||||||
tui.ColHeader, tui.ColHeader, false, false, line, line, true,
|
tui.ColHeader, tui.ColHeader, false, false, line, line, true,
|
||||||
func(int) { t.window.Print(" ") }, nil)
|
func(markerClass) { t.window.Print(" ") }, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1964,7 +1976,8 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
|
|||||||
if !t.multiLine {
|
if !t.multiLine {
|
||||||
return line
|
return line
|
||||||
}
|
}
|
||||||
return line + item.text.NumLines(maxLine-line+1) - 1
|
lines, _ := item.text.NumLines(maxLine - line + 1)
|
||||||
|
return line + lines - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
|
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
|
||||||
@ -1992,29 +2005,41 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
|
|||||||
}
|
}
|
||||||
|
|
||||||
var finalLineNum int
|
var finalLineNum int
|
||||||
|
markerFor := func(markerClass markerClass) string {
|
||||||
|
marker := t.marker
|
||||||
|
switch markerClass {
|
||||||
|
case markerTop:
|
||||||
|
marker = t.markerMultiLine[0]
|
||||||
|
case markerMiddle:
|
||||||
|
marker = t.markerMultiLine[1]
|
||||||
|
case markerBottom:
|
||||||
|
marker = t.markerMultiLine[2]
|
||||||
|
}
|
||||||
|
return marker
|
||||||
|
}
|
||||||
if current {
|
if current {
|
||||||
preTask := func(lineOffset int) {
|
preTask := func(marker markerClass) {
|
||||||
if len(label) == 0 {
|
if len(label) == 0 {
|
||||||
t.window.CPrint(tui.ColCurrentCursorEmpty, t.pointerEmpty)
|
t.window.CPrint(tui.ColCurrentCursorEmpty, t.pointerEmpty)
|
||||||
} else {
|
} else {
|
||||||
t.window.CPrint(tui.ColCurrentCursor, label)
|
t.window.CPrint(tui.ColCurrentCursor, label)
|
||||||
}
|
}
|
||||||
if selected {
|
if selected {
|
||||||
t.window.CPrint(tui.ColCurrentMarker, t.marker)
|
t.window.CPrint(tui.ColCurrentMarker, markerFor(marker))
|
||||||
} else {
|
} else {
|
||||||
t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty)
|
t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finalLineNum = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true, line, maxLine, forceRedraw, preTask, postTask)
|
finalLineNum = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true, line, maxLine, forceRedraw, preTask, postTask)
|
||||||
} else {
|
} else {
|
||||||
preTask := func(lineOffset int) {
|
preTask := func(marker markerClass) {
|
||||||
if len(label) == 0 {
|
if len(label) == 0 {
|
||||||
t.window.CPrint(tui.ColCursorEmpty, t.pointerEmpty)
|
t.window.CPrint(tui.ColCursorEmpty, t.pointerEmpty)
|
||||||
} else {
|
} else {
|
||||||
t.window.CPrint(tui.ColCursor, label)
|
t.window.CPrint(tui.ColCursor, label)
|
||||||
}
|
}
|
||||||
if selected {
|
if selected {
|
||||||
t.window.CPrint(tui.ColMarker, t.marker)
|
t.window.CPrint(tui.ColMarker, markerFor(marker))
|
||||||
} else {
|
} else {
|
||||||
t.window.Print(t.markerEmpty)
|
t.window.Print(t.markerEmpty)
|
||||||
}
|
}
|
||||||
@ -2070,7 +2095,7 @@ func (t *Terminal) overflow(runes []rune, max int) bool {
|
|||||||
return t.displayWidthWithLimit(runes, 0, max) > max
|
return t.displayWidthWithLimit(runes, 0, max) > max
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool, lineNum int, maxLineNum int, forceRedraw bool, preTask func(int), postTask func(int, int)) int {
|
func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool, lineNum int, maxLineNum int, forceRedraw bool, preTask func(markerClass), postTask func(int, int)) int {
|
||||||
var displayWidth int
|
var displayWidth int
|
||||||
item := result.item
|
item := result.item
|
||||||
matchOffsets := []Offset{}
|
matchOffsets := []Offset{}
|
||||||
@ -2096,12 +2121,16 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
|||||||
finalLineNum := lineNum
|
finalLineNum := lineNum
|
||||||
numItemLines := 1
|
numItemLines := 1
|
||||||
cutoff := 0
|
cutoff := 0
|
||||||
if t.multiLine && t.layout == layoutDefault {
|
overflow := false
|
||||||
|
topCutoff := false
|
||||||
|
if t.multiLine {
|
||||||
maxLines := maxLineNum - lineNum + 1
|
maxLines := maxLineNum - lineNum + 1
|
||||||
numItemLines = item.text.NumLines(maxLines)
|
numItemLines, overflow = item.text.NumLines(maxLines)
|
||||||
// Cut off the upper lines in the 'default' layout
|
// Cut off the upper lines in the 'default' layout
|
||||||
if !current && maxLines == numItemLines {
|
if t.layout == layoutDefault && !current && maxLines == numItemLines && overflow {
|
||||||
cutoff = item.text.NumLines(math.MaxInt32) - maxLines
|
actualLines, _ := item.text.NumLines(math.MaxInt32)
|
||||||
|
cutoff = actualLines - maxLines
|
||||||
|
topCutoff = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for lineOffset := 0; from <= len(text) && (lineNum <= maxLineNum || maxLineNum == 0); lineOffset++ {
|
for lineOffset := 0; from <= len(text) && (lineNum <= maxLineNum || maxLineNum == 0); lineOffset++ {
|
||||||
@ -2151,7 +2180,34 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
|||||||
t.move(actualLineNum, 0, forceRedraw)
|
t.move(actualLineNum, 0, forceRedraw)
|
||||||
|
|
||||||
if preTask != nil {
|
if preTask != nil {
|
||||||
preTask(lineOffset)
|
var marker markerClass
|
||||||
|
if numItemLines == 1 {
|
||||||
|
if !overflow {
|
||||||
|
marker = markerSingle
|
||||||
|
} else if topCutoff {
|
||||||
|
marker = markerBottom
|
||||||
|
} else {
|
||||||
|
marker = markerTop
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if lineOffset == 0 { // First line
|
||||||
|
if topCutoff {
|
||||||
|
marker = markerMiddle
|
||||||
|
} else {
|
||||||
|
marker = markerTop
|
||||||
|
}
|
||||||
|
} else if lineOffset == numItemLines-1 { // Last line
|
||||||
|
if topCutoff || !overflow {
|
||||||
|
marker = markerBottom
|
||||||
|
} else {
|
||||||
|
marker = markerMiddle
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
marker = markerMiddle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preTask(marker)
|
||||||
}
|
}
|
||||||
|
|
||||||
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
|
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
|
||||||
@ -4502,7 +4558,7 @@ func (t *Terminal) constrain() {
|
|||||||
linesSum := 0
|
linesSum := 0
|
||||||
|
|
||||||
add := func(i int) bool {
|
add := func(i int) bool {
|
||||||
lines := t.merger.Get(i).item.text.NumLines(numItems - linesSum)
|
lines, _ := t.merger.Get(i).item.text.NumLines(numItems - linesSum)
|
||||||
linesSum += lines
|
linesSum += lines
|
||||||
if linesSum >= numItems {
|
if linesSum >= numItems {
|
||||||
if numItemsFound == 0 {
|
if numItemsFound == 0 {
|
||||||
@ -4547,13 +4603,14 @@ func (t *Terminal) constrain() {
|
|||||||
numItems := t.merger.Length()
|
numItems := t.merger.Length()
|
||||||
itemLines := 1
|
itemLines := 1
|
||||||
if t.multiLine && t.cy < numItems {
|
if t.multiLine && t.cy < numItems {
|
||||||
itemLines = t.merger.Get(t.cy).item.text.NumLines(maxLines)
|
itemLines, _ = t.merger.Get(t.cy).item.text.NumLines(maxLines)
|
||||||
}
|
}
|
||||||
linesBefore := t.cy - newOffset
|
linesBefore := t.cy - newOffset
|
||||||
if t.multiLine {
|
if t.multiLine {
|
||||||
linesBefore = 0
|
linesBefore = 0
|
||||||
for i := newOffset; i < t.cy && i < numItems; i++ {
|
for i := newOffset; i < t.cy && i < numItems; i++ {
|
||||||
linesBefore += t.merger.Get(i).item.text.NumLines(maxLines - linesBefore - itemLines)
|
lines, _ := t.merger.Get(i).item.text.NumLines(maxLines - linesBefore - itemLines)
|
||||||
|
linesBefore += lines
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
linesAfter := maxLines - (linesBefore + itemLines)
|
linesAfter := maxLines - (linesBefore + itemLines)
|
||||||
|
@ -75,18 +75,18 @@ func (chars *Chars) Bytes() []byte {
|
|||||||
return chars.slice
|
return chars.slice
|
||||||
}
|
}
|
||||||
|
|
||||||
func (chars *Chars) NumLines(atMost int) int {
|
func (chars *Chars) NumLines(atMost int) (int, bool) {
|
||||||
lines := 1
|
lines := 1
|
||||||
if runes := chars.optionalRunes(); runes != nil {
|
if runes := chars.optionalRunes(); runes != nil {
|
||||||
for _, r := range runes {
|
for _, r := range runes {
|
||||||
if r == '\n' {
|
if r == '\n' {
|
||||||
lines++
|
lines++
|
||||||
}
|
}
|
||||||
if lines >= atMost {
|
if lines > atMost {
|
||||||
return atMost
|
return atMost, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return lines
|
return lines, false
|
||||||
}
|
}
|
||||||
|
|
||||||
for idx := 0; idx < len(chars.slice); idx++ {
|
for idx := 0; idx < len(chars.slice); idx++ {
|
||||||
@ -97,11 +97,11 @@ func (chars *Chars) NumLines(atMost int) int {
|
|||||||
|
|
||||||
idx += found
|
idx += found
|
||||||
lines++
|
lines++
|
||||||
if lines >= atMost {
|
if lines > atMost {
|
||||||
return atMost
|
return atMost, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return lines
|
return lines, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (chars *Chars) optionalRunes() []rune {
|
func (chars *Chars) optionalRunes() []rune {
|
||||||
|
@ -2752,8 +2752,9 @@ class TestGoFZF < TestBase
|
|||||||
|
|
||||||
def assert_block(expected, lines)
|
def assert_block(expected, lines)
|
||||||
cols = expected.lines.map(&:chomp).map(&:length).max
|
cols = expected.lines.map(&:chomp).map(&:length).max
|
||||||
actual = lines.reverse.take(expected.lines.length).reverse.map { _1[0, cols].rstrip + "\n" }.join
|
top = lines.take(expected.lines.length).map { _1[0, cols].rstrip + "\n" }.join
|
||||||
assert_equal_org expected, actual
|
bottom = lines.reverse.take(expected.lines.length).reverse.map { _1[0, cols].rstrip + "\n" }.join
|
||||||
|
assert_includes [top, bottom], expected
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_height_range_fit
|
def test_height_range_fit
|
||||||
@ -3268,6 +3269,55 @@ class TestGoFZF < TestBase
|
|||||||
tmux.send_keys '99'
|
tmux.send_keys '99'
|
||||||
tmux.until { |lines| assert(lines.any? { |line| line.include?('0 / 0') }) }
|
tmux.until { |lines| assert(lines.any? { |line| line.include?('0 / 0') }) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_fzf_multi_line
|
||||||
|
tmux.send_keys %[(echo -en '0\\0'; echo -en '1\\n2\\0'; seq 1000) | fzf --read0 --multi --bind load:select-all --border rounded], :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ ┃998
|
||||||
|
│ ┃999
|
||||||
|
│ ┃1000
|
||||||
|
│ ╹
|
||||||
|
│ ╻1
|
||||||
|
│ ╹2
|
||||||
|
│ >>0
|
||||||
|
│ 3/3 (3)
|
||||||
|
│ >
|
||||||
|
╰───────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, _1) }
|
||||||
|
tmux.send_keys :Up, :Up
|
||||||
|
block = <<~BLOCK
|
||||||
|
╭───────
|
||||||
|
│ >╻1
|
||||||
|
│ >┃2
|
||||||
|
│ >┃3
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, _1) }
|
||||||
|
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ >┃
|
||||||
|
│
|
||||||
|
│ >
|
||||||
|
╰───
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, _1) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_fzf_multi_line_reverse
|
||||||
|
tmux.send_keys %[(echo -en '0\\0'; echo -en '1\\n2\\0'; seq 1000) | fzf --read0 --multi --bind load:select-all --border rounded --reverse], :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
╭───────────
|
||||||
|
│ >
|
||||||
|
│ 3/3 (3)
|
||||||
|
│ >>0
|
||||||
|
│ ╻1
|
||||||
|
│ ╹2
|
||||||
|
│ ╻1
|
||||||
|
│ ┃2
|
||||||
|
│ ┃3
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, _1) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module TestShell
|
module TestShell
|
||||||
|
Loading…
x
Reference in New Issue
Block a user