peco.peco/action.go
2015-04-22 19:16:46 +09:00

628 lines
15 KiB
Go

package peco
import (
"errors"
"fmt"
"unicode"
"github.com/google/btree"
"github.com/nsf/termbox-go"
"github.com/peco/peco/keyseq"
)
// ErrUserCanceled is used to signal that the user deliberately
// canceled using peco
var ErrUserCanceled = errors.New("canceled")
// Action describes an action that can be executed upon receiving user
// input. It's an interface so you can create any kind of Action you need,
// but most everything is implemented in terms of ActionFunc, which is
// callback based Action
type Action interface {
Register(string, ...termbox.Key)
RegisterKeySequence(keyseq.KeyList)
Execute(*Input, termbox.Event)
}
// ActionFunc is a type of Action that is basically just a callback.
type ActionFunc func(*Input, termbox.Event)
// This is the global map of canonical action name to actions
var nameToActions map[string]Action
// This is the default keybinding used by NewKeymap()
var defaultKeyBinding map[string]Action
// Execute fulfills the Action interface for AfterFunc
func (a ActionFunc) Execute(i *Input, e termbox.Event) {
a(i, e)
}
// Register fulfills the Action interface for AfterFunc. Registers `a`
// into the global action registry by the name `name`, and maps to
// default keys via `defaultKeys`
func (a ActionFunc) Register(name string, defaultKeys ...termbox.Key) {
nameToActions["peco."+name] = a
for _, k := range defaultKeys {
a.RegisterKeySequence(keyseq.KeyList{keyseq.NewKeyFromKey(k)})
}
}
// RegisterKeySequence satisfies the Action interface for AfterFunc.
// Registers the action to be mapped against a key sequence
func (a ActionFunc) RegisterKeySequence(k keyseq.KeyList) {
defaultKeyBinding[k.String()] = a
}
func wrapDeprecated(fn func(*Input, termbox.Event), oldName, newName string) ActionFunc {
return ActionFunc(func(i *Input, e termbox.Event) {
i.SendStatusMsg(fmt.Sprintf("%s is deprecated. Use %s", oldName, newName))
fn(i, e)
})
}
func init() {
// Build the global maps
nameToActions = map[string]Action{}
defaultKeyBinding = map[string]Action{}
ActionFunc(doInvertSelection).Register("InvertSelection")
ActionFunc(doBeginningOfLine).Register("BeginningOfLine", termbox.KeyCtrlA)
ActionFunc(doBackwardChar).Register("BackwardChar", termbox.KeyCtrlB)
ActionFunc(doBackwardWord).Register("BackwardWord")
ActionFunc(doCancel).Register("Cancel", termbox.KeyCtrlC, termbox.KeyEsc)
ActionFunc(doDeleteAll).Register("DeleteAll")
ActionFunc(doDeleteBackwardChar).Register(
"DeleteBackwardChar",
termbox.KeyBackspace,
termbox.KeyBackspace2,
)
ActionFunc(doDeleteBackwardWord).Register(
"DeleteBackwardWord",
termbox.KeyCtrlW,
)
ActionFunc(doDeleteForwardChar).Register("DeleteForwardChar", termbox.KeyCtrlD)
ActionFunc(doDeleteForwardWord).Register("DeleteForwardWord")
ActionFunc(doEndOfFile).Register("EndOfFile")
ActionFunc(doEndOfLine).Register("EndOfLine", termbox.KeyCtrlE)
ActionFunc(doFinish).Register("Finish", termbox.KeyEnter)
ActionFunc(doForwardChar).Register("ForwardChar", termbox.KeyCtrlF)
ActionFunc(doForwardWord).Register("ForwardWord")
ActionFunc(doKillEndOfLine).Register("KillEndOfLine", termbox.KeyCtrlK)
ActionFunc(doKillBeginningOfLine).Register("KillBeginningOfLine", termbox.KeyCtrlU)
ActionFunc(doRotateFilter).Register("RotateFilter", termbox.KeyCtrlR)
wrapDeprecated(doRotateFilter, "RotateMatcher", "RotateFilter").Register("RotateMatcher")
ActionFunc(doSelectUp).Register("SelectUp", termbox.KeyArrowUp, termbox.KeyCtrlP)
wrapDeprecated(doSelectDown, "SelectNext", "SelectUp/SelectDown").Register("SelectNext")
ActionFunc(doScrollPageDown).Register("ScrollPageDown", termbox.KeyArrowRight)
wrapDeprecated(doScrollPageDown, "SelectNextPage", "ScrollPageDown/ScrollPageUp").Register("SelectNextPage")
ActionFunc(doSelectDown).Register("SelectDown", termbox.KeyArrowDown, termbox.KeyCtrlN)
wrapDeprecated(doSelectUp, "SelectPrevious", "SelectUp/SelectDown").Register("SelectPrevious")
ActionFunc(doScrollPageUp).Register("ScrollPageUp", termbox.KeyArrowLeft)
wrapDeprecated(doScrollPageUp, "SelectPreviousPage", "ScrollPageDown/ScrollPageUp").Register("SelectPreviousPage")
ActionFunc(doToggleSelection).Register("ToggleSelection")
ActionFunc(doToggleSelectionAndSelectNext).Register(
"ToggleSelectionAndSelectNext",
termbox.KeyCtrlSpace,
)
ActionFunc(doSelectNone).Register(
"SelectNone",
termbox.KeyCtrlG,
)
ActionFunc(doSelectAll).Register("SelectAll")
ActionFunc(doSelectVisible).Register("SelectVisible")
wrapDeprecated(doToggleRangeMode, "ToggleSelectMode", "ToggleRangeMode").Register("ToggleSelectMode")
wrapDeprecated(doCancelRangeMode, "CancelSelectMode", "CancelRangeMode").Register("CancelSelectMode")
ActionFunc(doToggleRangeMode).Register("ToggleRangeMode")
ActionFunc(doCancelRangeMode).Register("CancelRangeMode")
ActionFunc(doToggleQuery).Register("ToggleQuery", termbox.KeyCtrlT)
ActionFunc(doRefreshScreen).Register("RefreshScreen", termbox.KeyCtrlL)
ActionFunc(doKonamiCommand).RegisterKeySequence(
keyseq.KeyList{
keyseq.Key{Modifier: 0, Key: termbox.KeyCtrlX, Ch: 0},
keyseq.Key{Modifier: 0, Key: termbox.KeyArrowUp, Ch: 0},
keyseq.Key{Modifier: 0, Key: termbox.KeyArrowUp, Ch: 0},
keyseq.Key{Modifier: 0, Key: termbox.KeyArrowDown, Ch: 0},
keyseq.Key{Modifier: 0, Key: termbox.KeyArrowDown, Ch: 0},
keyseq.Key{Modifier: 0, Key: termbox.KeyArrowLeft, Ch: 0},
keyseq.Key{Modifier: 0, Key: termbox.KeyArrowRight, Ch: 0},
keyseq.Key{Modifier: 0, Key: termbox.KeyArrowLeft, Ch: 0},
keyseq.Key{Modifier: 0, Key: termbox.KeyArrowRight, Ch: 0},
keyseq.Key{Modifier: 0, Key: 0, Ch: 'b'},
keyseq.Key{Modifier: 0, Key: 0, Ch: 'a'},
},
)
}
// This is a noop action
func doNothing(_ *Input, _ termbox.Event) {}
// This is an exception to the rule. This does not get registered
// anywhere. You just call it directly
func doAcceptChar(i *Input, ev termbox.Event) {
if ev.Key == termbox.KeySpace {
ev.Ch = ' '
}
if ch := ev.Ch; ch > 0 {
if i.QueryLen() == i.CaretPos() {
i.AppendQuery(ch)
} else {
i.InsertQueryAt(ch, i.CaretPos())
}
i.MoveCaretPos(1)
i.DrawPrompt() // Update prompt before running query
i.ExecQuery()
}
}
func doRotateFilter(i *Input, ev termbox.Event) {
trace("doRotateFitler: START")
defer trace("doRotateFitler: END")
i.RotateFilter()
if i.ExecQuery() {
return
}
i.SendDrawPrompt()
}
func doToggleSelection(i *Input, _ termbox.Event) {
l, err := i.GetCurrentLineBuffer().LineAt(i.currentLine)
if err != nil {
return
}
if i.selection.Has(l) {
i.selection.Remove(l)
return
}
i.selection.Add(l)
}
func doToggleRangeMode(i *Input, _ termbox.Event) {
trace("doToggleRangeMode: START")
defer trace("doToggleRangeMode: END")
if i.IsRangeMode() {
i.selectionRangeStart = invalidSelectionRange
} else {
i.selectionRangeStart = i.currentLine
i.SelectionAdd(i.currentLine)
}
}
func doCancelRangeMode(i *Input, _ termbox.Event) {
i.selectionRangeStart = invalidSelectionRange
}
func doSelectNone(i *Input, _ termbox.Event) {
i.SelectionClear()
}
func doSelectAll(i *Input, _ termbox.Event) {
b := i.GetCurrentLineBuffer()
for x := 0; x < b.Size(); x++ {
if l, err := b.LineAt(x); err == nil {
i.selection.Add(l)
} else {
i.selection.Remove(l)
}
}
}
func doSelectVisible(i *Input, _ termbox.Event) {
trace("doSelectVisible: START")
defer trace("doSelectVisible: END")
b := i.GetCurrentLineBuffer()
pc := PageCrop{i.currentPage.perPage, i.currentPage.page}
lb := pc.Crop(b)
for x := 0; x < lb.Size(); x++ {
l, err := lb.LineAt(x)
if err != nil {
continue
}
l.SetDirty(true)
i.selection.Add(l)
}
i.SendDraw()
}
func doFinish(i *Input, _ termbox.Event) {
trace("doFinish: START")
defer trace("doFinish: END")
// Must end with all the selected lines.
if i.SelectionLen() == 0 {
i.SelectionAdd(i.currentLine)
}
i.resultCh = make(chan Line)
go func() {
i.selection.Ascend(func(it btree.Item) bool {
i.resultCh <- it.(Line)
return true
})
close(i.resultCh)
}()
i.ExitWith(nil)
}
func doCancel(i *Input, ev termbox.Event) {
if i.keymap.Keyseq.InMiddleOfChain() {
i.keymap.Keyseq.CancelChain()
return
}
if i.IsRangeMode() {
doCancelRangeMode(i, ev)
return
}
// peco.Cancel -> end program, exit with failure
i.ExitWith(ErrUserCanceled)
}
func doSelectDown(i *Input, ev termbox.Event) {
trace("doSelectDown: START")
defer trace("doSelectDown: END")
i.SendPaging(ToLineBelow)
}
func doSelectUp(i *Input, ev termbox.Event) {
i.SendPaging(ToLineAbove)
}
func doScrollPageUp(i *Input, ev termbox.Event) {
i.SendPaging(ToScrollPageUp)
}
func doScrollPageDown(i *Input, ev termbox.Event) {
i.SendPaging(ToScrollPageDown)
}
func doToggleSelectionAndSelectNext(i *Input, ev termbox.Event) {
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 doInvertSelection(i *Input, _ termbox.Event) {
trace("doInvertSelection: START")
defer trace("doInvertSelection: END")
old := i.selection
i.SelectionClear()
b := i.GetCurrentLineBuffer()
for x := 0; x < b.Size(); x++ {
if l, err := b.LineAt(x); err == nil {
l.SetDirty(true)
i.selection.Add(l)
} else {
i.selection.Remove(l)
}
}
old.Ascend(func(it btree.Item) bool {
i.selection.Delete(it.(Line))
return true
})
i.SendDraw()
}
func doDeleteBackwardWord(i *Input, _ termbox.Event) {
if i.CaretPos() == 0 {
return
}
q := i.Query()
start := i.CaretPos()
if l := len(q); l <= start {
start = l
}
sepFunc := unicode.IsSpace
if unicode.IsSpace(q[start-1]) {
sepFunc = func(r rune) bool { return !unicode.IsSpace(r) }
}
found := false
for pos := start - 1; pos >= 0; pos-- {
if sepFunc(q[pos]) {
buf := make([]rune, len(q)-(start-pos-1))
copy(buf, q[:pos+1])
copy(buf[pos+1:], q[start:])
i.SetQuery(buf)
i.SetCaretPos(pos + 1)
found = true
break
}
}
if !found {
i.SetQuery(q[start:])
i.SetCaretPos(0)
}
if i.ExecQuery() {
return
}
i.DrawPrompt()
}
func doForwardWord(i *Input, _ termbox.Event) {
if i.CaretPos() >= i.QueryLen() {
return
}
defer i.DrawPrompt()
foundSpace := false
for pos := i.CaretPos(); pos < i.QueryLen(); pos++ {
r := i.Query()[pos]
if foundSpace {
if !unicode.IsSpace(r) {
i.SetCaretPos(pos)
return
}
} else {
if unicode.IsSpace(r) {
foundSpace = true
}
}
}
// not found. just move to the end of the buffer
i.SetCaretPos(i.QueryLen())
}
func doBackwardWord(i *Input, _ termbox.Event) {
if i.CaretPos() == 0 {
return
}
defer i.DrawPrompt()
if i.CaretPos() >= i.QueryLen() {
i.MoveCaretPos(-1)
}
// if we start from a whitespace-ish position, we should
// rewind to the end of the previous word, and then do the
// search all over again
SEARCH_PREV_WORD:
if unicode.IsSpace(i.Query()[i.CaretPos()]) {
for pos := i.CaretPos(); pos > 0; pos-- {
if !unicode.IsSpace(i.Query()[pos]) {
i.SetCaretPos(pos)
break
}
}
}
// if we start from the first character of a word, we
// should attempt to move back and search for the previous word
if i.CaretPos() > 0 && unicode.IsSpace(i.Query()[i.CaretPos()-1]) {
i.MoveCaretPos(-1)
goto SEARCH_PREV_WORD
}
// Now look for a space
for pos := i.CaretPos(); pos > 0; pos-- {
if unicode.IsSpace(i.Query()[pos]) {
i.SetCaretPos(int(pos + 1))
return
}
}
// not found. just move to the beginning of the buffer
i.SetCaretPos(0)
}
func doForwardChar(i *Input, _ termbox.Event) {
if i.CaretPos() >= i.QueryLen() {
return
}
i.MoveCaretPos(1)
i.DrawPrompt()
}
func doBackwardChar(i *Input, _ termbox.Event) {
if i.CaretPos() <= 0 {
return
}
i.MoveCaretPos(-1)
i.DrawPrompt()
}
func doDeleteForwardWord(i *Input, _ termbox.Event) {
if i.QueryLen() <= i.CaretPos() {
return
}
defer i.DrawPrompt()
start := i.CaretPos()
// If we are on a word (non-Space, delete till the end of the word.
// If we are on a space, delete till the end of space.
q := i.Query()
sepFunc := unicode.IsSpace
if unicode.IsSpace(q[start]) {
sepFunc = func(r rune) bool { return !unicode.IsSpace(r) }
}
for pos := start; pos < i.QueryLen(); pos++ {
if pos == i.QueryLen()-1 {
i.SetQuery(q[:start])
i.SetCaretPos(start)
break
}
if sepFunc(q[pos]) {
buf := make([]rune, i.QueryLen()-(pos-start))
copy(buf, q[:start])
copy(buf[start:], q[pos:])
i.SetQuery(buf)
i.SetCaretPos(start)
break
}
}
if i.ExecQuery() {
return
}
}
func doBeginningOfLine(i *Input, _ termbox.Event) {
i.SetCaretPos(0)
i.DrawPrompt()
}
func doEndOfLine(i *Input, _ termbox.Event) {
i.SetCaretPos(i.QueryLen())
i.DrawPrompt()
}
func doEndOfFile(i *Input, ev termbox.Event) {
if i.QueryLen() > 0 {
doDeleteForwardChar(i, ev)
} else {
doCancel(i, ev)
}
}
func doKillBeginningOfLine(i *Input, _ termbox.Event) {
i.SetQuery(i.Query()[i.CaretPos():])
i.SetCaretPos(0)
if i.ExecQuery() {
return
}
i.DrawPrompt()
}
func doKillEndOfLine(i *Input, _ termbox.Event) {
if i.QueryLen() <= i.CaretPos() {
return
}
i.SetQuery(i.Query()[0:i.CaretPos()])
if i.ExecQuery() {
return
}
i.DrawPrompt()
}
func doDeleteAll(i *Input, _ termbox.Event) {
i.SetQuery(make([]rune, 0))
i.ExecQuery()
}
func doDeleteForwardChar(i *Input, _ termbox.Event) {
if i.QueryLen() <= i.CaretPos() {
return
}
pos := i.CaretPos()
buf := make([]rune, i.QueryLen()-1)
copy(buf, i.Query()[:i.CaretPos()])
copy(buf[i.CaretPos():], i.Query()[i.CaretPos()+1:])
i.SetQuery(buf)
i.SetCaretPos(pos)
if i.ExecQuery() {
return
}
i.DrawPrompt()
}
func doDeleteBackwardChar(i *Input, ev termbox.Event) {
trace("doDeleteBackwardChar: START")
defer trace("doDeleteBackwardChar: END")
qlen := i.QueryLen()
if qlen <= 0 {
trace("doDeleteBackwardChar: QueryLen <= 0, do nothing")
return
}
pos := i.CaretPos()
if pos == 0 {
trace("doDeleteBackwardChar: Already at position 0")
// No op
return
}
var buf []rune
if qlen == 1 {
// Micro optimization
buf = []rune{}
} else {
q := i.Query()
if pos == qlen {
buf = q[:qlen-1 : qlen-1]
} else {
buf = make([]rune, qlen-1)
copy(buf, q[:pos])
copy(buf[pos-1:], q[pos:])
}
}
i.SetQuery(buf)
i.SetCaretPos(pos - 1)
if i.ExecQuery() {
return
}
i.DrawPrompt()
}
func doRefreshScreen(i *Input, _ termbox.Event) {
i.ExecQuery()
}
func doToggleQuery(i *Input, _ termbox.Event) {
q := i.Query()
if len(q) == 0 {
sq := i.SavedQuery()
if len(sq) == 0 {
return
}
i.SetQuery(sq)
i.SetSavedQuery([]rune{})
} else {
i.SetSavedQuery(q)
i.SetQuery([]rune{})
}
if i.ExecQuery() {
return
}
i.DrawPrompt()
}
func doKonamiCommand(i *Input, ev termbox.Event) {
i.SendStatusMsg("All your filters are belongs to us")
}
func makeCombinedAction(actions ...Action) ActionFunc {
return ActionFunc(func(i *Input, ev termbox.Event) {
i.Batch(func() {
for _, a := range actions {
a.Execute(i, ev)
}
})
})
}