Add --margin option

Close #299
This commit is contained in:
Junegunn Choi 2015-07-26 23:02:04 +09:00
parent 2bebddefc0
commit ee0c8a2635
6 changed files with 196 additions and 36 deletions

View File

@ -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`

View File

@ -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

View File

@ -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)

View File

@ -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)
} }

View File

@ -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
} }

View File

@ -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