initial commit
This commit is contained in:
14
README.md
Normal file
14
README.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# barnard
|
||||||
|
|
||||||
|
barnard is a terminal-based client for the [Mumble](http://mumble.info) voice
|
||||||
|
chat software.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- [gumble](https://github.com/layeh/gumble/tree/master/gumble)
|
||||||
|
- [gumble_openal](https://github.com/layeh/gumble/tree/master/gumble_openal)
|
||||||
|
- [termbox-go](https://github.com/nsf/termbox-go)
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
Tim Cooper (<tim.cooper@layeh.com>)
|
||||||
21
barnard.go
Normal file
21
barnard.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package barnard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/layeh/barnard/uiterm"
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/layeh/gumble/gumble_openal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Barnard struct {
|
||||||
|
Config gumble.Config
|
||||||
|
Client *gumble.Client
|
||||||
|
|
||||||
|
Stream *gumble_openal.Stream
|
||||||
|
|
||||||
|
Ui *uiterm.Ui
|
||||||
|
UiOutput uiterm.Textview
|
||||||
|
UiInput uiterm.Textbox
|
||||||
|
UiStatus uiterm.Label
|
||||||
|
UiTree uiterm.Tree
|
||||||
|
UiInputStatus uiterm.Label
|
||||||
|
}
|
||||||
112
client.go
Normal file
112
client.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package barnard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *Barnard) OnConnect(e *gumble.ConnectEvent) {
|
||||||
|
b.Ui.SetActive(uiViewInput)
|
||||||
|
b.UiTree.Rebuild()
|
||||||
|
b.Ui.Refresh()
|
||||||
|
|
||||||
|
if b.Client.AudioEncoder().Bitrate() > e.MaximumBitrate {
|
||||||
|
b.Client.AudioEncoder().SetBitrate(e.MaximumBitrate / 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.UpdateInputStatus(fmt.Sprintf("To: %s", e.Client.Self().Channel().Name()))
|
||||||
|
b.AddOutputLine(fmt.Sprintf("Connected to %s", b.Client.Conn().RemoteAddr()))
|
||||||
|
if e.WelcomeMessage != "" {
|
||||||
|
b.AddOutputLine(fmt.Sprintf("Welcome message: %s", esc(e.WelcomeMessage)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnDisconnect(e *gumble.DisconnectEvent) {
|
||||||
|
var reason string
|
||||||
|
switch e.Type {
|
||||||
|
case gumble.DisconnectError:
|
||||||
|
reason = "connection error"
|
||||||
|
case gumble.DisconnectOther:
|
||||||
|
reason = e.String
|
||||||
|
case gumble.DisconnectVersion:
|
||||||
|
reason = "invalid version number"
|
||||||
|
case gumble.DisconnectUserName:
|
||||||
|
reason = "invalid user name"
|
||||||
|
case gumble.DisconnectUserCredentials:
|
||||||
|
reason = "incorrect user password/certificate"
|
||||||
|
case gumble.DisconnectServerPassword:
|
||||||
|
reason = "incorrect server password"
|
||||||
|
case gumble.DisconnectUsernameInUse:
|
||||||
|
reason = "user name in use"
|
||||||
|
case gumble.DisconnectServerFull:
|
||||||
|
reason = "server full"
|
||||||
|
case gumble.DisconnectNoCertificate:
|
||||||
|
reason = "missing certificate"
|
||||||
|
case gumble.DisconnectAuthenticatorFail:
|
||||||
|
reason = "authenticator via failed"
|
||||||
|
}
|
||||||
|
if reason == "" {
|
||||||
|
b.AddOutputLine("Disconnected")
|
||||||
|
} else {
|
||||||
|
b.AddOutputLine("Disconnected: " + reason)
|
||||||
|
}
|
||||||
|
b.UiTree.Rebuild()
|
||||||
|
b.Ui.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnTextMessage(e *gumble.TextMessageEvent) {
|
||||||
|
b.AddOutputMessage(e.Sender, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) {
|
||||||
|
if e.Type.Has(gumble.UserChangeChannel) && e.User == b.Client.Self() {
|
||||||
|
b.UpdateInputStatus(fmt.Sprintf("To: %s", e.User.Channel().Name()))
|
||||||
|
}
|
||||||
|
b.UiTree.Rebuild()
|
||||||
|
b.Ui.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnChannelChange(e *gumble.ChannelChangeEvent) {
|
||||||
|
b.UiTree.Rebuild()
|
||||||
|
b.Ui.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnPermissionDenied(e *gumble.PermissionDeniedEvent) {
|
||||||
|
var info string
|
||||||
|
switch e.Type {
|
||||||
|
case gumble.PermissionDeniedOther:
|
||||||
|
info = e.String
|
||||||
|
case gumble.PermissionDeniedPermission:
|
||||||
|
info = "insufficient permissions"
|
||||||
|
case gumble.PermissionDeniedSuperUser:
|
||||||
|
info = "cannot modify SuperUser"
|
||||||
|
case gumble.PermissionDeniedInvalidChannelName:
|
||||||
|
info = "invalid channel name"
|
||||||
|
case gumble.PermissionDeniedTextTooLong:
|
||||||
|
info = "text too long"
|
||||||
|
case gumble.PermissionDeniedTemporaryChannel:
|
||||||
|
info = "temporary channel"
|
||||||
|
case gumble.PermissionDeniedMissingCertificate:
|
||||||
|
info = "missing certificate"
|
||||||
|
case gumble.PermissionDeniedInvalidUserName:
|
||||||
|
info = "invalid user name"
|
||||||
|
case gumble.PermissionDeniedChannelFull:
|
||||||
|
info = "channel full"
|
||||||
|
case gumble.PermissionDeniedNestingLimit:
|
||||||
|
info = "nesting limit"
|
||||||
|
}
|
||||||
|
b.AddOutputLine(fmt.Sprintf("Permission denied: %s", info))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnUserList(e *gumble.UserListEvent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnAcl(e *gumble.AclEvent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnBanList(e *gumble.BanListEvent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnContextActionChange(e *gumble.ContextActionChangeEvent) {
|
||||||
|
}
|
||||||
62
cmd/barnard/main.go
Normal file
62
cmd/barnard/main.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/layeh/barnard"
|
||||||
|
"github.com/layeh/barnard/uiterm"
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/layeh/gumble/gumble_openal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Command line flags
|
||||||
|
server := flag.String("server", "localhost:64738", "the server to connect to")
|
||||||
|
username := flag.String("username", "", "the username of the client")
|
||||||
|
insecure := flag.Bool("insecure", false, "skip server certificate verification")
|
||||||
|
certificate := flag.String("certificate", "", "PEM encoded certificate and private key")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
b := barnard.Barnard{}
|
||||||
|
b.Ui = uiterm.New(&b)
|
||||||
|
|
||||||
|
// Gumble
|
||||||
|
b.Config = gumble.Config{
|
||||||
|
Username: *username,
|
||||||
|
Address: *server,
|
||||||
|
Listener: &b,
|
||||||
|
}
|
||||||
|
if *insecure {
|
||||||
|
b.Config.TlsConfig.InsecureSkipVerify = true
|
||||||
|
}
|
||||||
|
if *certificate != "" {
|
||||||
|
if cert, err := tls.LoadX509KeyPair(*certificate, *certificate); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
} else {
|
||||||
|
b.Config.TlsConfig.Certificates = []tls.Certificate{cert}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Client = gumble.NewClient(&b.Config)
|
||||||
|
// Audio
|
||||||
|
if stream, err := gumble_openal.New(b.Client); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
} else {
|
||||||
|
b.Config.AudioListener = stream
|
||||||
|
b.Stream = stream
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.Client.Connect(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Ui.Run()
|
||||||
|
}
|
||||||
175
ui.go
Normal file
175
ui.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package barnard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/layeh/barnard/uiterm"
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/kennygrant/sanitize"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
uiViewLogo = "logo"
|
||||||
|
uiViewTop = "top"
|
||||||
|
uiViewStatus = "status"
|
||||||
|
uiViewInput = "input"
|
||||||
|
uiViewInputStatus = "inputstatus"
|
||||||
|
uiViewOutput = "output"
|
||||||
|
uiViewTree = "tree"
|
||||||
|
)
|
||||||
|
|
||||||
|
func esc(str string) string {
|
||||||
|
return sanitize.HTML(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) UpdateInputStatus(status string) {
|
||||||
|
b.UiInputStatus.Text = status
|
||||||
|
b.UiTree.Rebuild()
|
||||||
|
b.Ui.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) AddOutputLine(line string) {
|
||||||
|
now := time.Now()
|
||||||
|
b.UiOutput.AddLine(fmt.Sprintf("[%02d:%02d:%02d] %s", now.Hour(), now.Minute(), now.Second(), line))
|
||||||
|
b.Ui.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) AddOutputMessage(sender *gumble.User, message string) {
|
||||||
|
if sender == nil {
|
||||||
|
b.AddOutputLine(message)
|
||||||
|
} else {
|
||||||
|
b.AddOutputLine(fmt.Sprintf("%s: %s", sender.Name(), strings.TrimSpace(esc(message))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnVoiceToggle(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
|
if b.UiStatus.Text == " Tx " {
|
||||||
|
b.UiStatus.Text = " Idle "
|
||||||
|
b.UiStatus.Fg = uiterm.ColorBlack
|
||||||
|
b.UiStatus.Bg = uiterm.ColorWhite
|
||||||
|
b.Stream.StopSource()
|
||||||
|
} else {
|
||||||
|
b.UiStatus.Fg = uiterm.ColorWhite | uiterm.AttrBold
|
||||||
|
b.UiStatus.Bg = uiterm.ColorRed
|
||||||
|
b.UiStatus.Text = " Tx "
|
||||||
|
b.Stream.StartSource()
|
||||||
|
}
|
||||||
|
ui.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnQuitPress(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
|
b.Client.Disconnect()
|
||||||
|
b.Ui.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnClearPress(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
|
b.UiOutput.Clear()
|
||||||
|
b.Ui.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnScrollOutputUp(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
|
b.UiOutput.ScrollUp()
|
||||||
|
b.Ui.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnScrollOutputDown(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
|
b.UiOutput.ScrollDown()
|
||||||
|
b.Ui.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnScrollOutputTop(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
|
b.UiOutput.ScrollTop()
|
||||||
|
b.Ui.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnScrollOutputBottom(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
|
b.UiOutput.ScrollBottom()
|
||||||
|
b.Ui.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnFocusPress(ui *uiterm.Ui, key uiterm.Key) {
|
||||||
|
active := b.Ui.Active()
|
||||||
|
if active == &b.UiInput {
|
||||||
|
b.Ui.SetActive(uiViewTree)
|
||||||
|
} else if active == &b.UiTree {
|
||||||
|
b.Ui.SetActive(uiViewInput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnTextInput(ui *uiterm.Ui, textbox *uiterm.Textbox, text string) {
|
||||||
|
if text == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if b.Client != nil && b.Client.Self() != nil {
|
||||||
|
b.Client.Self().Channel().Send(text, false)
|
||||||
|
b.AddOutputMessage(b.Client.Self(), text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnUiInitialize(ui *uiterm.Ui) {
|
||||||
|
ui.SetView(uiViewLogo, 0, 0, 0, 0, &uiterm.Label{
|
||||||
|
Text: " barnard ",
|
||||||
|
Fg: uiterm.ColorWhite | uiterm.AttrBold,
|
||||||
|
Bg: uiterm.ColorMagenta,
|
||||||
|
})
|
||||||
|
|
||||||
|
ui.SetView(uiViewTop, 0, 0, 0, 0, &uiterm.Label{
|
||||||
|
Fg: uiterm.ColorWhite,
|
||||||
|
Bg: uiterm.ColorBlue,
|
||||||
|
})
|
||||||
|
|
||||||
|
b.UiStatus = uiterm.Label{
|
||||||
|
Text: " Idle ",
|
||||||
|
Fg: uiterm.ColorBlack,
|
||||||
|
Bg: uiterm.ColorWhite,
|
||||||
|
}
|
||||||
|
ui.SetView(uiViewStatus, 0, 0, 0, 0, &b.UiStatus)
|
||||||
|
|
||||||
|
b.UiInput = uiterm.Textbox{
|
||||||
|
Fg: uiterm.ColorWhite,
|
||||||
|
Bg: uiterm.ColorBlack,
|
||||||
|
Input: b.OnTextInput,
|
||||||
|
}
|
||||||
|
ui.SetView(uiViewInput, 0, 0, 0, 0, &b.UiInput)
|
||||||
|
|
||||||
|
b.UiInputStatus = uiterm.Label{
|
||||||
|
Fg: uiterm.ColorBlack,
|
||||||
|
Bg: uiterm.ColorWhite,
|
||||||
|
}
|
||||||
|
ui.SetView(uiViewInputStatus, 0, 0, 0, 0, &b.UiInputStatus)
|
||||||
|
|
||||||
|
b.UiOutput = uiterm.Textview{
|
||||||
|
Fg: uiterm.ColorWhite,
|
||||||
|
Bg: uiterm.ColorBlack,
|
||||||
|
}
|
||||||
|
ui.SetView(uiViewOutput, 0, 0, 0, 0, &b.UiOutput)
|
||||||
|
|
||||||
|
b.UiTree = uiterm.Tree{
|
||||||
|
Generator: b.TreeItem,
|
||||||
|
Listener: b.TreeItemSelect,
|
||||||
|
Fg: uiterm.ColorWhite,
|
||||||
|
Bg: uiterm.ColorBlack,
|
||||||
|
}
|
||||||
|
ui.SetView(uiViewTree, 0, 0, 0, 0, &b.UiTree)
|
||||||
|
|
||||||
|
b.Ui.AddKeyListener(b.OnFocusPress, uiterm.KeyTab)
|
||||||
|
b.Ui.AddKeyListener(b.OnVoiceToggle, uiterm.KeyF1)
|
||||||
|
b.Ui.AddKeyListener(b.OnQuitPress, uiterm.KeyF10)
|
||||||
|
b.Ui.AddKeyListener(b.OnClearPress, uiterm.KeyCtrlL)
|
||||||
|
b.Ui.AddKeyListener(b.OnScrollOutputUp, uiterm.KeyPgup)
|
||||||
|
b.Ui.AddKeyListener(b.OnScrollOutputDown, uiterm.KeyPgdn)
|
||||||
|
b.Ui.AddKeyListener(b.OnScrollOutputTop, uiterm.KeyHome)
|
||||||
|
b.Ui.AddKeyListener(b.OnScrollOutputBottom, uiterm.KeyEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) OnUiResize(ui *uiterm.Ui, width, height int) {
|
||||||
|
ui.SetView(uiViewLogo, 0, 0, 9, 1, nil)
|
||||||
|
ui.SetView(uiViewTop, 9, 0, width-6, 1, nil)
|
||||||
|
ui.SetView(uiViewStatus, width-6, 0, width, 1, nil)
|
||||||
|
ui.SetView(uiViewInput, 0, height-1, width, height, nil)
|
||||||
|
ui.SetView(uiViewInputStatus, 0, height-2, width, height-1, nil)
|
||||||
|
ui.SetView(uiViewOutput, 0, 1, width-20, height-2, nil)
|
||||||
|
ui.SetView(uiViewTree, width-20, 1, width, height-2, nil)
|
||||||
|
}
|
||||||
77
ui_tree.go
Normal file
77
ui_tree.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package barnard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/layeh/barnard/uiterm"
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TreeItem struct {
|
||||||
|
User *gumble.User
|
||||||
|
Channel *gumble.Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ti TreeItem) String() string {
|
||||||
|
if ti.User != nil {
|
||||||
|
return ti.User.Name()
|
||||||
|
}
|
||||||
|
if ti.Channel != nil {
|
||||||
|
return ti.Channel.Name()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ti TreeItem) TreeItemStyle(active bool) (uiterm.Attribute, uiterm.Attribute) {
|
||||||
|
fg := uiterm.ColorDefault
|
||||||
|
bg := uiterm.ColorBlack
|
||||||
|
if ti.Channel != nil {
|
||||||
|
fg |= uiterm.AttrBold
|
||||||
|
}
|
||||||
|
if active {
|
||||||
|
bg |= uiterm.AttrReverse
|
||||||
|
}
|
||||||
|
return fg, bg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) TreeItemSelect(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm.TreeItem) {
|
||||||
|
treeItem := item.(TreeItem)
|
||||||
|
if treeItem.Channel != nil {
|
||||||
|
b.Client.Self().Move(treeItem.Channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Barnard) TreeItem(item uiterm.TreeItem) []uiterm.TreeItem {
|
||||||
|
var treeItem TreeItem
|
||||||
|
if ti, ok := item.(TreeItem); !ok {
|
||||||
|
root := b.Client.Channels()[0]
|
||||||
|
if root == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []uiterm.TreeItem{
|
||||||
|
TreeItem{
|
||||||
|
Channel: root,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
treeItem = ti
|
||||||
|
}
|
||||||
|
|
||||||
|
if treeItem.User != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
users := []uiterm.TreeItem{}
|
||||||
|
for _, user := range treeItem.Channel.Users() {
|
||||||
|
users = append(users, TreeItem{
|
||||||
|
User: user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
channels := []uiterm.TreeItem{}
|
||||||
|
for _, subchannel := range treeItem.Channel.Children() {
|
||||||
|
channels = append(channels, TreeItem{
|
||||||
|
Channel: subchannel,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(users, channels...)
|
||||||
|
}
|
||||||
25
uiterm/attributes.go
Normal file
25
uiterm/attributes.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package uiterm
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Source: https://godoc.org/github.com/nsf/termbox-go
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Attribute int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ColorDefault Attribute = iota
|
||||||
|
ColorBlack
|
||||||
|
ColorRed
|
||||||
|
ColorGreen
|
||||||
|
ColorYellow
|
||||||
|
ColorBlue
|
||||||
|
ColorMagenta
|
||||||
|
ColorCyan
|
||||||
|
ColorWhite
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AttrBold Attribute = 1 << (iota + 4)
|
||||||
|
AttrUnderline
|
||||||
|
AttrReverse
|
||||||
|
)
|
||||||
91
uiterm/keys.go
Normal file
91
uiterm/keys.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package uiterm
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Source: https://godoc.org/github.com/nsf/termbox-go
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Modifier uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModAlt Modifier = 0x01
|
||||||
|
)
|
||||||
|
|
||||||
|
type Key uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
KeyF1 Key = 0xFFFF - iota
|
||||||
|
KeyF2
|
||||||
|
KeyF3
|
||||||
|
KeyF4
|
||||||
|
KeyF5
|
||||||
|
KeyF6
|
||||||
|
KeyF7
|
||||||
|
KeyF8
|
||||||
|
KeyF9
|
||||||
|
KeyF10
|
||||||
|
KeyF11
|
||||||
|
KeyF12
|
||||||
|
KeyInsert
|
||||||
|
KeyDelete
|
||||||
|
KeyHome
|
||||||
|
KeyEnd
|
||||||
|
KeyPgup
|
||||||
|
KeyPgdn
|
||||||
|
KeyArrowUp
|
||||||
|
KeyArrowDown
|
||||||
|
KeyArrowLeft
|
||||||
|
KeyArrowRight
|
||||||
|
|
||||||
|
MouseLeft
|
||||||
|
MouseMiddle
|
||||||
|
MouseRight
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
KeyCtrlTilde Key = 0x00
|
||||||
|
KeyCtrl2 Key = 0x00
|
||||||
|
KeyCtrlSpace Key = 0x00
|
||||||
|
KeyCtrlA Key = 0x01
|
||||||
|
KeyCtrlB Key = 0x02
|
||||||
|
KeyCtrlC Key = 0x03
|
||||||
|
KeyCtrlD Key = 0x04
|
||||||
|
KeyCtrlE Key = 0x05
|
||||||
|
KeyCtrlF Key = 0x06
|
||||||
|
KeyCtrlG Key = 0x07
|
||||||
|
KeyBackspace Key = 0x08
|
||||||
|
KeyCtrlH Key = 0x08
|
||||||
|
KeyTab Key = 0x09
|
||||||
|
KeyCtrlI Key = 0x09
|
||||||
|
KeyCtrlJ Key = 0x0A
|
||||||
|
KeyCtrlK Key = 0x0B
|
||||||
|
KeyCtrlL Key = 0x0C
|
||||||
|
KeyEnter Key = 0x0D
|
||||||
|
KeyCtrlM Key = 0x0D
|
||||||
|
KeyCtrlN Key = 0x0E
|
||||||
|
KeyCtrlO Key = 0x0F
|
||||||
|
KeyCtrlP Key = 0x10
|
||||||
|
KeyCtrlQ Key = 0x11
|
||||||
|
KeyCtrlR Key = 0x12
|
||||||
|
KeyCtrlS Key = 0x13
|
||||||
|
KeyCtrlT Key = 0x14
|
||||||
|
KeyCtrlU Key = 0x15
|
||||||
|
KeyCtrlV Key = 0x16
|
||||||
|
KeyCtrlW Key = 0x17
|
||||||
|
KeyCtrlX Key = 0x18
|
||||||
|
KeyCtrlY Key = 0x19
|
||||||
|
KeyCtrlZ Key = 0x1A
|
||||||
|
KeyEsc Key = 0x1B
|
||||||
|
KeyCtrlLsqBracket Key = 0x1B
|
||||||
|
KeyCtrl3 Key = 0x1B
|
||||||
|
KeyCtrl4 Key = 0x1C
|
||||||
|
KeyCtrlBackslash Key = 0x1C
|
||||||
|
KeyCtrl5 Key = 0x1D
|
||||||
|
KeyCtrlRsqBracket Key = 0x1D
|
||||||
|
KeyCtrl6 Key = 0x1E
|
||||||
|
KeyCtrl7 Key = 0x1F
|
||||||
|
KeyCtrlSlash Key = 0x1F
|
||||||
|
KeyCtrlUnderscore Key = 0x1F
|
||||||
|
KeySpace Key = 0x20
|
||||||
|
KeyBackspace2 Key = 0x7F
|
||||||
|
KeyCtrl8 Key = 0x7F
|
||||||
|
)
|
||||||
46
uiterm/label.go
Normal file
46
uiterm/label.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package uiterm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Label struct {
|
||||||
|
Text string
|
||||||
|
Fg Attribute
|
||||||
|
Bg Attribute
|
||||||
|
|
||||||
|
x0, y0, x1, y1 int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Label) SetActive(ui *Ui, active bool) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Label) SetBounds(ui *Ui, x0, y0, x1, y1 int) {
|
||||||
|
l.x0 = x0
|
||||||
|
l.y0 = y0
|
||||||
|
l.x1 = x1
|
||||||
|
l.y1 = y1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Label) Draw(ui *Ui) {
|
||||||
|
reader := strings.NewReader(l.Text)
|
||||||
|
for y := l.y0; y < l.y1; y++ {
|
||||||
|
for x := l.x0; x < l.x1; x++ {
|
||||||
|
var chr rune
|
||||||
|
if ch, _, err := reader.ReadRune(); err != nil {
|
||||||
|
chr = ' '
|
||||||
|
} else {
|
||||||
|
chr = ch
|
||||||
|
}
|
||||||
|
termbox.SetCell(x, y, chr, termbox.Attribute(l.Fg), termbox.Attribute(l.Bg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Label) KeyEvent(ui *Ui, mod Modifier, key Key) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Label) CharacterEvent(ui *Ui, chr rune) {
|
||||||
|
}
|
||||||
88
uiterm/textbox.go
Normal file
88
uiterm/textbox.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package uiterm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InputFunc func(ui *Ui, textbox *Textbox, text string)
|
||||||
|
|
||||||
|
type Textbox struct {
|
||||||
|
Text string
|
||||||
|
Fg Attribute
|
||||||
|
Bg Attribute
|
||||||
|
|
||||||
|
Input InputFunc
|
||||||
|
|
||||||
|
active bool
|
||||||
|
x0, y0, x1, y1 int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Textbox) SetBounds(ui *Ui, x0, y0, x1, y1 int) {
|
||||||
|
t.x0 = x0
|
||||||
|
t.y0 = y0
|
||||||
|
t.x1 = x1
|
||||||
|
t.y1 = y1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Textbox) SetActive(ui *Ui, active bool) {
|
||||||
|
t.active = active
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Textbox) Draw(ui *Ui) {
|
||||||
|
var setCursor = false
|
||||||
|
reader := strings.NewReader(t.Text)
|
||||||
|
for y := t.y0; y < t.y1; y++ {
|
||||||
|
for x := t.x0; x < t.x1; x++ {
|
||||||
|
var chr rune
|
||||||
|
if ch, _, err := reader.ReadRune(); err != nil {
|
||||||
|
if t.active && !setCursor {
|
||||||
|
termbox.SetCursor(x, y)
|
||||||
|
setCursor = true
|
||||||
|
}
|
||||||
|
chr = ' '
|
||||||
|
} else {
|
||||||
|
chr = ch
|
||||||
|
}
|
||||||
|
termbox.SetCell(x, y, chr, termbox.Attribute(t.Fg), termbox.Attribute(t.Bg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Textbox) KeyEvent(ui *Ui, mod Modifier, key Key) {
|
||||||
|
redraw := false
|
||||||
|
switch key {
|
||||||
|
case KeyCtrlC:
|
||||||
|
t.Text = ""
|
||||||
|
redraw = true
|
||||||
|
case KeyEnter:
|
||||||
|
if t.Input != nil {
|
||||||
|
t.Input(ui, t, t.Text)
|
||||||
|
}
|
||||||
|
t.Text = ""
|
||||||
|
redraw = true
|
||||||
|
case KeySpace:
|
||||||
|
t.Text = t.Text + " "
|
||||||
|
redraw = true
|
||||||
|
case KeyBackspace:
|
||||||
|
case KeyBackspace2:
|
||||||
|
if len(t.Text) > 0 {
|
||||||
|
if r, size := utf8.DecodeLastRuneInString(t.Text); r != utf8.RuneError {
|
||||||
|
t.Text = t.Text[:len(t.Text)-size]
|
||||||
|
redraw = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if redraw {
|
||||||
|
t.Draw(ui)
|
||||||
|
termbox.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Textbox) CharacterEvent(ui *Ui, chr rune) {
|
||||||
|
t.Text = t.Text + string(chr)
|
||||||
|
t.Draw(ui)
|
||||||
|
termbox.Flush()
|
||||||
|
}
|
||||||
141
uiterm/textview.go
Normal file
141
uiterm/textview.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package uiterm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Textview struct {
|
||||||
|
Lines []string
|
||||||
|
CurrentLine int
|
||||||
|
Fg Attribute
|
||||||
|
Bg Attribute
|
||||||
|
|
||||||
|
parsedLines []string
|
||||||
|
x0, y0, x1, y1 int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Textview) SetActive(ui *Ui, active bool) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Textview) SetBounds(ui *Ui, x0, y0, x1, y1 int) {
|
||||||
|
t.x0 = x0
|
||||||
|
t.y0 = y0
|
||||||
|
t.x1 = x1
|
||||||
|
t.y1 = y1
|
||||||
|
t.updateParsedLines()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Textview) ScrollUp() {
|
||||||
|
if newLine := t.CurrentLine + 1; newLine < len(t.parsedLines) {
|
||||||
|
t.CurrentLine = newLine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Textview) ScrollDown() {
|
||||||
|
if newLine := t.CurrentLine - 1; newLine >= 0 {
|
||||||
|
t.CurrentLine = newLine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Textview) ScrollTop() {
|
||||||
|
if newLine := len(t.parsedLines) - 1; newLine > 0 {
|
||||||
|
t.CurrentLine = newLine
|
||||||
|
} else {
|
||||||
|
t.CurrentLine = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Textview) ScrollBottom() {
|
||||||
|
t.CurrentLine = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Textview) updateParsedLines() {
|
||||||
|
width := t.x1 - t.x0 - 3
|
||||||
|
|
||||||
|
if t.Lines == nil || width <= 0 {
|
||||||
|
t.parsedLines = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed := make([]string, 0, len(t.Lines))
|
||||||
|
for _, line := range t.Lines {
|
||||||
|
current := ""
|
||||||
|
chars := 0
|
||||||
|
reader := strings.NewReader(line)
|
||||||
|
for {
|
||||||
|
if chars >= width {
|
||||||
|
parsed = append(parsed, current)
|
||||||
|
chars = 0
|
||||||
|
current = ""
|
||||||
|
}
|
||||||
|
if reader.Len() <= 0 {
|
||||||
|
if chars > 0 {
|
||||||
|
parsed = append(parsed, current)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if ch, _, err := reader.ReadRune(); err == nil {
|
||||||
|
current = current + string(ch)
|
||||||
|
chars++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.parsedLines = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Textview) AddLine(line string) {
|
||||||
|
t.Lines = append(t.Lines, line)
|
||||||
|
t.updateParsedLines()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Textview) Clear() {
|
||||||
|
t.Lines = nil
|
||||||
|
t.CurrentLine = 0
|
||||||
|
t.parsedLines = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Textview) Draw(ui *Ui) {
|
||||||
|
var reader *strings.Reader
|
||||||
|
line := len(t.parsedLines) - 1 - t.CurrentLine
|
||||||
|
if line < 0 {
|
||||||
|
line = 0
|
||||||
|
}
|
||||||
|
totalLines := len(t.parsedLines)
|
||||||
|
if totalLines == 0 {
|
||||||
|
totalLines = 1
|
||||||
|
}
|
||||||
|
currentScrollLine := t.y1 - 1 - int((float32(t.CurrentLine)/float32(totalLines))*float32(t.y1-t.y0))
|
||||||
|
for y := t.y1 - 1; y >= t.y0; y-- {
|
||||||
|
if t.parsedLines != nil && line >= 0 {
|
||||||
|
reader = strings.NewReader(t.parsedLines[line])
|
||||||
|
} else {
|
||||||
|
reader = nil
|
||||||
|
}
|
||||||
|
for x := t.x0; x < t.x1; x++ {
|
||||||
|
var chr rune = ' '
|
||||||
|
if x == t.x1-1 { // scrollbar
|
||||||
|
if y == currentScrollLine {
|
||||||
|
chr = '█'
|
||||||
|
} else {
|
||||||
|
chr = '░'
|
||||||
|
}
|
||||||
|
} else if x < t.x1-3 {
|
||||||
|
if reader != nil {
|
||||||
|
if ch, _, err := reader.ReadRune(); err == nil {
|
||||||
|
chr = ch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
termbox.SetCell(x, y, chr, termbox.Attribute(t.Fg), termbox.Attribute(t.Bg))
|
||||||
|
}
|
||||||
|
line--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Textview) KeyEvent(ui *Ui, mod Modifier, key Key) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Textview) CharacterEvent(ui *Ui, chr rune) {
|
||||||
|
}
|
||||||
140
uiterm/tree.go
Normal file
140
uiterm/tree.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package uiterm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TreeItem interface {
|
||||||
|
TreeItemStyle(active bool) (Attribute, Attribute)
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type renderedTreeItem struct {
|
||||||
|
//String string
|
||||||
|
Level int
|
||||||
|
Item TreeItem
|
||||||
|
}
|
||||||
|
|
||||||
|
type TreeFunc func(item TreeItem) []TreeItem
|
||||||
|
type TreeListener func(ui *Ui, tree *Tree, item TreeItem)
|
||||||
|
|
||||||
|
type Tree struct {
|
||||||
|
Fg Attribute
|
||||||
|
Bg Attribute
|
||||||
|
Generator TreeFunc
|
||||||
|
Listener TreeListener
|
||||||
|
|
||||||
|
lines []renderedTreeItem
|
||||||
|
activeLine int
|
||||||
|
active bool
|
||||||
|
x0, y0, x1, y1 int
|
||||||
|
}
|
||||||
|
|
||||||
|
func bounded(i, lower, upper int) int {
|
||||||
|
if i < lower {
|
||||||
|
return lower
|
||||||
|
}
|
||||||
|
if i > upper {
|
||||||
|
return upper
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tree) SetBounds(ui *Ui, x0, y0, x1, y1 int) {
|
||||||
|
t.x0 = x0
|
||||||
|
t.y0 = y0
|
||||||
|
t.x1 = x1
|
||||||
|
t.y1 = y1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tree) Rebuild() {
|
||||||
|
if t.Generator == nil {
|
||||||
|
t.lines = []renderedTreeItem{}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := []renderedTreeItem{}
|
||||||
|
for _, item := range t.Generator(nil) {
|
||||||
|
children := t.rebuild_rec(item, 0)
|
||||||
|
if children != nil {
|
||||||
|
lines = append(lines, children...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.lines = lines
|
||||||
|
t.activeLine = bounded(t.activeLine, 0, len(t.lines)-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tree) rebuild_rec(parent TreeItem, level int) []renderedTreeItem {
|
||||||
|
if parent == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lines := []renderedTreeItem{
|
||||||
|
renderedTreeItem{
|
||||||
|
Level: level,
|
||||||
|
Item: parent,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, item := range t.Generator(parent) {
|
||||||
|
children := t.rebuild_rec(item, level+1)
|
||||||
|
if children != nil {
|
||||||
|
lines = append(lines, children...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tree) Draw(ui *Ui) {
|
||||||
|
if t.lines == nil {
|
||||||
|
t.Rebuild()
|
||||||
|
}
|
||||||
|
|
||||||
|
line := 0
|
||||||
|
for y := t.y0; y < t.y1; y++ {
|
||||||
|
var reader *strings.Reader
|
||||||
|
var item TreeItem
|
||||||
|
level := 0
|
||||||
|
if line < len(t.lines) {
|
||||||
|
item = t.lines[line].Item
|
||||||
|
level = t.lines[line].Level
|
||||||
|
reader = strings.NewReader(item.String())
|
||||||
|
}
|
||||||
|
for x := t.x0; x < t.x1; x++ {
|
||||||
|
var chr rune = ' '
|
||||||
|
fg := t.Fg
|
||||||
|
bg := t.Bg
|
||||||
|
dx := x - t.x0
|
||||||
|
dy := y - t.y0
|
||||||
|
if reader != nil && level*2 <= dx {
|
||||||
|
if ch, _, err := reader.ReadRune(); err == nil {
|
||||||
|
chr = ch
|
||||||
|
fg, bg = item.TreeItemStyle(t.active && t.activeLine == dy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
termbox.SetCell(x, y, chr, termbox.Attribute(fg), termbox.Attribute(bg))
|
||||||
|
}
|
||||||
|
line++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tree) SetActive(ui *Ui, active bool) {
|
||||||
|
t.active = active
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tree) KeyEvent(ui *Ui, mod Modifier, key Key) {
|
||||||
|
switch key {
|
||||||
|
case KeyArrowUp:
|
||||||
|
t.activeLine = bounded(t.activeLine-1, 0, len(t.lines)-1)
|
||||||
|
case KeyArrowDown:
|
||||||
|
t.activeLine = bounded(t.activeLine+1, 0, len(t.lines)-1)
|
||||||
|
case KeyEnter:
|
||||||
|
if t.Listener != nil && t.activeLine >= 0 && t.activeLine < len(t.lines) {
|
||||||
|
t.Listener(ui, t, t.lines[t.activeLine].Item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tree) CharacterEvent(ui *Ui, ch rune) {
|
||||||
|
}
|
||||||
170
uiterm/ui.go
Normal file
170
uiterm/ui.go
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
package uiterm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayoutFunc func(ui *Ui, width, height int)
|
||||||
|
|
||||||
|
type KeyListener func(ui *Ui, key Key)
|
||||||
|
|
||||||
|
type UiManager interface {
|
||||||
|
OnUiInitialize(ui *Ui)
|
||||||
|
OnUiResize(ui *Ui, width, height int)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ui struct {
|
||||||
|
close chan bool
|
||||||
|
manager UiManager
|
||||||
|
|
||||||
|
elements map[string]*uiElement
|
||||||
|
activeElement *uiElement
|
||||||
|
|
||||||
|
keyListeners map[Key][]KeyListener
|
||||||
|
|
||||||
|
fg Attribute
|
||||||
|
bg Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
type uiElement struct {
|
||||||
|
X0, Y0, X1, Y1 int
|
||||||
|
View View
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(manager UiManager) *Ui {
|
||||||
|
ui := &Ui{
|
||||||
|
close: make(chan bool, 10),
|
||||||
|
elements: make(map[string]*uiElement),
|
||||||
|
manager: manager,
|
||||||
|
keyListeners: make(map[Key][]KeyListener),
|
||||||
|
}
|
||||||
|
return ui
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Ui) Close() {
|
||||||
|
if termbox.IsInit {
|
||||||
|
ui.close <- true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Ui) Refresh() {
|
||||||
|
if termbox.IsInit {
|
||||||
|
termbox.Clear(termbox.Attribute(ui.fg), termbox.Attribute(ui.bg))
|
||||||
|
termbox.HideCursor()
|
||||||
|
for _, element := range ui.elements {
|
||||||
|
element.View.Draw(ui)
|
||||||
|
}
|
||||||
|
termbox.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Ui) Active() View {
|
||||||
|
return ui.activeElement.View
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Ui) SetActive(name string) {
|
||||||
|
element, _ := ui.elements[name]
|
||||||
|
if ui.activeElement != nil {
|
||||||
|
ui.activeElement.View.SetActive(ui, false)
|
||||||
|
}
|
||||||
|
ui.activeElement = element
|
||||||
|
if element != nil {
|
||||||
|
element.View.SetActive(ui, true)
|
||||||
|
}
|
||||||
|
ui.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Ui) SetClear(fg, bg Attribute) {
|
||||||
|
ui.fg = fg
|
||||||
|
ui.bg = bg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Ui) Run() error {
|
||||||
|
if termbox.IsInit {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := termbox.Init(); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer termbox.Close()
|
||||||
|
termbox.SetInputMode(termbox.InputAlt)
|
||||||
|
|
||||||
|
events := make(chan termbox.Event)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
events <- termbox.PollEvent()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ui.manager.OnUiInitialize(ui)
|
||||||
|
width, height := termbox.Size()
|
||||||
|
ui.manager.OnUiResize(ui, width, height)
|
||||||
|
ui.Refresh()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ui.close:
|
||||||
|
return nil
|
||||||
|
case event := <-events:
|
||||||
|
switch event.Type {
|
||||||
|
case termbox.EventResize:
|
||||||
|
ui.manager.OnUiResize(ui, event.Width, event.Height)
|
||||||
|
ui.Refresh()
|
||||||
|
case termbox.EventKey:
|
||||||
|
if event.Ch != 0 {
|
||||||
|
ui.onCharacterEvent(event.Ch)
|
||||||
|
} else {
|
||||||
|
ui.onKeyEvent(Modifier(event.Mod), Key(event.Key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Ui) onCharacterEvent(ch rune) {
|
||||||
|
if ui.activeElement != nil {
|
||||||
|
ui.activeElement.View.CharacterEvent(ui, ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Ui) onKeyEvent(mod Modifier, key Key) {
|
||||||
|
if ui.keyListeners[key] != nil {
|
||||||
|
for _, listener := range ui.keyListeners[key] {
|
||||||
|
listener(ui, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ui.activeElement != nil {
|
||||||
|
ui.activeElement.View.KeyEvent(ui, mod, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Ui) SetView(name string, x0, y0, x1, y1 int, view View) {
|
||||||
|
if element, ok := ui.elements[name]; ok {
|
||||||
|
element.X0 = x0
|
||||||
|
element.Y0 = y0
|
||||||
|
element.X1 = x1
|
||||||
|
element.Y1 = y1
|
||||||
|
view = element.View
|
||||||
|
} else {
|
||||||
|
ui.elements[name] = &uiElement{
|
||||||
|
X0: x0,
|
||||||
|
Y0: y0,
|
||||||
|
X1: x1,
|
||||||
|
Y1: y1,
|
||||||
|
View: view,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.SetBounds(ui, x0, y0, x1, y1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Ui) View(name string) View {
|
||||||
|
if element, ok := ui.elements[name]; !ok {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return element.View
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *Ui) AddKeyListener(listener KeyListener, key Key) {
|
||||||
|
ui.keyListeners[key] = append(ui.keyListeners[key], listener)
|
||||||
|
}
|
||||||
9
uiterm/view.go
Normal file
9
uiterm/view.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package uiterm
|
||||||
|
|
||||||
|
type View interface {
|
||||||
|
SetBounds(ui *Ui, x0, y0, x1, y1 int)
|
||||||
|
Draw(ui *Ui)
|
||||||
|
SetActive(ui *Ui, active bool)
|
||||||
|
KeyEvent(ui *Ui, mod Modifier, key Key)
|
||||||
|
CharacterEvent(ui *Ui, ch rune)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user