cli: Provide a Go-friendly API.

Eventually, the newedit package will become just an Elvish binding of this package.
This commit is contained in:
Qi Xiao 2019-04-27 17:08:58 +01:00
parent 6c4f100b83
commit 726ce887a0
8 changed files with 310 additions and 6 deletions

View File

@ -4,12 +4,37 @@ package main
import (
"fmt"
"io"
"unicode"
"github.com/elves/elvish/cli/clicore"
"github.com/elves/elvish/cli"
"github.com/elves/elvish/edit/ui"
"github.com/elves/elvish/styled"
)
func highlight(code string) styled.Text {
t := styled.Text{}
for _, r := range code {
style := ""
if unicode.IsDigit(r) {
style = "green"
}
t = append(t, styled.MakeText(string(r), style)...)
}
return t
}
func main() {
app := clicore.NewAppFromStdIO()
app := cli.NewApp(&cli.AppConfig{
Prompt: cli.ConstPlainPrompt("> "),
Highlighter: cli.FuncHighlighterNoError(highlight),
InsertConfig: cli.InsertModeConfig{
Binding: cli.MapBinding(map[ui.Key]cli.KeyHandler{
ui.K('D', ui.Ctrl): cli.CommitEOF,
ui.Default: cli.DefaultInsert,
}),
},
})
for {
code, err := app.ReadCode()
if err != nil {
@ -19,9 +44,5 @@ func main() {
break
}
fmt.Println("got:", code)
if code == "exit" {
fmt.Println("bye")
break
}
}
}

40
cli/app.go Normal file
View File

@ -0,0 +1,40 @@
package cli
import (
"github.com/elves/elvish/cli/clicore"
)
// App represents a CLI app.
type App = clicore.App
// AppConfig is a struct containing configurations for initializing an App.
type AppConfig struct {
MaxHeight int
BeforeReadline []func()
AfterReadline []func(string)
Highlighter Highlighter
Prompt, RPrompt Prompt
RPromptPersistent bool
InsertConfig InsertModeConfig
}
// NewApp creates a new App.
func NewApp(cfg *AppConfig) *App {
app := clicore.NewAppFromStdIO()
app.Config.Raw = clicore.RawConfig{
MaxHeight: cfg.MaxHeight,
RPromptPersistent: cfg.RPromptPersistent,
}
app.BeforeReadline = cfg.BeforeReadline
app.AfterReadline = cfg.AfterReadline
app.Highlighter = cfg.Highlighter
app.Prompt = cfg.Prompt
app.RPrompt = cfg.RPrompt
insertMode := newInsertMode(&cfg.InsertConfig, app.State())
app.InitMode = insertMode
return app
}

82
cli/binding.go Normal file
View File

@ -0,0 +1,82 @@
package cli
import (
"github.com/elves/elvish/cli/clitypes"
"github.com/elves/elvish/edit/ui"
)
// Binding represents key binding.
type Binding interface {
// KeyHandler returns a KeyHandler for the given key.
KeyHandler(ui.Key) KeyHandler
}
// KeyHandler is a function that can handle a key event.
type KeyHandler func(KeyEvent)
// KeyEvent is passed to a KeyHandler, containing information about the event
// and can be used for specifying actions.
type KeyEvent interface {
// Key returns the key that triggered the KeyEvent.
Key() ui.Key
// State returns the State of the app.
State() *clitypes.State
// CommitEOF specifies that the app should return from ReadCode with io.EOF
// after the key handler returns.
CommitEOF()
// CommitCode specifies that the app should return from ReadCode after the
// key handler returns.
CommitCode()
}
// Internal implementation of KeyHandler interface.
type keyEvent struct {
key ui.Key
state *clitypes.State
commitEOF bool
commitLine bool
}
func (ev *keyEvent) Key() ui.Key { return ev.key }
func (ev *keyEvent) State() *clitypes.State { return ev.state }
func (ev *keyEvent) CommitEOF() { ev.commitEOF = true }
func (ev *keyEvent) CommitCode() { ev.commitLine = true }
// MapBinding builds a Binding from a map. The map may contain the special
// key ui.Default for a default KeyHandler.
func MapBinding(m map[ui.Key]KeyHandler) Binding {
return mapBinding(m)
}
type mapBinding map[ui.Key]KeyHandler
func (b mapBinding) KeyHandler(k ui.Key) KeyHandler {
handler, ok := b[k]
if ok {
return handler
}
return b[ui.Default]
}
func adaptBinding(b Binding, st *clitypes.State) func(ui.Key) clitypes.HandlerAction {
if b == nil {
return nil
}
return func(k ui.Key) clitypes.HandlerAction {
ev := &keyEvent{k, st, false, false}
handler := b.KeyHandler(k)
if handler == nil {
return clitypes.NoAction
}
handler(ev)
switch {
case ev.commitEOF:
return clitypes.CommitEOF
case ev.commitLine:
return clitypes.CommitCode
default:
return clitypes.NoAction
}
}
}

25
cli/builtin_handlers.go Normal file
View File

@ -0,0 +1,25 @@
package cli
import (
"github.com/elves/elvish/cli/clitypes"
"github.com/elves/elvish/cli/cliutil"
"github.com/elves/elvish/edit/tty"
)
// CommitEOF is an EventHandler that calls CommitEOF.
var CommitEOF = KeyEvent.CommitEOF
// CommitCode is an EventHandler that calls CommitCode.
var CommitCode = KeyEvent.CommitCode
// DefaultInsert is an EventHandler that is suitable as the default EventHandler
// of insert mode.
func DefaultInsert(ev KeyEvent) {
action := cliutil.BasicHandler(tty.KeyEvent(ev.Key()), ev.State())
switch action {
case clitypes.CommitCode:
ev.CommitCode()
case clitypes.CommitEOF:
ev.CommitEOF()
}
}

8
cli/doc.go Normal file
View File

@ -0,0 +1,8 @@
// Package cli provides the facility for building a readline-like CLI with very
// rich features.
//
// The current implementation involves a lot of wrappers and additional
// utilities, to avoid breaking the newedit package. Eventually, those changes
// will be moved upstream and the newedit package will be an Elvish binding for
// this package.
package cli

35
cli/highlighter.go Normal file
View File

@ -0,0 +1,35 @@
package cli
import (
"github.com/elves/elvish/cli/clicore"
"github.com/elves/elvish/styled"
)
// Highlighter represents a highlighter.
type Highlighter = clicore.Highlighter
// FuncHighlighter builds a Highlighter from a function that takes the code and
// returns styled text and a slice of errors.
func FuncHighlighter(f func(string) (styled.Text, []error)) Highlighter {
return funcHighlighter{f}
}
// FuncHighlighterNoError builds a Highlighter from a function that takes the
// code and returns styled text.
func FuncHighlighterNoError(f func(string) styled.Text) Highlighter {
return funcHighlighter{func(code string) (styled.Text, []error) {
return f(code), nil
}}
}
type funcHighlighter struct {
f func(string) (styled.Text, []error)
}
func (hl funcHighlighter) Get(code string) (styled.Text, []error) {
return hl.f(code)
}
func (hl funcHighlighter) LateUpdates() <-chan styled.Text {
return nil
}

51
cli/insert_mode.go Normal file
View File

@ -0,0 +1,51 @@
package cli
import (
"github.com/elves/elvish/cli/clitypes"
"github.com/elves/elvish/newedit/insert"
)
// InsertModeConfig is a struct containing configuration for the insert mode.
type InsertModeConfig struct {
Binding Binding
Abbrs StringPairs
QuotePaste bool
}
// StringPairs is a general interface for accessing pairs of strings.
type StringPairs interface {
IterateStringPairs(func(a, b string))
}
// StringsPairsFromSlice builds a StringPairs from a slice.
func StringsPairsFromSlice(s [][2]string) StringPairs {
return sliceStringPairs(s)
}
type sliceStringPairs [][2]string
func (s sliceStringPairs) IterateStringPairs(f func(abbr, full string)) {
for _, a := range s {
f(a[0], a[1])
}
}
func makeAbbrIterate(sp StringPairs) func(func(abbr, full string)) {
if sp == nil {
return nil
}
return sp.IterateStringPairs
}
// Initializes an insert mode.
func newInsertMode(cfg *InsertModeConfig, st *clitypes.State) clitypes.Mode {
return &insert.Mode{
KeyHandler: adaptBinding(cfg.Binding, st),
AbbrIterate: makeAbbrIterate(cfg.Abbrs),
Config: insert.Config{
Raw: insert.RawConfig{
QuotePaste: cfg.QuotePaste,
},
},
}
}

42
cli/prompt.go Normal file
View File

@ -0,0 +1,42 @@
package cli
import (
"github.com/elves/elvish/cli/clicore"
"github.com/elves/elvish/styled"
)
// Prompt represents a prompt.
type Prompt = clicore.Prompt
// ConstPrompt builds a styled Prompt that does not change.
func ConstPrompt(t styled.Text) Prompt {
return constPrompt{t}
}
// ConstPlainPrompt builds a plain Prompt that does not change.
func ConstPlainPrompt(s string) Prompt {
return constPrompt{styled.Plain(s)}
}
// FuncPrompt builds a styled Prompt from a function.
func FuncPrompt(f func() styled.Text) Prompt {
return funcPrompt{f}
}
// FuncPlainPrompt builds a plain Prompt from a function.
func FuncPlainPrompt(f func() string) Prompt {
return funcPrompt{func() styled.Text { return styled.Plain(f()) }}
}
// A Prompt implementation that always return the same styled.Text.
type constPrompt struct{ t styled.Text }
func (constPrompt) Trigger(force bool) {}
func (p constPrompt) Get() styled.Text { return p.t }
func (constPrompt) LateUpdates() <-chan styled.Text { return nil }
type funcPrompt struct{ f func() styled.Text }
func (funcPrompt) Trigger(force bool) {}
func (p funcPrompt) Get() styled.Text { return p.f() }
func (funcPrompt) LateUpdates() <-chan styled.Text { return nil }