Merge pull request #172 from peco/selectable-layout

Selectable layout
This commit is contained in:
lestrrat 2014-08-18 06:34:25 +09:00
commit f6b6928262
8 changed files with 617 additions and 293 deletions

14
Changes
View file

@ -1,6 +1,20 @@
Changes
=======
v0.2.5
Features
* Add --layout option, which allows you to switch between `top-down`
and `bottom-up` layout mode. This is equivalent of percol's
`--prompt-bottom --result-bottom-up`. Default is `top-down`.
The same option can be specified in the config file as "Layout"
Miscellaneous
* Because of the layout option, SelectNext/SelectPrevious and
SelectNextPage/SelectPreviousPage no longer made sense.
Now all of these are DEPRECATED, and are aliases to different
action names. See the README for the details.
In particular, you would need to configure your key bindings
using these if you want to use the `bottom-up` layout
v0.2.4 - 13 Aug 2014
Features
* Add --initial-matcher command line option to specify which

View file

@ -151,6 +151,10 @@ Specifies the initial matcher to use upon start up. You should specify the name
Specifies the query line's prompt string. When specified, takes precedence over the configuration file's `Prompt` section. The default value is `QUERY>`
### --layout `top-down|bottom-up`
Specifies the display layout. Default is `top-down`, where query prompt is at the top, followed by the list, then the system status message line. `bottom-up` changes this to the list first (displayed in reverse order), the query prompt, and then the system status message line.
Configuration File
==================
@ -177,8 +181,8 @@ Example:
```json
{
"Keymap": {
"M-v": "peco.SelectPreviousPage",
"C-v": "peco.SelectNextPage",
"M-v": "peco.ScrollPageUp",
"C-v": "peco.ScrollPageDown",
"C-x,C-c": "peco.Cancel"
}
}
@ -197,9 +201,9 @@ As of v0.2.1, you can create custom combined actions. For example, if you find y
"Action": {
"foo.SelectFour": [
"peco.ToggleRangeMode",
"peco.SelectNext",
"peco.SelectNext",
"peco.SelectNext",
"peco.SelectDown",
"peco.SelectDown",
"peco.SelectDown",
"peco.ToggleRangeMode"
]
},
@ -265,10 +269,14 @@ Some keys just... don't map correctly / too easily for various reasons. Here, we
| peco.DeleteBackwardWord | Delete one word backward |
| peco.KillEndOfLine | Delete the characters under the cursor until the end of the line |
| peco.DeleteAll | Delete all entered characters |
| peco.SelectPreviousPage | Jumps to previous page |
| peco.SelectNextPage | Jumps to next page|
| peco.SelectPrevious | Selects previous line |
| peco.SelectNext | Selects next line |
| peco.SelectPreviousPage | (DEPRECATED) Alias to ScrollPageUp |
| peco.SelectNextPage | (DEPRECATED) Alias to ScrollPageDown |
| peco.ScrollPageDown | Moves the selected line cursor for an entire page, downwards |
| peco.ScrollPageUp | Moves the selected line cursor for an entire page, upwards |
| peco.SelectUp | Moves the selected line cursor to one line above |
| peco.SelectDown | Moves the selected line cursor to one line below |
| peco.SelectPrevious | (DEPRECATED) Alias to SelectUp |
| peco.SelectNext | (DEPRECATED) Alias to SelectDown |
| peco.ToggleSelection | Selects the current line, and saves it |
| peco.ToggleSelectionAndSelectNext | Selects the current line, saves it, and proceeds to the next line |
| peco.ToggleRangeMode | Start selecting by range, or append selecting range to selections |
@ -398,6 +406,10 @@ Specifies the matcher name to start peco with. You should specify the name of th
Note: `Matcher` key has been deprecated in favor of `InitialMatcher`. `Matcher` will be unavailable in peco 0.3.0
## Layout
See --layout.
Hacking
=======

View file

@ -76,24 +76,30 @@ func init() {
ActionFunc(doKillEndOfLine).Register("KillEndOfLine", termbox.KeyCtrlK)
ActionFunc(doKillBeginningOfLine).Register("KillBeginningOfLine", termbox.KeyCtrlU)
ActionFunc(doRotateMatcher).Register("RotateMatcher", termbox.KeyCtrlR)
ActionFunc(doSelectNext).Register(
"SelectNext",
termbox.KeyArrowDown,
termbox.KeyCtrlN,
)
ActionFunc(doSelectNextPage).Register(
"SelectNextPage",
termbox.KeyArrowRight,
)
ActionFunc(doSelectPrevious).Register(
"SelectPrevious",
termbox.KeyArrowUp,
termbox.KeyCtrlP,
)
ActionFunc(doSelectPreviousPage).Register(
"SelectPreviousPage",
termbox.KeyArrowLeft,
)
ActionFunc(doSelectUp).Register("SelectUp", termbox.KeyArrowUp, termbox.KeyCtrlP)
ActionFunc(func(i *Input, ev termbox.Event) {
i.SendStatusMsg("SelectNext is deprecated. Use SelectUp/SelectDown")
doSelectUp(i, ev)
}).Register("SelectNext")
ActionFunc(doScrollPageDown).Register("ScrollPageDown", termbox.KeyArrowRight)
ActionFunc(func(i *Input, ev termbox.Event) {
i.SendStatusMsg("SelectNextPage is deprecated. Use ScrollPageDown/ScrollPageUp")
doScrollPageDown(i, ev)
}).Register("SelectNextPage")
ActionFunc(doSelectDown).Register("SelectDown", termbox.KeyArrowDown, termbox.KeyCtrlN)
ActionFunc(func(i *Input, ev termbox.Event) {
i.SendStatusMsg("SelectPrevious is deprecated. Use SelectUp/SelectDown")
doSelectDown(i, ev)
}).Register("SelectPrevious")
ActionFunc(doScrollPageUp).Register("ScrollPageUp", termbox.KeyArrowLeft)
ActionFunc(func(i *Input, ev termbox.Event) {
i.SendStatusMsg("SelectPreviousPage is deprecated. Uselect ScrollPageDown/ScrollPageUp")
doScrollPageUp(i, ev)
}).Register("SelectPreviousPage")
ActionFunc(doToggleSelection).Register("ToggleSelection")
ActionFunc(doToggleSelectionAndSelectNext).Register(
@ -248,29 +254,36 @@ func doCancel(i *Input, ev termbox.Event) {
i.ExitWith(1)
}
func doSelectPrevious(i *Input, ev termbox.Event) {
i.SendPaging(ToPrevLine)
func doSelectDown(i *Input, ev termbox.Event) {
i.SendPaging(ToLineBelow)
i.DrawMatches(nil)
}
func doSelectNext(i *Input, ev termbox.Event) {
i.SendPaging(ToNextLine)
func doSelectUp(i *Input, ev termbox.Event) {
i.SendPaging(ToLineAbove)
i.DrawMatches(nil)
}
func doSelectPreviousPage(i *Input, ev termbox.Event) {
i.SendPaging(ToPrevPage)
func doScrollPageUp(i *Input, ev termbox.Event) {
i.SendPaging(ToScrollPageUp)
i.DrawMatches(nil)
}
func doSelectNextPage(i *Input, ev termbox.Event) {
i.SendPaging(ToNextPage)
func doScrollPageDown(i *Input, ev termbox.Event) {
i.SendPaging(ToScrollPageDown)
i.DrawMatches(nil)
}
func doToggleSelectionAndSelectNext(i *Input, ev termbox.Event) {
doToggleSelection(i, ev)
doSelectNext(i, ev)
i.Batch(func() {
doToggleSelection(i, ev)
// XXX This is sucky. Fix later
if i.layoutType == "top-down" {
doSelectDown(i, ev)
} else {
doSelectUp(i, ev)
}
})
}
func doDeleteBackwardWord(i *Input, _ termbox.Event) {

View file

@ -27,6 +27,7 @@ Options:
--initial-index position of the initial index of the selection (0 base)
--initial-matcher specify default matcher
--prompt specify prompt
--layout specify the layout to use. default is 'top-down'
`
os.Stderr.Write([]byte(v))
}
@ -43,6 +44,7 @@ type cmdOptions struct {
OptInitialIndex int `long:"initial-index" description:"position of the initial index of the selection (0 base)"`
OptInitialMatcher string `long:"initial-matcher" description:"matcher"`
OptPrompt string `long:"prompt"`
OptLayout string `long:"layout" description:"layout to be used 'top-down' (default) or 'bottom-up'"`
}
// BufferSize returns the specified buffer size. Fulfills peco.CtxOptions
@ -62,6 +64,10 @@ func (o cmdOptions) InitialIndex() int {
return 1
}
func (o cmdOptions) LayoutType() string {
return o.OptLayout
}
func main() {
var err error
var st int
@ -81,6 +87,14 @@ func main() {
return
}
if opts.OptLayout != "" {
if ! peco.IsValidLayoutType(opts.OptLayout) {
fmt.Fprintf(os.Stderr, "Unknown layout: '%s'\n", opts.OptLayout)
st = 1
return
}
}
if opts.OptHelp {
showHelp()
return

View file

@ -20,10 +20,11 @@ type Config struct {
// events against user input, but since then this has changed
// into something that just records the user's config input
Keymap map[string]string `json:"Keymap"`
Matcher string `json:"Matcher"` // Deprecated.
Matcher string `json:"Matcher"` // Deprecated.
InitialMatcher string `json:"InitialMatcher"` // Use this instead of Matcher
Style StyleSet `json:"Style"`
Style *StyleSet `json:"Style"`
Prompt string `json:"Prompt"`
Layout string `json:"Layout"`
CustomMatcher map[string][]string
}
@ -34,6 +35,7 @@ func NewConfig() *Config {
InitialMatcher: IgnoreCaseMatch,
Style: NewStyleSet(),
Prompt: "QUERY>",
Layout: "top-down",
}
}
@ -51,6 +53,10 @@ func (c *Config) ReadFilename(filename string) error {
return err
}
if !IsValidLayoutType(c.Layout) {
return fmt.Errorf("invalid layout type: %s", c.Layout)
}
return nil
}
@ -97,16 +103,56 @@ type StyleSet struct {
}
// NewStyleSet creates a new StyleSet struct
func NewStyleSet() StyleSet {
return StyleSet{
func NewStyleSet() *StyleSet {
return &StyleSet{
Basic: Style{fg: termbox.ColorDefault, bg: termbox.ColorDefault},
SavedSelection: Style{fg: termbox.ColorBlack | termbox.AttrBold, bg: termbox.ColorCyan},
Selected: Style{fg: termbox.ColorDefault | termbox.AttrUnderline, bg: termbox.ColorMagenta},
Query: Style{fg: termbox.ColorDefault, bg: termbox.ColorDefault},
Matched: Style{fg: termbox.ColorCyan, bg: termbox.ColorDefault},
SavedSelection: Style{fg: termbox.ColorBlack | termbox.AttrBold, bg: termbox.ColorCyan},
Selected: Style{fg: termbox.ColorDefault | termbox.AttrUnderline, bg: termbox.ColorMagenta},
}
}
func (s StyleSet) BasicFG() termbox.Attribute {
return s.Basic.fg
}
func (s StyleSet) BasicBG() termbox.Attribute {
return s.Basic.bg
}
func (s StyleSet) QueryFG() termbox.Attribute {
return s.Query.fg
}
func (s StyleSet) QueryBG() termbox.Attribute {
return s.Query.bg
}
func (s StyleSet) MatchedFG() termbox.Attribute {
return s.Matched.fg
}
func (s StyleSet) MatchedBG() termbox.Attribute {
return s.Matched.bg
}
func (s StyleSet) SavedSelectionFG() termbox.Attribute {
return s.SavedSelection.fg
}
func (s StyleSet) SavedSelectionBG() termbox.Attribute {
return s.SavedSelection.bg
}
func (s StyleSet) SelectedFG() termbox.Attribute {
return s.Selected.fg
}
func (s StyleSet) SelectedBG() termbox.Attribute {
return s.Selected.bg
}
// Style describes termbox styles
type Style struct {
fg termbox.Attribute

26
ctx.go
View file

@ -24,6 +24,9 @@ type CtxOptions interface {
// InitialIndex is the line number to put the cursor on
// when peco starts
InitialIndex() int
// LayoutType returns the name of the layout to use
LayoutType() string
}
type PageInfo struct {
@ -43,7 +46,8 @@ type Ctx struct {
prompt []rune
caretPos int
currentLine int
currentPage PageInfo
currentPage *PageInfo
maxPage int
selection Selection
lines []Match
current []Match
@ -53,6 +57,7 @@ type Ctx struct {
CurrentMatcher int
ExitStatus int
selectionRangeStart int
layoutType string
wait *sync.WaitGroup
}
@ -67,7 +72,8 @@ func NewCtx(o CtxOptions) *Ctx {
[]rune{},
0,
o.InitialIndex(),
struct{ index, offset, perPage int }{0, 1, 0},
&PageInfo{0, 1, 0},
0,
Selection([]int{}),
[]Match{},
nil,
@ -81,6 +87,7 @@ func NewCtx(o CtxOptions) *Ctx {
0,
0,
invalidSelectionRange,
o.LayoutType(),
&sync.WaitGroup{},
}
}
@ -103,6 +110,12 @@ func (c *Ctx) ReadConfig(file string) error {
c.SetCurrentMatcher(c.config.InitialMatcher)
if c.layoutType == "" { // Not set yet
if c.config.Layout != "" {
c.layoutType = c.config.Layout
}
}
return nil
}
@ -179,7 +192,14 @@ func (c *Ctx) NewBufferReader(r io.ReadCloser) *BufferReader {
}
func (c *Ctx) NewView() *View {
return &View{c, nil}
var layout Layout
switch c.layoutType {
case "bottom-up":
layout = NewBottomUpLayout(c)
default:
layout = NewDefaultLayout(c)
}
return &View{c, layout}
}
func (c *Ctx) NewFilter() *Filter {

422
layout.go Normal file
View file

@ -0,0 +1,422 @@
package peco
import (
"fmt"
"time"
"unicode/utf8"
"github.com/mattn/go-runewidth"
"github.com/nsf/termbox-go"
)
type LayoutType string
const (
LayoutTypeTopDown = "top-down"
LayoutTypeBottomUp = "bottom-up"
)
// IsValidLayoutType checks if a string is a supported layout type
func IsValidLayoutType(v string) bool {
return v == LayoutTypeTopDown || v == LayoutTypeBottomUp
}
type VerticalAnchor int
const (
AnchorTop VerticalAnchor = iota + 1
AnchorBottom
)
type Layout interface {
ClearStatus(time.Duration)
PrintStatus(string)
DrawScreen([]Match)
MovePage(PagingRequest)
}
// Utility function
func mergeAttribute(a, b termbox.Attribute) termbox.Attribute {
if a&0x0F == 0 || b&0x0F == 0 {
return a | b
} else {
return ((a - 1) | (b - 1)) + 1
}
}
// Utility function
func printScreen(x, y int, fg, bg termbox.Attribute, msg string, fill bool) {
for len(msg) > 0 {
c, w := utf8.DecodeRuneInString(msg)
if c == utf8.RuneError {
c = '?'
w = 1
}
msg = msg[w:]
termbox.SetCell(x, y, c, fg, bg)
x += runewidth.RuneWidth(c)
}
if !fill {
return
}
width, _ := termbox.Size()
for ; x < width; x++ {
termbox.SetCell(x, y, ' ', fg, bg)
}
}
type AnchorSettings struct {
anchor VerticalAnchor // AnchorTop or AnchorBottom
anchorOffset int // offset this many lines from the anchor
}
// AnchorPosition returns the starting y-offset, based on the
// anchor type and offset
func (as AnchorSettings) AnchorPosition() int {
var pos int
switch as.anchor {
case AnchorTop:
pos = as.anchorOffset
case AnchorBottom:
_, h := termbox.Size()
pos = h - as.anchorOffset - 1 // -1 is required because y is 0 base, but h is 1 base
default:
panic("Unknown anchor type!")
}
return pos
}
// UserPrompt draws the prompt line
type UserPrompt struct {
*Ctx
*AnchorSettings
prefix string
prefixLen int
}
func NewUserPrompt(ctx *Ctx, anchor VerticalAnchor, anchorOffset int) *UserPrompt {
prefix := ctx.config.Prompt
if len(prefix) <= 0 { // default
prefix = "QUERY>"
}
prefixLen := runewidth.StringWidth(prefix)
return &UserPrompt{
Ctx: ctx,
AnchorSettings: &AnchorSettings{anchor, anchorOffset},
prefix: prefix,
prefixLen: prefixLen,
}
}
func (u UserPrompt) Draw() {
location := u.AnchorPosition()
// print "QUERY>"
printScreen(0, location, u.config.Style.BasicFG(), u.config.Style.BasicBG(), u.prefix, false)
if u.caretPos <= 0 {
u.caretPos = 0 // sanity
}
if u.caretPos > len(u.query) {
u.caretPos = len(u.query)
}
if u.caretPos == len(u.query) {
// the entire string + the caret after the string
fg := u.config.Style.QueryFG()
bg := u.config.Style.QueryBG()
qs := string(u.query)
ql := runewidth.StringWidth(qs)
printScreen(u.prefixLen+1, location, fg, bg, qs, false)
printScreen(u.prefixLen+1+ql, location, fg|termbox.AttrReverse, bg|termbox.AttrReverse, " ", false)
printScreen(u.prefixLen+1+ql+1, location, fg, bg, "", true)
} else {
// the caret is in the middle of the string
prev := 0
fg := u.config.Style.QueryFG()
bg := u.config.Style.QueryBG()
for i, r := range u.query {
if i == u.caretPos {
fg |= termbox.AttrReverse
bg |= termbox.AttrReverse
}
termbox.SetCell(u.prefixLen+1+prev, location, r, fg, bg)
prev += runewidth.RuneWidth(r)
}
}
width, _ := termbox.Size()
pmsg := fmt.Sprintf("%s [%d/%d]", u.Matcher().String(), u.currentPage.index, u.maxPage)
printScreen(width-runewidth.StringWidth(pmsg), location, u.config.Style.BasicFG(), u.config.Style.BasicBG(), pmsg, false)
}
// StatusBar draws the status message bar
type StatusBar struct {
*Ctx
*AnchorSettings
clearTimer *time.Timer
}
func NewStatusBar(ctx *Ctx, anchor VerticalAnchor, anchorOffset int) *StatusBar {
return &StatusBar{
ctx,
&AnchorSettings{ anchor, anchorOffset },
nil,
}
}
func (s *StatusBar) stopTimer() {
if t := s.clearTimer; t != nil {
t.Stop()
}
}
func (s *StatusBar) ClearStatus(d time.Duration) {
s.stopTimer()
s.clearTimer = time.AfterFunc(d, func() {
s.PrintStatus("")
})
}
func (s *StatusBar) PrintStatus(msg string) {
s.stopTimer()
location := s.AnchorPosition()
w, _ := termbox.Size()
width := runewidth.StringWidth(msg)
for width > w {
_, rw := utf8.DecodeRuneInString(msg)
width = width - rw
msg = msg[rw:]
}
var pad []byte
if w > width {
pad = make([]byte, w-width)
for i := 0; i < w-width; i++ {
pad[i] = ' '
}
}
fgAttr := s.config.Style.BasicFG()
bgAttr := s.config.Style.BasicBG()
if w > width {
printScreen(0, location, fgAttr, bgAttr, string(pad), false)
}
if width > 0 {
printScreen(w-width, location, fgAttr|termbox.AttrReverse|termbox.AttrBold, bgAttr|termbox.AttrReverse, msg, false)
}
termbox.Flush()
}
type ListArea struct {
*Ctx
*AnchorSettings
sortTopDown bool
}
func NewListArea(ctx *Ctx, anchor VerticalAnchor, anchorOffset int, sortTopDown bool) *ListArea {
return &ListArea{
ctx,
&AnchorSettings{ anchor, anchorOffset },
sortTopDown,
}
}
func (l *ListArea) Draw(targets []Match, perPage int) {
currentPage := l.currentPage
start := l.AnchorPosition()
var y int
var fgAttr, bgAttr termbox.Attribute
for n := 0; n < perPage; n++ {
switch {
case n+currentPage.offset == l.currentLine-1:
fgAttr = l.config.Style.SelectedFG()
bgAttr = l.config.Style.SelectedBG()
case l.selection.Has(n+currentPage.offset+1) || l.SelectedRange().Has(n+currentPage.offset+1):
fgAttr = l.config.Style.SavedSelectionFG()
bgAttr = l.config.Style.SavedSelectionBG()
default:
fgAttr = l.config.Style.BasicFG()
bgAttr = l.config.Style.BasicBG()
}
targetIdx := currentPage.offset + n
if targetIdx >= len(targets) {
break
}
if l.sortTopDown {
y = n + start
} else {
y = start - n
}
target := targets[targetIdx]
line := target.Line()
matches := target.Indices()
if matches == nil {
printScreen(0, y, fgAttr, bgAttr, line, true)
} else {
prev := 0
index := 0
for _, m := range matches {
if m[0] > index {
c := line[index:m[0]]
printScreen(prev, y, fgAttr, bgAttr, c, false)
prev += runewidth.StringWidth(c)
index += len(c)
}
c := line[m[0]:m[1]]
printScreen(prev, y, l.config.Style.MatchedFG(), mergeAttribute(bgAttr, l.config.Style.MatchedBG()), c, true)
prev += runewidth.StringWidth(c)
index += len(c)
}
m := matches[len(matches)-1]
if m[0] > index {
printScreen(prev, y, l.config.Style.QueryFG(), mergeAttribute(bgAttr, l.config.Style.QueryBG()), line[m[0]:m[1]], true)
} else if len(line) > m[1] {
printScreen(prev, y, fgAttr, bgAttr, line[m[1]:len(line)], true)
}
}
}
}
// BasicLayout is... the basic layout :) At this point this is the
// only struct for layouts, which means that while the position
// of components may be configurable, the actual types of components
// that are used are set and static
type BasicLayout struct {
*Ctx
*StatusBar
prompt *UserPrompt
list *ListArea
}
func NewDefaultLayout(ctx *Ctx) *BasicLayout {
return &BasicLayout{
Ctx: ctx,
StatusBar: NewStatusBar(ctx, AnchorBottom, 0),
// The prompt is at the top
prompt: NewUserPrompt(ctx, AnchorTop, 0),
// The list area is at the top, after the prompt
// It's also displayed top-to-bottom order
list: NewListArea(ctx, AnchorTop, 1, true),
}
}
func NewBottomUpLayout(ctx *Ctx) *BasicLayout {
return &BasicLayout{
Ctx: ctx,
StatusBar: NewStatusBar(ctx, AnchorBottom, 0),
// The prompt is at the bottom, above the status bar
prompt: NewUserPrompt(ctx, AnchorBottom, 1),
// The list area is at the bottom, above the prompt
// IT's displayed in bottom-to-top order
list: NewListArea(ctx, AnchorBottom, 2, false),
}
}
func (l *BasicLayout) CalculatePage(targets []Match, perPage int) error {
CALCULATE_PAGE:
currentPage := l.currentPage
currentPage.index = ((l.currentLine - 1) / perPage) + 1
if currentPage.index <= 0 {
currentPage.index = 1
}
currentPage.offset = (currentPage.index - 1) * perPage
currentPage.perPage = perPage
if len(targets) == 0 {
l.maxPage = 1
} else {
l.maxPage = ((len(targets) + perPage - 1) / perPage)
}
if l.maxPage < currentPage.index {
if len(targets) == 0 && len(l.query) == 0 {
// wait for targets
return fmt.Errorf("no targets or query. nothing to do")
}
l.currentLine = currentPage.offset
goto CALCULATE_PAGE
}
return nil
}
func (l *BasicLayout) DrawScreen(targets []Match) {
if err := termbox.Clear(l.config.Style.BasicFG(), l.config.Style.BasicBG()); err != nil {
return
}
if l.currentLine > len(targets) && len(targets) > 0 {
l.currentLine = len(targets)
}
perPage := linesPerPage()
if err := l.CalculatePage(targets, perPage); err != nil {
return
}
l.prompt.Draw()
l.list.Draw(targets, perPage)
if err := termbox.Flush(); err != nil {
return
}
}
func linesPerPage() int {
_, height := termbox.Size()
return height - 2 // list area is always the display area - 2 lines for prompt and status
}
func (l *BasicLayout) MovePage(p PagingRequest) {
if l.list.sortTopDown {
switch p {
case ToLineAbove:
l.currentLine--
case ToLineBelow:
l.currentLine++
case ToScrollPageDown:
l.currentLine += linesPerPage()
case ToScrollPageUp:
l.currentLine -= linesPerPage()
}
} else {
switch p {
case ToLineAbove:
l.currentLine++
case ToLineBelow:
l.currentLine--
case ToScrollPageDown:
l.currentLine -= linesPerPage()
case ToScrollPageUp:
l.currentLine += linesPerPage()
}
}
if l.currentLine < 1 {
if l.current != nil {
// Go to last page, if possible
l.currentLine = len(l.current)
} else {
l.currentLine = 1
}
} else if l.current != nil && l.currentLine > len(l.current) {
l.currentLine = 1
}
}

277
view.go
View file

@ -1,32 +1,25 @@
package peco
import (
"fmt"
"time"
"unicode/utf8"
"github.com/mattn/go-runewidth"
"github.com/nsf/termbox-go"
)
import "time"
// View handles the drawing/updating the screen
type View struct {
*Ctx
clearTimer *time.Timer
layout Layout
}
// PagingRequest can be sent to move the selection cursor
type PagingRequest int
const (
// ToNextLine moves the selection to the next line
ToNextLine PagingRequest = iota
// ToNextPage moves the selection to the next page
ToNextPage
// ToPrevLine moves the selection to the previous line
ToPrevLine
// ToPrevPage moves the selection to the previous page
ToPrevPage
// ToLineAbove moves the selection to the line above
ToLineAbove PagingRequest = iota
// ToScrollPageDown moves the selection to the next page
ToScrollPageDown
// ToLineBelow moves the selection to the line below
ToLineBelow
// ToScrollPageUp moves the selection to the previous page
ToScrollPageUp
)
// Loop receives requests to update the screen
@ -52,248 +45,38 @@ func (v *View) Loop() {
}
}
func (v *View) printStatus(m string) {
v.layout.PrintStatus(m)
}
func (v *View) clearStatus(d time.Duration) {
if t := v.clearTimer; t != nil {
t.Stop()
}
v.clearTimer = time.AfterFunc(d, func() {
v.printStatus("")
})
v.layout.ClearStatus(d)
}
func (v *View) printStatus(msg string) {
if t := v.clearTimer; t != nil {
t.Stop()
}
w, h := termbox.Size()
width := runewidth.StringWidth(msg)
for width > w {
_, rw := utf8.DecodeRuneInString(msg)
width = width - rw
msg = msg[rw:]
}
var pad []byte
if w > width {
pad = make([]byte, w-width)
for i := 0; i < w-width; i++ {
pad[i] = ' '
}
}
fgAttr := v.config.Style.Basic.fg
bgAttr := v.config.Style.Basic.bg
if w > width {
printTB(0, h-2, fgAttr, bgAttr, string(pad))
}
if width > 0 {
printTB(w-width, h-2, fgAttr|termbox.AttrReverse|termbox.AttrBold, bgAttr|termbox.AttrReverse, msg)
}
termbox.Flush()
}
func printTB(x, y int, fg, bg termbox.Attribute, msg string) {
for len(msg) > 0 {
c, w := utf8.DecodeRuneInString(msg)
if c == utf8.RuneError {
c = '?'
w = 1
}
msg = msg[w:]
termbox.SetCell(x, y, c, fg, bg)
x += runewidth.RuneWidth(c)
}
width, _ := termbox.Size()
for ; x < width; x++ {
termbox.SetCell(x, y, ' ', fg, bg)
}
}
func (v *View) movePage(p PagingRequest) {
_, height := termbox.Size()
perPage := height - 4
switch p {
case ToPrevLine:
v.currentLine--
case ToNextLine:
v.currentLine++
case ToPrevPage, ToNextPage:
if p == ToPrevPage {
v.currentLine -= perPage
func (v *View) drawScreenNoLock(targets []Match) {
if targets == nil {
if current := v.current; current != nil {
targets = v.current
} else {
v.currentLine += perPage
targets = v.lines
}
}
if v.currentLine < 1 {
if v.current != nil {
// Go to last page, if possible
v.currentLine = len(v.current)
} else {
v.currentLine = 1
}
} else if v.current != nil && v.currentLine > len(v.current) {
v.currentLine = 1
}
v.drawScreen(nil)
v.layout.DrawScreen(targets)
// FIXME
v.current = targets
}
func (v *View) drawScreen(targets []Match) {
v.mutex.Lock()
defer v.mutex.Unlock()
fgAttr := v.config.Style.Basic.fg
bgAttr := v.config.Style.Basic.bg
if err := termbox.Clear(fgAttr, bgAttr); err != nil {
return
}
if targets == nil {
if current := v.Ctx.current; current != nil {
targets = v.Ctx.current
} else {
targets = v.Ctx.lines
}
}
if v.Ctx.currentLine > len(targets) && len(targets) > 0 {
v.Ctx.currentLine = len(targets)
}
width, height := termbox.Size()
perPage := height - 4
CALCULATE_PAGE:
currentPage := &v.Ctx.currentPage
currentPage.index = ((v.Ctx.currentLine - 1) / perPage) + 1
if currentPage.index <= 0 {
currentPage.index = 1
}
currentPage.offset = (currentPage.index - 1) * perPage
currentPage.perPage = perPage
var maxPage int
if len(targets) == 0 {
maxPage = 1
} else {
maxPage = ((len(targets) + perPage - 1) / perPage)
}
if maxPage < currentPage.index {
if len(targets) == 0 && len(v.Ctx.query) == 0 {
// wait for targets
return
}
v.Ctx.currentLine = currentPage.offset
goto CALCULATE_PAGE
}
fgAttr = v.config.Style.Query.fg
bgAttr = v.config.Style.Query.bg
var prompt string
if len(v.Ctx.prompt) > 0 {
prompt = string(v.Ctx.prompt)
} else {
prompt = v.config.Prompt
}
promptLen := runewidth.StringWidth(prompt)
printTB(0, 0, fgAttr, bgAttr, prompt)
if v.caretPos <= 0 {
v.caretPos = 0 // sanity
}
if v.caretPos > len(v.query) {
v.caretPos = len(v.query)
}
if v.caretPos == len(v.query) {
// the entire string + the caret after the string
printTB(promptLen+1, 0, fgAttr, bgAttr, string(v.query))
termbox.SetCell(promptLen+1+runewidth.StringWidth(string(v.query)), 0, ' ', fgAttr|termbox.AttrReverse, bgAttr|termbox.AttrReverse)
} else {
// the caret is in the middle of the string
prev := 0
for i, r := range v.query {
fg := v.config.Style.Query.fg
bg := v.config.Style.Query.bg
if i == v.caretPos {
fg |= termbox.AttrReverse
bg |= termbox.AttrReverse
}
termbox.SetCell(promptLen+1+prev, 0, r, fg, bg)
prev += runewidth.RuneWidth(r)
}
}
pmsg := fmt.Sprintf("%s [%d/%d]", v.Ctx.Matcher().String(), currentPage.index, maxPage)
printTB(width-runewidth.StringWidth(pmsg), 0, fgAttr, bgAttr, pmsg)
for n := 1; n <= perPage; n++ {
fgAttr = v.config.Style.Basic.fg
bgAttr = v.config.Style.Basic.bg
if n+currentPage.offset == v.currentLine {
fgAttr = v.config.Style.Selected.fg
bgAttr = v.config.Style.Selected.bg
} else if v.selection.Has(n+currentPage.offset) || v.SelectedRange().Has(n+currentPage.offset) {
fgAttr = v.config.Style.SavedSelection.fg
bgAttr = v.config.Style.SavedSelection.bg
}
targetIdx := currentPage.offset + n - 1
if targetIdx >= len(targets) {
break
}
target := targets[targetIdx]
line := target.Line()
matches := target.Indices()
if matches == nil {
printTB(0, n, fgAttr, bgAttr, line)
} else {
prev := 0
index := 0
for _, m := range matches {
if m[0] > index {
c := line[index:m[0]]
printTB(prev, n, fgAttr, bgAttr, c)
prev += runewidth.StringWidth(c)
index += len(c)
}
c := line[m[0]:m[1]]
printTB(prev, n, v.config.Style.Matched.fg, mergeAttribute(bgAttr, v.config.Style.Matched.bg), c)
prev += runewidth.StringWidth(c)
index += len(c)
}
m := matches[len(matches)-1]
if m[0] > index {
printTB(prev, n, v.config.Style.Query.fg, mergeAttribute(bgAttr, v.config.Style.Query.bg), line[m[0]:m[1]])
} else if len(line) > m[1] {
printTB(prev, n, fgAttr, bgAttr, line[m[1]:len(line)])
}
}
}
if err := termbox.Flush(); err != nil {
return
}
// FIXME
v.current = targets
v.drawScreenNoLock(targets)
}
func mergeAttribute(a, b termbox.Attribute) termbox.Attribute {
if a&0x0F == 0 || b&0x0F == 0 {
return a | b
} else {
return ((a - 1) | (b - 1)) + 1
}
func (v *View) movePage(p PagingRequest) {
v.mutex.Lock()
defer v.mutex.Unlock()
v.layout.MovePage(p)
v.drawScreenNoLock(nil)
}