Bladeren bron

initial commit

master
Tim Cooper 10 jaren geleden
commit
562dec1e35
14 gewijzigde bestanden met toevoegingen van 1171 en 0 verwijderingen
  1. +14
    -0
      README.md
  2. +21
    -0
      barnard.go
  3. +112
    -0
      client.go
  4. +62
    -0
      cmd/barnard/main.go
  5. +175
    -0
      ui.go
  6. +77
    -0
      ui_tree.go
  7. +25
    -0
      uiterm/attributes.go
  8. +91
    -0
      uiterm/keys.go
  9. +46
    -0
      uiterm/label.go
  10. +88
    -0
      uiterm/textbox.go
  11. +141
    -0
      uiterm/textview.go
  12. +140
    -0
      uiterm/tree.go
  13. +170
    -0
      uiterm/ui.go
  14. +9
    -0
      uiterm/view.go

+ 14
- 0
README.md Bestand weergeven

@@ -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
- 0
barnard.go Bestand weergeven

@@ -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
- 0
client.go Bestand weergeven

@@ -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
- 0
cmd/barnard/main.go Bestand weergeven

@@ -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
- 0
ui.go Bestand weergeven

@@ -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
- 0
ui_tree.go Bestand weergeven

@@ -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
- 0
uiterm/attributes.go Bestand weergeven

@@ -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
- 0
uiterm/keys.go Bestand weergeven

@@ -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
- 0
uiterm/label.go Bestand weergeven

@@ -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
- 0
uiterm/textbox.go Bestand weergeven

@@ -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
- 0
uiterm/textview.go Bestand weergeven

@@ -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
- 0
uiterm/tree.go Bestand weergeven

@@ -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
- 0
uiterm/ui.go Bestand weergeven

@@ -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
- 0
uiterm/view.go Bestand weergeven

@@ -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)
}

Laden…
Annuleren
Opslaan