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)
|
fill('.'), ui.BORDER_RIGHT)).At(1, 0).Span(2, 1)
|
||||||
grid.AddChild(tabs.TabStrip).At(0, 1)
|
grid.AddChild(tabs.TabStrip).At(0, 1)
|
||||||
grid.AddChild(tabs.TabContent).At(1, 1)
|
grid.AddChild(tabs.TabContent).At(1, 1)
|
||||||
// ex line placeholder:
|
exline := ui.NewExLine()
|
||||||
grid.AddChild(ui.NewText("Connected").
|
grid.AddChild(exline).At(2, 1)
|
||||||
Color(tb.ColorBlack, tb.ColorWhite)).At(2, 1)
|
|
||||||
|
|
||||||
_ui, err := ui.Initialize(conf, grid)
|
_ui, err := ui.Initialize(conf, grid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -80,6 +79,8 @@ func main() {
|
||||||
}
|
}
|
||||||
defer _ui.Close()
|
defer _ui.Close()
|
||||||
|
|
||||||
|
_ui.AddInteractive(exline)
|
||||||
|
|
||||||
go (func() {
|
go (func() {
|
||||||
for {
|
for {
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
@ -89,7 +90,8 @@ func main() {
|
||||||
|
|
||||||
for !_ui.Exit {
|
for !_ui.Exit {
|
||||||
if !_ui.Tick() {
|
if !_ui.Tick() {
|
||||||
time.Sleep(100 * time.Millisecond)
|
// ~60 FPS
|
||||||
|
time.Sleep(16 * time.Millisecond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,14 @@ type Context struct {
|
||||||
height int
|
height int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) X() int {
|
||||||
|
return ctx.x
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Y() int {
|
||||||
|
return ctx.y
|
||||||
|
}
|
||||||
|
|
||||||
func (ctx *Context) Width() int {
|
func (ctx *Context) Width() int {
|
||||||
return ctx.width
|
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
|
Content Drawable
|
||||||
ctx *Context
|
ctx *Context
|
||||||
|
|
||||||
|
interactive []Interactive
|
||||||
|
|
||||||
tbEvents chan tb.Event
|
tbEvents chan tb.Event
|
||||||
invalidations chan interface{}
|
invalidations chan interface{}
|
||||||
}
|
}
|
||||||
|
@ -58,6 +60,11 @@ func (state *UI) Tick() bool {
|
||||||
state.ctx = NewContext(event.Width, event.Height)
|
state.ctx = NewContext(event.Width, event.Height)
|
||||||
state.Content.Invalidate()
|
state.Content.Invalidate()
|
||||||
}
|
}
|
||||||
|
if state.interactive != nil {
|
||||||
|
for _, i := range state.interactive {
|
||||||
|
i.Event(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
case <-state.invalidations:
|
case <-state.invalidations:
|
||||||
state.Content.Draw(state.ctx)
|
state.Content.Draw(state.ctx)
|
||||||
tb.Flush()
|
tb.Flush()
|
||||||
|
@ -66,3 +73,7 @@ func (state *UI) Tick() bool {
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (state *UI) AddInteractive(i Interactive) {
|
||||||
|
state.interactive = append(state.interactive, i)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue