267 lines
5.5 KiB
Go
267 lines
5.5 KiB
Go
package uiterm
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/nsf/termbox-go"
|
|
)
|
|
|
|
type KeyListener func(ui *Ui, key Key)
|
|
type CommandListener func(ui *Ui, cmd string)
|
|
|
|
type UiManager interface {
|
|
OnUiInitialize(ui *Ui)
|
|
OnUiDoneInitialize(ui *Ui)
|
|
OnUiResize(ui *Ui, width, height int)
|
|
}
|
|
|
|
type Ui struct {
|
|
Fg, Bg Attribute
|
|
|
|
close chan struct{}
|
|
closeOnce sync.Once
|
|
events chan func()
|
|
manager UiManager
|
|
|
|
drawCount int32
|
|
elements map[string]*uiElement
|
|
activeElement *uiElement
|
|
|
|
keyListeners map[Key][]KeyListener
|
|
commandListeners map[string][]CommandListener
|
|
}
|
|
|
|
type uiElement struct {
|
|
Name string
|
|
X0, Y0, X1, Y1 int
|
|
View View
|
|
}
|
|
|
|
func New(manager UiManager) *Ui {
|
|
ui := &Ui{
|
|
close: make(chan struct{}),
|
|
events: make(chan func(), 256),
|
|
elements: make(map[string]*uiElement),
|
|
manager: manager,
|
|
keyListeners: make(map[Key][]KeyListener),
|
|
commandListeners: make(map[string][]CommandListener),
|
|
}
|
|
return ui
|
|
}
|
|
|
|
// Close is safe to call repeatedly and never blocks a caller.
|
|
func (ui *Ui) Close() {
|
|
ui.closeOnce.Do(func() { close(ui.close) })
|
|
}
|
|
|
|
// Post schedules UI work on Run's owning goroutine. It is deliberately
|
|
// bounded: network callbacks must not block behind slow terminal rendering.
|
|
func (ui *Ui) Post(fn func()) bool {
|
|
if fn == nil {
|
|
return true
|
|
}
|
|
select {
|
|
case <-ui.close:
|
|
return false
|
|
case ui.events <- fn:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (ui *Ui) Refresh() {
|
|
if termbox.IsInit {
|
|
ui.beginDraw()
|
|
defer ui.endDraw()
|
|
|
|
termbox.Clear(termbox.Attribute(ui.Fg), termbox.Attribute(ui.Bg))
|
|
termbox.HideCursor()
|
|
for _, element := range ui.elements {
|
|
element.View.uiDraw()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ui *Ui) beginDraw() {
|
|
atomic.AddInt32(&ui.drawCount, 1)
|
|
}
|
|
|
|
func (ui *Ui) endDraw() {
|
|
if count := atomic.AddInt32(&ui.drawCount, -1); count == 0 {
|
|
termbox.Flush()
|
|
}
|
|
}
|
|
|
|
func (ui *Ui) Active() string {
|
|
return ui.activeElement.Name
|
|
}
|
|
|
|
func (ui *Ui) SetActive(name string) {
|
|
element, _ := ui.elements[name]
|
|
if ui.activeElement != nil {
|
|
ui.activeElement.View.uiSetActive(false)
|
|
}
|
|
ui.activeElement = element
|
|
if element != nil {
|
|
element.View.uiSetActive(true)
|
|
}
|
|
}
|
|
|
|
func (ui *Ui) Run(cmds chan string) error {
|
|
if termbox.IsInit {
|
|
return nil
|
|
}
|
|
if err := termbox.Init(); err != nil {
|
|
return err
|
|
}
|
|
termbox.SetInputMode(termbox.InputAlt)
|
|
|
|
// Closing termbox wakes PollEvent. Keep delivery cancellable so the polling
|
|
// goroutine cannot become stranded trying to send after Run returns.
|
|
events := make(chan termbox.Event, 1)
|
|
pollDone := make(chan struct{})
|
|
go func() {
|
|
defer close(pollDone)
|
|
for {
|
|
event := termbox.PollEvent()
|
|
select {
|
|
case <-ui.close:
|
|
return
|
|
default:
|
|
}
|
|
select {
|
|
case events <- event:
|
|
case <-ui.close:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
defer func() {
|
|
termbox.Close()
|
|
// Some termbox backends do not wake PollEvent promptly on Close. A fatal
|
|
// startup failure must print its stderr error instead of hanging here.
|
|
select {
|
|
case <-pollDone:
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
}()
|
|
|
|
ui.manager.OnUiInitialize(ui)
|
|
ui.manager.OnUiDoneInitialize(ui)
|
|
width, height := termbox.Size()
|
|
ui.manager.OnUiResize(ui, width, height)
|
|
ui.Refresh()
|
|
|
|
for {
|
|
select {
|
|
case <-ui.close:
|
|
return nil
|
|
case fn := <-ui.events:
|
|
fn()
|
|
case cmd, ok := <-cmds:
|
|
if !ok {
|
|
cmds = nil
|
|
continue
|
|
}
|
|
ui.onCommandEvent(cmd)
|
|
case event := <-events:
|
|
switch event.Type {
|
|
case termbox.EventResize:
|
|
ui.manager.OnUiResize(ui, event.Width, event.Height)
|
|
ui.Refresh()
|
|
case termbox.EventKey:
|
|
var k = uint32(event.Key)
|
|
if event.Ch != 0 && event.Mod != 0 {
|
|
k = uint32(event.Ch)
|
|
}
|
|
if event.Ch != 0 && event.Mod == 0 {
|
|
ui.onCharacterEvent(event.Ch)
|
|
} else {
|
|
if event.Mod > 0 {
|
|
k = k + (uint32(event.Mod) << 16)
|
|
}
|
|
ui.onKeyEvent(Key(k))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ui *Ui) onCharacterEvent(ch rune) {
|
|
if ui.activeElement != nil {
|
|
ui.activeElement.View.uiCharacterEvent(ch)
|
|
}
|
|
}
|
|
|
|
func (ui *Ui) onKeyEvent(key Key) {
|
|
if ui.keyListeners[key] != nil {
|
|
for _, listener := range ui.keyListeners[key] {
|
|
listener(ui, key)
|
|
}
|
|
}
|
|
if ui.activeElement != nil {
|
|
ui.activeElement.View.uiKeyEvent(key)
|
|
}
|
|
}
|
|
|
|
func (ui *Ui) onCommandEvent(cmd string) {
|
|
ta := strings.SplitN(cmd, " ", 2)
|
|
t := ta[0]
|
|
rest := ""
|
|
if len(ta) == 2 {
|
|
rest = ta[1]
|
|
}
|
|
if ui.commandListeners[t] != nil {
|
|
for _, listener := range ui.commandListeners[t] {
|
|
listener(ui, rest)
|
|
}
|
|
}
|
|
if ui.commandListeners["*"] != nil {
|
|
for _, listener := range ui.commandListeners["*"] {
|
|
listener(ui, cmd)
|
|
}
|
|
}
|
|
// if ui.activeElement != nil {
|
|
// ui.activeElement.View.uiKeyEvent(key)
|
|
// }
|
|
}
|
|
|
|
func (ui *Ui) Add(name string, view View) error {
|
|
if _, ok := ui.elements[name]; ok {
|
|
return errors.New("view already exists")
|
|
}
|
|
ui.elements[name] = &uiElement{
|
|
Name: name,
|
|
View: view,
|
|
}
|
|
view.uiInitialize(ui)
|
|
return nil
|
|
}
|
|
|
|
func (ui *Ui) SetBounds(name string, x0, y0, x1, y1 int) error {
|
|
element, ok := ui.elements[name]
|
|
if !ok {
|
|
return errors.New("view does not exist")
|
|
}
|
|
element.X0, element.Y0, element.X1, element.Y1 = x0, y0, x1, y1
|
|
element.View.uiSetBounds(x0, y0, x1, y1)
|
|
return nil
|
|
}
|
|
|
|
func (ui *Ui) AddKeyListener(listener KeyListener, key *Key) {
|
|
if key != nil {
|
|
ui.keyListeners[*key] = append(ui.keyListeners[*key], listener)
|
|
}
|
|
}
|
|
|
|
func (ui *Ui) AddCommandListener(listener CommandListener, cmd string) {
|
|
//if cmd!=nil {
|
|
ui.commandListeners[cmd] = append(ui.commandListeners[cmd], listener)
|
|
//}
|
|
}
|