@@ -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>) |
@@ -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 | |||||
} |
@@ -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) { | |||||
} |
@@ -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() | |||||
} |
@@ -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) | |||||
} |
@@ -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...) | |||||
} |
@@ -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 | |||||
) |
@@ -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 | |||||
) |
@@ -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) { | |||||
} |
@@ -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() | |||||
} |
@@ -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) { | |||||
} |
@@ -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) { | |||||
} |
@@ -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) | |||||
} |
@@ -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) | |||||
} |