Implement basic ex line input
TODO: - scrolling - commit/cancel - command history (via an external command history provider) - tab completion (via an external tab completion provider)
This commit is contained in:
parent
07f7cac2f3
commit
661e3ec2a4
5 changed files with 162 additions and 4 deletions
|
@ -70,9 +70,8 @@ func main() {
|
|||
fill('.'), ui.BORDER_RIGHT)).At(1, 0).Span(2, 1)
|
||||
grid.AddChild(tabs.TabStrip).At(0, 1)
|
||||
grid.AddChild(tabs.TabContent).At(1, 1)
|
||||
// ex line placeholder:
|
||||
grid.AddChild(ui.NewText("Connected").
|
||||
Color(tb.ColorBlack, tb.ColorWhite)).At(2, 1)
|
||||
exline := ui.NewExLine()
|
||||
grid.AddChild(exline).At(2, 1)
|
||||
|
||||
_ui, err := ui.Initialize(conf, grid)
|
||||
if err != nil {
|
||||
|
@ -80,6 +79,8 @@ func main() {
|
|||
}
|
||||
defer _ui.Close()
|
||||
|
||||
_ui.AddInteractive(exline)
|
||||
|
||||
go (func() {
|
||||
for {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
@ -89,7 +90,8 @@ func main() {
|
|||
|
||||
for !_ui.Exit {
|
||||
if !_ui.Tick() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// ~60 FPS
|
||||
time.Sleep(16 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,14 @@ type Context struct {
|
|||
height int
|
||||
}
|
||||
|
||||
func (ctx *Context) X() int {
|
||||
return ctx.x
|
||||
}
|
||||
|
||||
func (ctx *Context) Y() int {
|
||||
return ctx.y
|
||||
}
|
||||
|
||||
func (ctx *Context) Width() int {
|
||||
return ctx.width
|
||||
}
|
||||
|
|
127
ui/exline.go
Normal file
127
ui/exline.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
tb "github.com/nsf/termbox-go"
|
||||
)
|
||||
|
||||
// TODO: history
|
||||
// TODO: tab completion
|
||||
// TODO: commit
|
||||
// TODO: cancel (via esc/ctrl+c)
|
||||
// TODO: scrolling
|
||||
|
||||
type ExLine struct {
|
||||
command *string
|
||||
commit func(cmd *string)
|
||||
index int
|
||||
scroll int
|
||||
|
||||
onInvalidate func(d Drawable)
|
||||
}
|
||||
|
||||
func NewExLine() *ExLine {
|
||||
cmd := ""
|
||||
return &ExLine{command: &cmd}
|
||||
}
|
||||
|
||||
func (ex *ExLine) OnInvalidate(onInvalidate func(d Drawable)) {
|
||||
ex.onInvalidate = onInvalidate
|
||||
}
|
||||
|
||||
func (ex *ExLine) Invalidate() {
|
||||
if ex.onInvalidate != nil {
|
||||
ex.onInvalidate(ex)
|
||||
}
|
||||
}
|
||||
|
||||
func (ex *ExLine) Draw(ctx *Context) {
|
||||
cell := tb.Cell{
|
||||
Fg: tb.ColorDefault,
|
||||
Bg: tb.ColorDefault,
|
||||
Ch: ' ',
|
||||
}
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), cell)
|
||||
ctx.Printf(0, 0, cell, ":%s", *ex.command)
|
||||
tb.SetCursor(ctx.X()+ex.index-ex.scroll+1, ctx.Y())
|
||||
}
|
||||
|
||||
func (ex *ExLine) insert(ch rune) {
|
||||
newCmd := (*ex.command)[:ex.index] + string(ch) + (*ex.command)[ex.index:]
|
||||
ex.command = &newCmd
|
||||
ex.index++
|
||||
ex.Invalidate()
|
||||
}
|
||||
|
||||
func (ex *ExLine) deleteWord() {
|
||||
// TODO: Break on any of / " '
|
||||
if len(*ex.command) == 0 {
|
||||
return
|
||||
}
|
||||
i := ex.index - 1
|
||||
if (*ex.command)[i] == ' ' {
|
||||
i--
|
||||
}
|
||||
for ; i >= 0; i-- {
|
||||
if (*ex.command)[i] == ' ' {
|
||||
break
|
||||
}
|
||||
}
|
||||
newCmd := (*ex.command)[:i+1] + (*ex.command)[ex.index:]
|
||||
ex.command = &newCmd
|
||||
ex.index = i + 1
|
||||
ex.Invalidate()
|
||||
}
|
||||
|
||||
func (ex *ExLine) deleteChar() {
|
||||
if len(*ex.command) > 0 && ex.index != len(*ex.command) {
|
||||
newCmd := (*ex.command)[:ex.index] + (*ex.command)[ex.index+1:]
|
||||
ex.command = &newCmd
|
||||
ex.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (ex *ExLine) backspace() {
|
||||
if len(*ex.command) > 0 && ex.index != 0 {
|
||||
newCmd := (*ex.command)[:ex.index-1] + (*ex.command)[ex.index:]
|
||||
ex.command = &newCmd
|
||||
ex.index--
|
||||
ex.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
func (ex *ExLine) Event(event tb.Event) bool {
|
||||
switch event.Type {
|
||||
case tb.EventKey:
|
||||
switch event.Key {
|
||||
case tb.KeySpace:
|
||||
ex.insert(' ')
|
||||
case tb.KeyBackspace, tb.KeyBackspace2:
|
||||
ex.backspace()
|
||||
case tb.KeyCtrlD, tb.KeyDelete:
|
||||
ex.deleteChar()
|
||||
case tb.KeyCtrlB, tb.KeyArrowLeft:
|
||||
if ex.index > 0 {
|
||||
ex.index--
|
||||
ex.Invalidate()
|
||||
}
|
||||
case tb.KeyCtrlF, tb.KeyArrowRight:
|
||||
if ex.index < len(*ex.command) {
|
||||
ex.index++
|
||||
ex.Invalidate()
|
||||
}
|
||||
case tb.KeyCtrlA, tb.KeyHome:
|
||||
ex.index = 0
|
||||
ex.Invalidate()
|
||||
case tb.KeyCtrlE, tb.KeyEnd:
|
||||
ex.index = len(*ex.command)
|
||||
ex.Invalidate()
|
||||
case tb.KeyCtrlW:
|
||||
ex.deleteWord()
|
||||
default:
|
||||
if event.Ch != 0 {
|
||||
ex.insert(event.Ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
10
ui/interactive.go
Normal file
10
ui/interactive.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
tb "github.com/nsf/termbox-go"
|
||||
)
|
||||
|
||||
type Interactive interface {
|
||||
// Returns true if the event was handled by this component
|
||||
Event(event tb.Event) bool
|
||||
}
|
11
ui/ui.go
11
ui/ui.go
|
@ -11,6 +11,8 @@ type UI struct {
|
|||
Content Drawable
|
||||
ctx *Context
|
||||
|
||||
interactive []Interactive
|
||||
|
||||
tbEvents chan tb.Event
|
||||
invalidations chan interface{}
|
||||
}
|
||||
|
@ -58,6 +60,11 @@ func (state *UI) Tick() bool {
|
|||
state.ctx = NewContext(event.Width, event.Height)
|
||||
state.Content.Invalidate()
|
||||
}
|
||||
if state.interactive != nil {
|
||||
for _, i := range state.interactive {
|
||||
i.Event(event)
|
||||
}
|
||||
}
|
||||
case <-state.invalidations:
|
||||
state.Content.Draw(state.ctx)
|
||||
tb.Flush()
|
||||
|
@ -66,3 +73,7 @@ func (state *UI) Tick() bool {
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (state *UI) AddInteractive(i Interactive) {
|
||||
state.interactive = append(state.interactive, i)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue