--- /dev/null
+# 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>)
--- /dev/null
+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
+}
--- /dev/null
+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) {
+}
--- /dev/null
+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()
+}
--- /dev/null
+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)
+}
--- /dev/null
+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...)
+}
--- /dev/null
+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
+)
--- /dev/null
+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
+)
--- /dev/null
+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) {
+}
--- /dev/null
+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()
+}
--- /dev/null
+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) {
+}
--- /dev/null
+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) {
+}
--- /dev/null
+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)
+}
--- /dev/null
+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)
+}