mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-06-03 07:50:49 +00:00
parent
2bebddefc0
commit
ee0c8a2635
|
@ -6,6 +6,7 @@ CHANGELOG
|
||||||
|
|
||||||
### New features
|
### New features
|
||||||
|
|
||||||
|
- Added `--margin` option
|
||||||
- Added options for sticky header
|
- Added options for sticky header
|
||||||
- `--header-file`
|
- `--header-file`
|
||||||
- `--header-lines`
|
- `--header-lines`
|
||||||
|
|
|
@ -131,6 +131,31 @@ Use black background
|
||||||
.B "--reverse"
|
.B "--reverse"
|
||||||
Reverse orientation
|
Reverse orientation
|
||||||
.TP
|
.TP
|
||||||
|
.BI "--margin=" MARGIN
|
||||||
|
Comma-separated expression for margins around the finder.
|
||||||
|
.br
|
||||||
|
.R ""
|
||||||
|
.br
|
||||||
|
.RS
|
||||||
|
.BR TRBL " Same margin for top, right, bottom, and left"
|
||||||
|
.br
|
||||||
|
.BR TB,RL " Vertical, horizontal margin"
|
||||||
|
.br
|
||||||
|
.BR T,RL,B " Top, horizontal, bottom margin"
|
||||||
|
.br
|
||||||
|
.BR T,R,B,L " Top, right, bottom, left margin"
|
||||||
|
.br
|
||||||
|
.R ""
|
||||||
|
.br
|
||||||
|
Each part can be given in absolute number or in percentage relative to the
|
||||||
|
terminal size with \fB%\fR suffix.
|
||||||
|
.br
|
||||||
|
.R ""
|
||||||
|
.br
|
||||||
|
e.g. \fBfzf --margin 10%\fR
|
||||||
|
\fBfzf --margin 1,5%\fR
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
.B "--cycle"
|
.B "--cycle"
|
||||||
Enable cyclic scroll
|
Enable cyclic scroll
|
||||||
.TP
|
.TP
|
||||||
|
|
|
@ -50,7 +50,8 @@ _fzf_opts_completion() {
|
||||||
--history
|
--history
|
||||||
--history-size
|
--history-size
|
||||||
--header-file
|
--header-file
|
||||||
--header-lines"
|
--header-lines
|
||||||
|
--margin"
|
||||||
|
|
||||||
case "${prev}" in
|
case "${prev}" in
|
||||||
--tiebreak)
|
--tiebreak)
|
||||||
|
|
|
@ -38,6 +38,7 @@ const usage = `usage: fzf [options]
|
||||||
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
|
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
|
||||||
--black Use black background
|
--black Use black background
|
||||||
--reverse Reverse orientation
|
--reverse Reverse orientation
|
||||||
|
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
|
||||||
--cycle Enable cyclic scroll
|
--cycle Enable cyclic scroll
|
||||||
--no-hscroll Disable horizontal scroll
|
--no-hscroll Disable horizontal scroll
|
||||||
--inline-info Display finder info inline with the query
|
--inline-info Display finder info inline with the query
|
||||||
|
@ -93,6 +94,10 @@ const (
|
||||||
byIndex
|
byIndex
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func defaultMargin() [4]string {
|
||||||
|
return [4]string{"0", "0", "0", "0"}
|
||||||
|
}
|
||||||
|
|
||||||
// Options stores the values of command-line options
|
// Options stores the values of command-line options
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Mode Mode
|
Mode Mode
|
||||||
|
@ -127,6 +132,7 @@ type Options struct {
|
||||||
History *History
|
History *History
|
||||||
Header []string
|
Header []string
|
||||||
HeaderLines int
|
HeaderLines int
|
||||||
|
Margin [4]string
|
||||||
Version bool
|
Version bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,6 +177,7 @@ func defaultOptions() *Options {
|
||||||
History: nil,
|
History: nil,
|
||||||
Header: make([]string, 0),
|
Header: make([]string, 0),
|
||||||
HeaderLines: 0,
|
HeaderLines: 0,
|
||||||
|
Margin: defaultMargin(),
|
||||||
Version: false}
|
Version: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,6 +225,14 @@ func atoi(str string) int {
|
||||||
return num
|
return num
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func atof(str string) float64 {
|
||||||
|
num, err := strconv.ParseFloat(str, 64)
|
||||||
|
if err != nil {
|
||||||
|
errorExit("not a valid number: " + str)
|
||||||
|
}
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
func nextInt(args []string, i *int, message string) int {
|
func nextInt(args []string, i *int, message string) int {
|
||||||
if len(args) > *i+1 {
|
if len(args) > *i+1 {
|
||||||
*i++
|
*i++
|
||||||
|
@ -592,6 +607,48 @@ func readHeaderFile(filename string) []string {
|
||||||
return strings.Split(strings.TrimSuffix(string(content), "\n"), "\n")
|
return strings.Split(strings.TrimSuffix(string(content), "\n"), "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseMargin(margin string) [4]string {
|
||||||
|
margins := strings.Split(margin, ",")
|
||||||
|
checked := func(str string) string {
|
||||||
|
if strings.HasSuffix(str, "%") {
|
||||||
|
val := atof(str[:len(str)-1])
|
||||||
|
if val < 0 {
|
||||||
|
errorExit("margin must be non-negative")
|
||||||
|
}
|
||||||
|
if val > 100 {
|
||||||
|
errorExit("margin too large")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val := atoi(str)
|
||||||
|
if val < 0 {
|
||||||
|
errorExit("margin must be non-negative")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
switch len(margins) {
|
||||||
|
case 1:
|
||||||
|
m := checked(margins[0])
|
||||||
|
return [4]string{m, m, m, m}
|
||||||
|
case 2:
|
||||||
|
tb := checked(margins[0])
|
||||||
|
rl := checked(margins[1])
|
||||||
|
return [4]string{tb, rl, tb, rl}
|
||||||
|
case 3:
|
||||||
|
t := checked(margins[0])
|
||||||
|
rl := checked(margins[1])
|
||||||
|
b := checked(margins[2])
|
||||||
|
return [4]string{t, rl, b, rl}
|
||||||
|
case 4:
|
||||||
|
return [4]string{
|
||||||
|
checked(margins[0]), checked(margins[1]),
|
||||||
|
checked(margins[2]), checked(margins[3])}
|
||||||
|
default:
|
||||||
|
errorExit("invalid margin: " + margin)
|
||||||
|
}
|
||||||
|
return defaultMargin()
|
||||||
|
}
|
||||||
|
|
||||||
func parseOptions(opts *Options, allArgs []string) {
|
func parseOptions(opts *Options, allArgs []string) {
|
||||||
keymap := make(map[int]actionType)
|
keymap := make(map[int]actionType)
|
||||||
var historyMax int
|
var historyMax int
|
||||||
|
@ -743,6 +800,11 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||||
opts.Header = []string{}
|
opts.Header = []string{}
|
||||||
opts.HeaderLines = atoi(
|
opts.HeaderLines = atoi(
|
||||||
nextString(allArgs, &i, "number of header lines required"))
|
nextString(allArgs, &i, "number of header lines required"))
|
||||||
|
case "--no-margin":
|
||||||
|
opts.Margin = defaultMargin()
|
||||||
|
case "--margin":
|
||||||
|
opts.Margin = parseMargin(
|
||||||
|
nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
|
||||||
case "--version":
|
case "--version":
|
||||||
opts.Version = true
|
opts.Version = true
|
||||||
default:
|
default:
|
||||||
|
@ -782,6 +844,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||||
} else if match, value := optString(arg, "--header-lines="); match {
|
} else if match, value := optString(arg, "--header-lines="); match {
|
||||||
opts.Header = []string{}
|
opts.Header = []string{}
|
||||||
opts.HeaderLines = atoi(value)
|
opts.HeaderLines = atoi(value)
|
||||||
|
} else if match, value := optString(arg, "--margin="); match {
|
||||||
|
opts.Margin = parseMargin(value)
|
||||||
} else {
|
} else {
|
||||||
errorExit("unknown option: " + arg)
|
errorExit("unknown option: " + arg)
|
||||||
}
|
}
|
||||||
|
|
127
src/terminal.go
127
src/terminal.go
|
@ -8,6 +8,7 @@ import (
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
@ -41,6 +42,8 @@ type Terminal struct {
|
||||||
history *History
|
history *History
|
||||||
cycle bool
|
cycle bool
|
||||||
header []string
|
header []string
|
||||||
|
margin [4]string
|
||||||
|
marginInt [4]int
|
||||||
count int
|
count int
|
||||||
progress int
|
progress int
|
||||||
reading bool
|
reading bool
|
||||||
|
@ -200,6 +203,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||||
pressed: "",
|
pressed: "",
|
||||||
printQuery: opts.PrintQuery,
|
printQuery: opts.PrintQuery,
|
||||||
history: opts.History,
|
history: opts.History,
|
||||||
|
margin: opts.Margin,
|
||||||
|
marginInt: [4]int{0, 0, 0, 0},
|
||||||
cycle: opts.Cycle,
|
cycle: opts.Cycle,
|
||||||
header: opts.Header,
|
header: opts.Header,
|
||||||
reading: true,
|
reading: true,
|
||||||
|
@ -317,10 +322,50 @@ func displayWidth(runes []rune) int {
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const minWidth = 16
|
||||||
|
const minHeight = 4
|
||||||
|
|
||||||
|
func (t *Terminal) calculateMargins() {
|
||||||
|
screenWidth := C.MaxX()
|
||||||
|
screenHeight := C.MaxY()
|
||||||
|
for idx, str := range t.margin {
|
||||||
|
if str == "0" {
|
||||||
|
t.marginInt[idx] = 0
|
||||||
|
} else if strings.HasSuffix(str, "%") {
|
||||||
|
num, _ := strconv.ParseFloat(str[:len(str)-1], 64)
|
||||||
|
var val float64
|
||||||
|
if idx%2 == 0 {
|
||||||
|
val = float64(screenHeight)
|
||||||
|
} else {
|
||||||
|
val = float64(screenWidth)
|
||||||
|
}
|
||||||
|
t.marginInt[idx] = int(val * num * 0.01)
|
||||||
|
} else {
|
||||||
|
num, _ := strconv.Atoi(str)
|
||||||
|
t.marginInt[idx] = num
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adjust := func(idx1 int, idx2 int, max int, min int) {
|
||||||
|
if max >= min {
|
||||||
|
margin := t.marginInt[idx1] + t.marginInt[idx2]
|
||||||
|
if max-margin < min {
|
||||||
|
desired := max - min
|
||||||
|
t.marginInt[idx1] = desired * t.marginInt[idx1] / margin
|
||||||
|
t.marginInt[idx2] = desired * t.marginInt[idx2] / margin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adjust(1, 3, screenWidth, minWidth)
|
||||||
|
adjust(0, 2, screenHeight, minHeight)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Terminal) move(y int, x int, clear bool) {
|
func (t *Terminal) move(y int, x int, clear bool) {
|
||||||
|
x += t.marginInt[3]
|
||||||
maxy := C.MaxY()
|
maxy := C.MaxY()
|
||||||
if !t.reverse {
|
if !t.reverse {
|
||||||
y = maxy - y - 1
|
y = maxy - y - 1 - t.marginInt[2]
|
||||||
|
} else {
|
||||||
|
y += t.marginInt[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if clear {
|
if clear {
|
||||||
|
@ -375,11 +420,15 @@ func (t *Terminal) printInfo() {
|
||||||
C.CPrint(C.ColInfo, false, output)
|
C.CPrint(C.ColInfo, false, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) maxHeight() int {
|
||||||
|
return C.MaxY() - t.marginInt[0] - t.marginInt[2]
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Terminal) printHeader() {
|
func (t *Terminal) printHeader() {
|
||||||
if len(t.header) == 0 {
|
if len(t.header) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
max := C.MaxY()
|
max := t.maxHeight()
|
||||||
var state *ansiState
|
var state *ansiState
|
||||||
for idx, lineStr := range t.header {
|
for idx, lineStr := range t.header {
|
||||||
if !t.reverse {
|
if !t.reverse {
|
||||||
|
@ -490,7 +539,7 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
|
||||||
// Overflow
|
// Overflow
|
||||||
text := []rune(*item.text)
|
text := []rune(*item.text)
|
||||||
offsets := item.colorOffsets(col2, bold, current)
|
offsets := item.colorOffsets(col2, bold, current)
|
||||||
maxWidth := C.MaxX() - 3
|
maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3]
|
||||||
fullWidth := displayWidth(text)
|
fullWidth := displayWidth(text)
|
||||||
if fullWidth > maxWidth {
|
if fullWidth > maxWidth {
|
||||||
if t.hscroll {
|
if t.hscroll {
|
||||||
|
@ -573,6 +622,7 @@ func processTabs(runes []rune, prefixWidth int) (string, int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) printAll() {
|
func (t *Terminal) printAll() {
|
||||||
|
t.calculateMargins()
|
||||||
t.printList()
|
t.printList()
|
||||||
t.printPrompt()
|
t.printPrompt()
|
||||||
t.printInfo()
|
t.printInfo()
|
||||||
|
@ -652,6 +702,7 @@ func (t *Terminal) Loop() {
|
||||||
{ // Late initialization
|
{ // Late initialization
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
t.initFunc()
|
t.initFunc()
|
||||||
|
t.calculateMargins()
|
||||||
t.printPrompt()
|
t.printPrompt()
|
||||||
t.placeCursor()
|
t.placeCursor()
|
||||||
C.Refresh()
|
C.Refresh()
|
||||||
|
@ -942,40 +993,46 @@ func (t *Terminal) Loop() {
|
||||||
}
|
}
|
||||||
case actMouse:
|
case actMouse:
|
||||||
me := event.MouseEvent
|
me := event.MouseEvent
|
||||||
mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y
|
mx, my := me.X, me.Y
|
||||||
if !t.reverse {
|
if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] &&
|
||||||
my = C.MaxY() - my - 1
|
my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] {
|
||||||
}
|
mx -= t.marginInt[3]
|
||||||
min := 2 + len(t.header)
|
my -= t.marginInt[0]
|
||||||
if t.inlineInfo {
|
mx = util.Constrain(mx-len(t.prompt), 0, len(t.input))
|
||||||
min -= 1
|
if !t.reverse {
|
||||||
}
|
my = t.maxHeight() - my - 1
|
||||||
if me.S != 0 {
|
|
||||||
// Scroll
|
|
||||||
if t.merger.Length() > 0 {
|
|
||||||
if t.multi && me.Mod {
|
|
||||||
toggle()
|
|
||||||
}
|
|
||||||
t.vmove(me.S)
|
|
||||||
req(reqList)
|
|
||||||
}
|
}
|
||||||
} else if me.Double {
|
min := 2 + len(t.header)
|
||||||
// Double-click
|
if t.inlineInfo {
|
||||||
if my >= min {
|
min -= 1
|
||||||
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
|
|
||||||
req(reqClose)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if me.Down {
|
if me.S != 0 {
|
||||||
if my == 0 && mx >= 0 {
|
// Scroll
|
||||||
// Prompt
|
if t.merger.Length() > 0 {
|
||||||
t.cx = mx
|
if t.multi && me.Mod {
|
||||||
} else if my >= min {
|
toggle()
|
||||||
// List
|
}
|
||||||
if t.vset(t.offset+my-min) && t.multi && me.Mod {
|
t.vmove(me.S)
|
||||||
toggle()
|
req(reqList)
|
||||||
|
}
|
||||||
|
} else if me.Double {
|
||||||
|
// Double-click
|
||||||
|
if my >= min {
|
||||||
|
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
|
||||||
|
req(reqClose)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if me.Down {
|
||||||
|
if my == 0 && mx >= 0 {
|
||||||
|
// Prompt
|
||||||
|
t.cx = mx
|
||||||
|
} else if my >= min {
|
||||||
|
// List
|
||||||
|
if t.vset(t.offset+my-min) && t.multi && me.Mod {
|
||||||
|
toggle()
|
||||||
|
}
|
||||||
|
req(reqList)
|
||||||
}
|
}
|
||||||
req(reqList)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1040,7 +1097,7 @@ func (t *Terminal) vset(o int) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) maxItems() int {
|
func (t *Terminal) maxItems() int {
|
||||||
max := C.MaxY() - 2 - len(t.header)
|
max := t.maxHeight() - 2 - len(t.header)
|
||||||
if t.inlineInfo {
|
if t.inlineInfo {
|
||||||
max += 1
|
max += 1
|
||||||
}
|
}
|
||||||
|
|
|
@ -731,6 +731,18 @@ class TestGoFZF < TestBase
|
||||||
tmux.prepare
|
tmux.prepare
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_margin
|
||||||
|
tmux.send_keys "yes | head -1000 | #{fzf "--margin 5,3"}", :Enter
|
||||||
|
tmux.until { |lines| lines[4] == '' && lines[5] == ' y' }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_margin_reverse
|
||||||
|
tmux.send_keys "seq 1000 | #{fzf "--margin 7,5 --reverse"}", :Enter
|
||||||
|
tmux.until { |lines| lines[1 + 7] == ' 1000/1000' }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def writelines path, lines
|
def writelines path, lines
|
||||||
File.unlink path while File.exists? path
|
File.unlink path while File.exists? path
|
||||||
|
|
Loading…
Reference in New Issue
Block a user