diff --git a/comp.go b/comp.go index 5d8acc3..40fceac 100644 --- a/comp.go +++ b/comp.go @@ -38,6 +38,7 @@ var ( "renew", "echo", "cd", + "push", } gOptWords = []string{ diff --git a/doc.go b/doc.go index da7b80e..53778ea 100644 --- a/doc.go +++ b/doc.go @@ -37,8 +37,9 @@ The following commands are provided by lf with default keybindings. The following commands are provided by lf without default keybindings. - echo - cd + echo prints its arguments to the message line + cd changes working directory to its argument + push simulate key pushes given in its argument The following options can be used to customize the behavior of lf. @@ -104,7 +105,32 @@ group statements until a "\n" occurs. This is especially useful for "map" and "cmd" commands. If you need multiline you can wrap statements in "{{" and "}}" after the proper prefix. -Custom Commands +Mappings + +The usual way to map a key sequence is to assign it to a named or unnamed +command. While this provides a clean way to remap builtin keys as well as other +commands, it can be limiting at times. For this reason "push" command is +provided by lf. This command is used to simulate key pushes given as its +arguments. You can "map" a key to a "push" command with an argument to create +various keybindings. + +This is mainly useful for two purposes. First, it can be used to map a command +with a command count. + + map push 10j + +Second, it can be used to avoid typing the name when a command takes arguments. + + map r push :rename + +One thing to be careful is that since "push" command works with keys instead of +commands it is possible to accidentally create recursive bindings. + + map j push 2j + +These types of bindings create a deadlock when executed. + +Commands For demonstration let us write a shell command to move selected file(s) to trash. diff --git a/docstring.go b/docstring.go index df3f523..501750e 100644 --- a/docstring.go +++ b/docstring.go @@ -41,8 +41,9 @@ The following commands are provided by lf with default keybindings. The following commands are provided by lf without default keybindings. - echo - cd + echo prints its arguments to the message line + cd changes working directory to its argument + push simulate key pushes given in its argument The following options can be used to customize the behavior of lf. @@ -113,7 +114,34 @@ and "cmd" commands. If you need multiline you can wrap statements in "{{" and "}}" after the proper prefix. -Custom Commands +Mappings + +The usual way to map a key sequence is to assign it to a named or unnamed +command. While this provides a clean way to remap builtin keys as well as +other commands, it can be limiting at times. For this reason "push" command +is provided by lf. This command is used to simulate key pushes given as its +arguments. You can "map" a key to a "push" command with an argument to +create various keybindings. + +This is mainly useful for two purposes. First, it can be used to map a +command with a command count. + + map push 10j + +Second, it can be used to avoid typing the name when a command takes +arguments. + + map r push :rename + +One thing to be careful is that since "push" command works with keys instead +of commands it is possible to accidentally create recursive bindings. + + map j push 2j + +These types of bindings create a deadlock when executed. + + +Commands For demonstration let us write a shell command to move selected file(s) to trash. diff --git a/etc/lfrc.example b/etc/lfrc.example index ad243b4..0bda384 100644 --- a/etc/lfrc.example +++ b/etc/lfrc.example @@ -74,6 +74,7 @@ cmd open-file ${{ # rename current file without overwrite cmd rename $[ -e "$1" ] || mv "$f" "$1" +map r push :rename # show disk usage cmd usage $du -h . | less diff --git a/eval.go b/eval.go index b388614..b452f55 100644 --- a/eval.go +++ b/eval.go @@ -6,6 +6,7 @@ import ( "os" "strconv" "strings" + "unicode/utf8" ) func (e *SetExpr) eval(app *App, args []string) { @@ -120,6 +121,25 @@ func (e *CmdExpr) eval(app *App, args []string) { gOpts.cmds[e.name] = e.expr } +func splitKeys(s string) (keys []string) { + for i := 0; i < len(s); { + c, w := utf8.DecodeRuneInString(s[i:]) + if c != '<' { + keys = append(keys, s[i:i+w]) + i += w + } else { + j := i + w + for c != '>' && j < len(s) { + c, w = utf8.DecodeRuneInString(s[j:]) + j += w + } + keys = append(keys, s[i:j]) + i = j + } + } + return +} + func (e *CallExpr) eval(app *App, args []string) { // TODO: check for extra toks in each case switch e.name { @@ -303,6 +323,10 @@ func (e *CallExpr) eval(app *App, args []string) { return } app.ui.loadFile(app.nav) + case "push": + if len(e.args) > 0 { + app.ui.keysbuf = append(app.ui.keysbuf, splitKeys(strings.Join(e.args, ""))...) + } default: cmd, ok := gOpts.cmds[e.name] if !ok { diff --git a/eval_test.go b/eval_test.go index b6c1997..d652335 100644 --- a/eval_test.go +++ b/eval_test.go @@ -1,7 +1,11 @@ package main -// These inputs are used in scan and parse tests. +import ( + "reflect" + "testing" +) +// These inputs are used in scan and parse tests. var gTests = []struct { inp string toks []string @@ -199,3 +203,33 @@ var gTests = []struct { `}}}, }, } + +func TestSplitKeys(t *testing.T) { + inps := []struct { + s string + keys []string + }{ + {"", nil}, + {"j", []string{"j"}}, + {"jk", []string{"j", "k"}}, + {"1j", []string{"1", "j"}}, + {"42j", []string{"4", "2", "j"}}, + {"", []string{""}}, + {"j", []string{"j", ""}}, + {"jk", []string{"j", "", "k"}}, + {"1jk", []string{"1", "j", "", "k"}}, + {"1j1k", []string{"1", "j", "", "1", "k"}}, + {"<>", []string{"<>"}}, + {"", []string{""}}, + {">", []string{">", ""}}, + {">>", []string{">", "", ">"}}, + } + + for _, inp := range inps { + if keys := splitKeys(inp.s); !reflect.DeepEqual(keys, inp.keys) { + t.Errorf("at input '%s' expected '%v' but got '%v'", inp.s, inp.keys, keys) + } + } +} diff --git a/ui.go b/ui.go index 99d1abd..e528b84 100644 --- a/ui.go +++ b/ui.go @@ -22,6 +22,74 @@ import ( const EscapeCode = 27 +var gKeyVal = map[termbox.Key][]rune{ + termbox.KeyF1: []rune{'<', 'f', '-', '1', '>'}, + termbox.KeyF2: []rune{'<', 'f', '-', '2', '>'}, + termbox.KeyF3: []rune{'<', 'f', '-', '3', '>'}, + termbox.KeyF4: []rune{'<', 'f', '-', '4', '>'}, + termbox.KeyF5: []rune{'<', 'f', '-', '5', '>'}, + termbox.KeyF6: []rune{'<', 'f', '-', '6', '>'}, + termbox.KeyF7: []rune{'<', 'f', '-', '7', '>'}, + termbox.KeyF8: []rune{'<', 'f', '-', '8', '>'}, + termbox.KeyF9: []rune{'<', 'f', '-', '9', '>'}, + termbox.KeyF10: []rune{'<', 'f', '-', '1', '0', '>'}, + termbox.KeyF11: []rune{'<', 'f', '-', '1', '1', '>'}, + termbox.KeyF12: []rune{'<', 'f', '-', '1', '2', '>'}, + termbox.KeyInsert: []rune{'<', 'i', 'n', 's', 'e', 'r', 't', '>'}, + termbox.KeyDelete: []rune{'<', 'd', 'e', 'l', 'e', 't', 'e', '>'}, + termbox.KeyHome: []rune{'<', 'h', 'o', 'm', 'e', '>'}, + termbox.KeyEnd: []rune{'<', 'e', 'n', 'd', '>'}, + termbox.KeyPgup: []rune{'<', 'p', 'g', 'u', 'p', '>'}, + termbox.KeyPgdn: []rune{'<', 'p', 'g', 'd', 'n', '>'}, + termbox.KeyArrowUp: []rune{'<', 'u', 'p', '>'}, + termbox.KeyArrowDown: []rune{'<', 'd', 'o', 'w', 'n', '>'}, + termbox.KeyArrowLeft: []rune{'<', 'l', 'e', 'f', 't', '>'}, + termbox.KeyArrowRight: []rune{'<', 'r', 'i', 'g', 'h', 't', '>'}, + termbox.KeyCtrlSpace: []rune{'<', 'c', '-', 's', 'p', 'a', 'c', 'e', '>'}, + termbox.KeyCtrlA: []rune{'<', 'c', '-', 'a', '>'}, + termbox.KeyCtrlB: []rune{'<', 'c', '-', 'b', '>'}, + termbox.KeyCtrlC: []rune{'<', 'c', '-', 'c', '>'}, + termbox.KeyCtrlD: []rune{'<', 'c', '-', 'd', '>'}, + termbox.KeyCtrlE: []rune{'<', 'c', '-', 'e', '>'}, + termbox.KeyCtrlF: []rune{'<', 'c', '-', 'f', '>'}, + termbox.KeyCtrlG: []rune{'<', 'c', '-', 'g', '>'}, + termbox.KeyBackspace: []rune{'<', 'b', 's', '>'}, + termbox.KeyTab: []rune{'<', 't', 'a', 'b', '>'}, + termbox.KeyCtrlJ: []rune{'<', 'c', '-', 'j', '>'}, + termbox.KeyCtrlK: []rune{'<', 'c', '-', 'k', '>'}, + termbox.KeyCtrlL: []rune{'<', 'c', '-', 'l', '>'}, + termbox.KeyEnter: []rune{'<', 'e', 'n', 't', 'e', 'r', '>'}, + termbox.KeyCtrlN: []rune{'<', 'c', '-', 'n', '>'}, + termbox.KeyCtrlO: []rune{'<', 'c', '-', 'o', '>'}, + termbox.KeyCtrlP: []rune{'<', 'c', '-', 'p', '>'}, + termbox.KeyCtrlQ: []rune{'<', 'c', '-', 'q', '>'}, + termbox.KeyCtrlR: []rune{'<', 'c', '-', 'r', '>'}, + termbox.KeyCtrlS: []rune{'<', 'c', '-', 's', '>'}, + termbox.KeyCtrlT: []rune{'<', 'c', '-', 't', '>'}, + termbox.KeyCtrlU: []rune{'<', 'c', '-', 'u', '>'}, + termbox.KeyCtrlV: []rune{'<', 'c', '-', 'v', '>'}, + termbox.KeyCtrlW: []rune{'<', 'c', '-', 'w', '>'}, + termbox.KeyCtrlX: []rune{'<', 'c', '-', 'x', '>'}, + termbox.KeyCtrlY: []rune{'<', 'c', '-', 'y', '>'}, + termbox.KeyCtrlZ: []rune{'<', 'c', '-', 'z', '>'}, + termbox.KeyEsc: []rune{'<', 'e', 's', 'c', '>'}, + termbox.KeyCtrlBackslash: []rune{'<', 'c', '-', '\\', '>'}, + termbox.KeyCtrlRsqBracket: []rune{'<', 'c', '-', ']', '>'}, + termbox.KeyCtrl6: []rune{'<', 'c', '-', '6', '>'}, + termbox.KeyCtrlSlash: []rune{'<', 'c', '-', '/', '>'}, + termbox.KeySpace: []rune{'<', 's', 'p', 'a', 'c', 'e', '>'}, + termbox.KeyBackspace2: []rune{'<', 'b', 's', '2', '>'}, +} + +var gValKey map[string]termbox.Key + +func init() { + gValKey = make(map[string]termbox.Key) + for k, v := range gKeyVal { + gValKey[string(v)] = k + } +} + type Win struct { w int h int @@ -253,6 +321,7 @@ type UI struct { message string regprev []string dirprev *Dir + keysbuf []string } func getWidths(wtot int) []int { @@ -467,6 +536,35 @@ func findBinds(keys map[string]Expr, prefix string) (binds map[string]Expr, ok b return } +func (ui *UI) pollEvent() termbox.Event { + if len(ui.keysbuf) > 0 { + ev := termbox.Event{Type: termbox.EventKey} + keys := ui.keysbuf[0] + if len(keys) == 1 { + ev.Ch, _ = utf8.DecodeRuneInString(keys) + } else { + switch keys { + case "": + ev.Ch = '<' + case "": + ev.Ch = '>' + default: + if val, ok := gValKey[keys]; ok { + ev.Key = val + } else { + ev.Key = termbox.KeyEsc + msg := fmt.Sprintf("unknown key: %s", keys) + ui.message = msg + log.Print(msg) + } + } + } + ui.keysbuf = ui.keysbuf[1:] + return ev + } + return termbox.PollEvent() +} + func (ui *UI) getExpr(nav *Nav) (expr Expr, count int) { expr = &CallExpr{"renew", nil} count = 1 @@ -475,7 +573,7 @@ func (ui *UI) getExpr(nav *Nav) (expr Expr, count int) { var cnt []rune for { - switch ev := termbox.PollEvent(); ev.Type { + switch ev := ui.pollEvent(); ev.Type { case termbox.EventKey: if ev.Ch != 0 { switch { @@ -492,121 +590,12 @@ func (ui *UI) getExpr(nav *Nav) (expr Expr, count int) { acc = append(acc, ev.Ch) } } else { - switch ev.Key { - case termbox.KeyF1: - acc = append(acc, '<', 'f', '-', '1', '>') - case termbox.KeyF2: - acc = append(acc, '<', 'f', '-', '2', '>') - case termbox.KeyF3: - acc = append(acc, '<', 'f', '-', '3', '>') - case termbox.KeyF4: - acc = append(acc, '<', 'f', '-', '4', '>') - case termbox.KeyF5: - acc = append(acc, '<', 'f', '-', '5', '>') - case termbox.KeyF6: - acc = append(acc, '<', 'f', '-', '6', '>') - case termbox.KeyF7: - acc = append(acc, '<', 'f', '-', '7', '>') - case termbox.KeyF8: - acc = append(acc, '<', 'f', '-', '8', '>') - case termbox.KeyF9: - acc = append(acc, '<', 'f', '-', '9', '>') - case termbox.KeyF10: - acc = append(acc, '<', 'f', '-', '1', '0', '>') - case termbox.KeyF11: - acc = append(acc, '<', 'f', '-', '1', '1', '>') - case termbox.KeyF12: - acc = append(acc, '<', 'f', '-', '1', '2', '>') - case termbox.KeyInsert: - acc = append(acc, '<', 'i', 'n', 's', 'e', 'r', 't', '>') - case termbox.KeyDelete: - acc = append(acc, '<', 'd', 'e', 'l', 'e', 't', 'e', '>') - case termbox.KeyHome: - acc = append(acc, '<', 'h', 'o', 'm', 'e', '>') - case termbox.KeyEnd: - acc = append(acc, '<', 'e', 'n', 'd', '>') - case termbox.KeyPgup: - acc = append(acc, '<', 'p', 'g', 'u', 'p', '>') - case termbox.KeyPgdn: - acc = append(acc, '<', 'p', 'g', 'd', 'n', '>') - case termbox.KeyArrowUp: - acc = append(acc, '<', 'u', 'p', '>') - case termbox.KeyArrowDown: - acc = append(acc, '<', 'd', 'o', 'w', 'n', '>') - case termbox.KeyArrowLeft: - acc = append(acc, '<', 'l', 'e', 'f', 't', '>') - case termbox.KeyArrowRight: - acc = append(acc, '<', 'r', 'i', 'g', 'h', 't', '>') - case termbox.KeyCtrlSpace: // also KeyCtrlTilde and KeyCtrl2 - acc = append(acc, '<', 'c', '-', 's', 'p', 'a', 'c', 'e', '>') - case termbox.KeyCtrlA: - acc = append(acc, '<', 'c', '-', 'a', '>') - case termbox.KeyCtrlB: - acc = append(acc, '<', 'c', '-', 'b', '>') - case termbox.KeyCtrlC: - acc = append(acc, '<', 'c', '-', 'c', '>') - case termbox.KeyCtrlD: - acc = append(acc, '<', 'c', '-', 'd', '>') - case termbox.KeyCtrlE: - acc = append(acc, '<', 'c', '-', 'e', '>') - case termbox.KeyCtrlF: - acc = append(acc, '<', 'c', '-', 'f', '>') - case termbox.KeyCtrlG: - acc = append(acc, '<', 'c', '-', 'g', '>') - case termbox.KeyBackspace: // also KeyCtrlH - acc = append(acc, '<', 'b', 's', '>') - case termbox.KeyTab: // also KeyCtrlI - acc = append(acc, '<', 't', 'a', 'b', '>') - case termbox.KeyCtrlJ: - acc = append(acc, '<', 'c', '-', 'j', '>') - case termbox.KeyCtrlK: - acc = append(acc, '<', 'c', '-', 'k', '>') - case termbox.KeyCtrlL: - acc = append(acc, '<', 'c', '-', 'l', '>') - case termbox.KeyEnter: // also KeyCtrlM - acc = append(acc, '<', 'e', 'n', 't', 'e', 'r', '>') - case termbox.KeyCtrlN: - acc = append(acc, '<', 'c', '-', 'n', '>') - case termbox.KeyCtrlO: - acc = append(acc, '<', 'c', '-', 'o', '>') - case termbox.KeyCtrlP: - acc = append(acc, '<', 'c', '-', 'p', '>') - case termbox.KeyCtrlQ: - acc = append(acc, '<', 'c', '-', 'q', '>') - case termbox.KeyCtrlR: - acc = append(acc, '<', 'c', '-', 'r', '>') - case termbox.KeyCtrlS: - acc = append(acc, '<', 'c', '-', 's', '>') - case termbox.KeyCtrlT: - acc = append(acc, '<', 'c', '-', 't', '>') - case termbox.KeyCtrlU: - acc = append(acc, '<', 'c', '-', 'u', '>') - case termbox.KeyCtrlV: - acc = append(acc, '<', 'c', '-', 'v', '>') - case termbox.KeyCtrlW: - acc = append(acc, '<', 'c', '-', 'w', '>') - case termbox.KeyCtrlX: - acc = append(acc, '<', 'c', '-', 'x', '>') - case termbox.KeyCtrlY: - acc = append(acc, '<', 'c', '-', 'y', '>') - case termbox.KeyCtrlZ: - acc = append(acc, '<', 'c', '-', 'z', '>') - case termbox.KeyEsc: // also KeyCtrlLsqBracket and KeyCtrl3 + val := gKeyVal[ev.Key] + if string(val) == "" { acc = nil return - case termbox.KeyCtrlBackslash: // also KeyCtrl4 - acc = append(acc, '<', 'c', '-', '\\', '>') - case termbox.KeyCtrlRsqBracket: // also KeyCtrl5 - acc = append(acc, '<', 'c', '-', ']', '>') - case termbox.KeyCtrl6: - acc = append(acc, '<', 'c', '-', '6', '>') - case termbox.KeyCtrlSlash: // also KeyCtrlUnderscore and KeyCtrl7 - acc = append(acc, '<', 'c', '-', '/', '>') - case termbox.KeySpace: - acc = append(acc, '<', 's', 'p', 'a', 'c', 'e', '>') - case termbox.KeyBackspace2: // also KeyCtrl8 - acc = append(acc, '<', 'b', 's', '2', '>') } + acc = append(acc, val...) } binds, ok := findBinds(gOpts.keys, string(acc)) @@ -672,7 +661,7 @@ func (ui *UI) prompt(nav *Nav, pref string) string { var buf []rune for { - switch ev := termbox.PollEvent(); ev.Type { + switch ev := ui.pollEvent(); ev.Type { case termbox.EventKey: if ev.Ch != 0 { lacc = append(lacc, ev.Ch)