summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--man/man1/fzf.125
-rw-r--r--shell/completion.bash3
-rw-r--r--src/options.go64
-rw-r--r--src/terminal.go127
-rw-r--r--test/test_go.rb12
6 files changed, 196 insertions, 36 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3f411708..5bd791fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ CHANGELOG
### New features
+- Added `--margin` option
- Added options for sticky header
- `--header-file`
- `--header-lines`
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 0c6b375e..1448eb95 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -131,6 +131,31 @@ Use black background
.B "--reverse"
Reverse orientation
.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"
Enable cyclic scroll
.TP
diff --git a/shell/completion.bash b/shell/completion.bash
index abe3363c..63de5463 100644
--- a/shell/completion.bash
+++ b/shell/completion.bash
@@ -50,7 +50,8 @@ _fzf_opts_completion() {
--history
--history-size
--header-file
- --header-lines"
+ --header-lines
+ --margin"
case "${prev}" in
--tiebreak)
diff --git a/src/options.go b/src/options.go
index 983a7d3d..b2360a10 100644
--- a/src/options.go
+++ b/src/options.go
@@ -38,6 +38,7 @@ const usage = `usage: fzf [options]
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
--black Use black background
--reverse Reverse orientation
+ --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--cycle Enable cyclic scroll
--no-hscroll Disable horizontal scroll
--inline-info Display finder info inline with the query
@@ -93,6 +94,10 @@ const (
byIndex
)
+func defaultMargin() [4]string {
+ return [4]string{"0", "0", "0", "0"}
+}
+
// Options stores the values of command-line options
type Options struct {
Mode Mode
@@ -127,6 +132,7 @@ type Options struct {
History *History
Header []string
HeaderLines int
+ Margin [4]string
Version bool
}
@@ -171,6 +177,7 @@ func defaultOptions() *Options {
History: nil,
Header: make([]string, 0),
HeaderLines: 0,
+ Margin: defaultMargin(),
Version: false}
}
@@ -218,6 +225,14 @@ func atoi(str string) int {
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 {
if len(args) > *i+1 {
*i++
@@ -592,6 +607,48 @@ func readHeaderFile(filename string) []string {
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) {
keymap := make(map[int]actionType)
var historyMax int
@@ -743,6 +800,11 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Header = []string{}
opts.HeaderLines = atoi(
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":
opts.Version = true
default:
@@ -782,6 +844,8 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--header-lines="); match {
opts.Header = []string{}
opts.HeaderLines = atoi(value)
+ } else if match, value := optString(arg, "--margin="); match {
+ opts.Margin = parseMargin(value)
} else {
errorExit("unknown option: " + arg)
}
diff --git a/src/terminal.go b/src/terminal.go
index 18b37d5a..070d0a90 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -8,6 +8,7 @@ import (
"os/signal"
"regexp"
"sort"
+ "strconv"
"strings"
"sync"
"syscall"
@@ -41,6 +42,8 @@ type Terminal struct {
history *History
cycle bool
header []string
+ margin [4]string
+ marginInt [4]int
count int
progress int
reading bool
@@ -200,6 +203,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
pressed: "",
printQuery: opts.PrintQuery,
history: opts.History,
+ margin: opts.Margin,
+ marginInt: [4]int{0, 0, 0, 0},
cycle: opts.Cycle,
header: opts.Header,
reading: true,
@@ -317,10 +322,50 @@ func displayWidth(runes []rune) int {
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) {
+ x += t.marginInt[3]
maxy := C.MaxY()
if !t.reverse {
- y = maxy - y - 1
+ y = maxy - y - 1 - t.marginInt[2]
+ } else {
+ y += t.marginInt[0]
}
if clear {
@@ -375,11 +420,15 @@ func (t *Terminal) printInfo() {
C.CPrint(C.ColInfo, false, output)
}
+func (t *Terminal) maxHeight() int {
+ return C.MaxY() - t.marginInt[0] - t.marginInt[2]
+}
+
func (t *Terminal) printHeader() {
if len(t.header) == 0 {
return
}
- max := C.MaxY()
+ max := t.maxHeight()
var state *ansiState
for idx, lineStr := range t.header {
if !t.reverse {
@@ -490,7 +539,7 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
// Overflow
text := []rune(*item.text)
offsets := item.colorOffsets(col2, bold, current)
- maxWidth := C.MaxX() - 3
+ maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3]
fullWidth := displayWidth(text)
if fullWidth > maxWidth {
if t.hscroll {
@@ -573,6 +622,7 @@ func processTabs(runes []rune, prefixWidth int) (string, int) {
}
func (t *Terminal) printAll() {
+ t.calculateMargins()
t.printList()
t.printPrompt()
t.printInfo()
@@ -652,6 +702,7 @@ func (t *Terminal) Loop() {
{ // Late initialization
t.mutex.Lock()
t.initFunc()
+ t.calculateMargins()
t.printPrompt()
t.placeCursor()
C.Refresh()
@@ -942,40 +993,46 @@ func (t *Terminal) Loop() {
}
case actMouse:
me := event.MouseEvent
- mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y
- if !t.reverse {
- my = C.MaxY() - my - 1
- }
- min := 2 + len(t.header)
- if t.inlineInfo {
- min -= 1
- }
- if me.S != 0 {
- // Scroll
- if t.merger.Length() > 0 {
- if t.multi && me.Mod {
- toggle()
- }
- t.vmove(me.S)
- req(reqList)
+ mx, my := me.X, me.Y
+ if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] &&
+ my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] {
+ mx -= t.marginInt[3]
+ my -= t.marginInt[0]
+ mx = util.Constrain(mx-len(t.prompt), 0, len(t.input))
+ if !t.reverse {
+ my = t.maxHeight() - my - 1
}
- } else if me.Double {
- // Double-click
- if my >= min {
- if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
- req(reqClose)
- }
+ min := 2 + len(t.header)
+ if t.inlineInfo {
+ min -= 1
}
- } 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()
+ 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 {
+ // 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 {
- max := C.MaxY() - 2 - len(t.header)
+ max := t.maxHeight() - 2 - len(t.header)
if t.inlineInfo {
max += 1
}
diff --git a/test/test_go.rb b/test/test_go.rb
index ad2150e0..f702efcb 100644
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -731,6 +731,18 @@ class TestGoFZF < TestBase
tmux.prepare
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
def writelines path, lines
File.unlink path while File.exists? path