mirror of
https://github.com/peco/peco.git
synced 2026-09-10 07:16:29 -04:00
commit
f6b6928262
14
Changes
14
Changes
|
|
@ -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
|
||||
|
|
|
|||
30
README.md
30
README.md
|
|
@ -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
|
||||
=======
|
||||
|
||||
|
|
|
|||
69
action.go
69
action.go
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
58
config.go
58
config.go
|
|
@ -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
26
ctx.go
|
|
@ -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
422
layout.go
Normal 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
277
view.go
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue