Compare commits
10 Commits
bbd809c73f
...
41f69052b5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41f69052b5 | ||
|
|
c95736b632 | ||
|
|
7ea5814c03 | ||
|
|
f190e073ba | ||
|
|
72d549eade | ||
|
|
06eb85ee35 | ||
|
|
147c8a774d | ||
|
|
5552874cd9 | ||
|
|
6d3146270d | ||
|
|
fc1aaf354b |
7
Makefile
7
Makefile
@@ -2,13 +2,14 @@ PREFIX ?= /usr/local
|
||||
BINDIR ?= $(PREFIX)/bin
|
||||
BINARY := barnard
|
||||
|
||||
GO ?= go
|
||||
GOFLAGS ?=
|
||||
GO ?= go
|
||||
GOFLAGS ?=
|
||||
export CGO_CFLAGS += -w -O2
|
||||
|
||||
.PHONY: build install uninstall clean fmt vet
|
||||
|
||||
build:
|
||||
$(GO) build $(GOFLAGS) -o $(BINARY) .
|
||||
@$(GO) build $(GOFLAGS) -o $(BINARY) .
|
||||
|
||||
install: build
|
||||
install -d $(DESTDIR)$(BINDIR)
|
||||
|
||||
15
README.md
15
README.md
@@ -29,11 +29,24 @@ variable (preferred over the `-password` flag, which is visible in process
|
||||
listings). Use `-password-prompt` to read the password interactively from
|
||||
stdin.
|
||||
|
||||
## Options
|
||||
|
||||
- `-server host[:port]` — server to connect to (default: `localhost:64738`)
|
||||
- `-username name` — client username
|
||||
- `-password pass` — server password (prefer `MUMBLE_PASSWORD` env var)
|
||||
- `-password-prompt` — prompt for password on stdin
|
||||
- `-certificate path` — PEM encoded certificate and private key
|
||||
- `-insecure` — skip server certificate verification
|
||||
- `-tree-width n` — width of the channel tree pane (default: `20`)
|
||||
- `-log path` — append chat messages to file
|
||||
- `-debug` — enable debug logging to stderr
|
||||
|
||||
## Manual
|
||||
|
||||
### Key bindings
|
||||
|
||||
- <kbd>F1</kbd>: toggle voice transmission
|
||||
- <kbd>F1</kbd>: toggle deafen (mute + deaf)
|
||||
- <kbd>F4</kbd>: toggle voice transmission
|
||||
- <kbd>Ctrl+L</kbd>: clear chat log
|
||||
- <kbd>Tab</kbd>: toggle focus between chat and user tree
|
||||
- <kbd>Page Up</kbd>: scroll chat up
|
||||
|
||||
24
barnard.go
24
barnard.go
@@ -2,6 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"layeh.com/barnard/uiterm"
|
||||
"layeh.com/gumble/gumble"
|
||||
@@ -14,8 +18,13 @@ type Barnard struct {
|
||||
|
||||
Address string
|
||||
TLSConfig tls.Config
|
||||
Debug bool
|
||||
Logger *log.Logger
|
||||
TreeWidth int
|
||||
LogFile *os.File
|
||||
|
||||
Stream *gumbleopenal.Stream
|
||||
Stream *gumbleopenal.Stream
|
||||
transmitting bool
|
||||
|
||||
Ui *uiterm.Ui
|
||||
UiOutput uiterm.Textview
|
||||
@@ -24,3 +33,16 @@ type Barnard struct {
|
||||
UiTree uiterm.Tree
|
||||
UiInputStatus uiterm.Label
|
||||
}
|
||||
|
||||
func (b *Barnard) debugf(format string, args ...interface{}) {
|
||||
if b.Debug && b.Logger != nil {
|
||||
b.Logger.Printf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Barnard) logMessage(line string) {
|
||||
if b.LogFile != nil {
|
||||
now := time.Now()
|
||||
fmt.Fprintf(b.LogFile, "[%s] %s\n", now.Format("2006-01-02 15:04:05"), line)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ func (b *Barnard) start() {
|
||||
b.Config.Attach(gumbleutil.AutoBitrate)
|
||||
b.Config.Attach(b)
|
||||
|
||||
b.debugf("dialing %s", b.Address)
|
||||
var err error
|
||||
_, err = gumble.DialWithDialer(new(net.Dialer), b.Address, b.Config, &b.TLSConfig)
|
||||
if err != nil {
|
||||
@@ -25,6 +26,7 @@ func (b *Barnard) start() {
|
||||
if os.Getenv("ALSOFT_LOGLEVEL") == "" {
|
||||
os.Setenv("ALSOFT_LOGLEVEL", "0")
|
||||
}
|
||||
b.debugf("initializing audio stream")
|
||||
if stream, err := gumbleopenal.New(b.Client); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
os.Exit(1)
|
||||
@@ -35,6 +37,7 @@ func (b *Barnard) start() {
|
||||
|
||||
func (b *Barnard) OnConnect(e *gumble.ConnectEvent) {
|
||||
b.Client = e.Client
|
||||
b.debugf("connected to %s", b.Client.Conn.RemoteAddr())
|
||||
|
||||
b.Ui.SetActive(uiViewInput)
|
||||
b.UiTree.Rebuild()
|
||||
@@ -48,6 +51,7 @@ func (b *Barnard) OnConnect(e *gumble.ConnectEvent) {
|
||||
}
|
||||
|
||||
func (b *Barnard) OnDisconnect(e *gumble.DisconnectEvent) {
|
||||
b.debugf("disconnected (type=%d)", e.Type)
|
||||
var reason string
|
||||
switch e.Type {
|
||||
case gumble.DisconnectError:
|
||||
@@ -63,10 +67,14 @@ func (b *Barnard) OnDisconnect(e *gumble.DisconnectEvent) {
|
||||
}
|
||||
|
||||
func (b *Barnard) OnTextMessage(e *gumble.TextMessageEvent) {
|
||||
if e.Sender != nil {
|
||||
b.debugf("message from %s (%d bytes)", e.Sender.Name, len(e.Message))
|
||||
}
|
||||
b.AddOutputMessage(e.Sender, e.Message)
|
||||
}
|
||||
|
||||
func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) {
|
||||
b.debugf("user change: %s (type=%d)", e.User.Name, e.Type)
|
||||
if e.Type.Has(gumble.UserChangeChannel) && e.User == b.Client.Self {
|
||||
b.UpdateInputStatus(fmt.Sprintf("To: %s", e.User.Channel.Name))
|
||||
}
|
||||
@@ -80,6 +88,7 @@ func (b *Barnard) OnChannelChange(e *gumble.ChannelChangeEvent) {
|
||||
}
|
||||
|
||||
func (b *Barnard) OnPermissionDenied(e *gumble.PermissionDeniedEvent) {
|
||||
b.debugf("permission denied (type=%d)", e.Type)
|
||||
var info string
|
||||
switch e.Type {
|
||||
case gumble.PermissionDeniedOther:
|
||||
|
||||
19
main.go
19
main.go
@@ -4,6 +4,7 @@ import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
@@ -23,6 +24,9 @@ func main() {
|
||||
passwordPrompt := flag.Bool("password-prompt", false, "prompt for server password on stdin")
|
||||
insecure := flag.Bool("insecure", false, "skip server certificate verification")
|
||||
certificate := flag.String("certificate", "", "PEM encoded certificate and private key")
|
||||
debug := flag.Bool("debug", false, "enable debug logging to stderr")
|
||||
treeWidth := flag.Int("tree-width", 20, "width of the channel tree pane")
|
||||
logFile := flag.String("log", "", "write chat messages to file")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
@@ -56,6 +60,21 @@ func main() {
|
||||
|
||||
b.Config.Username = *username
|
||||
b.Config.Password = pass
|
||||
b.TreeWidth = *treeWidth
|
||||
b.Debug = *debug
|
||||
if b.Debug {
|
||||
b.Logger = log.New(os.Stderr, "barnard: ", log.Ltime)
|
||||
b.debugf("connecting to %s as %q", addr, *username)
|
||||
}
|
||||
if *logFile != "" {
|
||||
f, err := os.OpenFile(*logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer f.Close()
|
||||
b.LogFile = f
|
||||
}
|
||||
|
||||
if *insecure {
|
||||
b.TLSConfig.InsecureSkipVerify = true
|
||||
|
||||
107
ui.go
107
ui.go
@@ -33,6 +33,7 @@ func (b *Barnard) UpdateInputStatus(status string) {
|
||||
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.logMessage(line)
|
||||
}
|
||||
|
||||
func (b *Barnard) AddOutputMessage(sender *gumble.User, message string) {
|
||||
@@ -43,18 +44,59 @@ func (b *Barnard) AddOutputMessage(sender *gumble.User, message string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Barnard) OnVoiceToggle(ui *uiterm.Ui, key uiterm.Key) {
|
||||
if b.UiStatus.Text == " Tx " {
|
||||
func (b *Barnard) updateStatus() {
|
||||
if b.Client == nil || b.Client.Self == nil {
|
||||
return
|
||||
}
|
||||
if b.Client.Self.SelfDeafened {
|
||||
b.UiStatus.Text = " Deaf "
|
||||
b.UiStatus.Fg = uiterm.ColorWhite | uiterm.AttrBold
|
||||
b.UiStatus.Bg = uiterm.ColorBlue
|
||||
} else if b.transmitting {
|
||||
b.UiStatus.Text = " Tx "
|
||||
b.UiStatus.Fg = uiterm.ColorWhite | uiterm.AttrBold
|
||||
b.UiStatus.Bg = uiterm.ColorRed
|
||||
} else {
|
||||
b.UiStatus.Text = " Idle "
|
||||
b.UiStatus.Fg = uiterm.ColorBlack
|
||||
b.UiStatus.Bg = uiterm.ColorWhite
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Barnard) OnDeafenToggle(ui *uiterm.Ui, key uiterm.Key) {
|
||||
if b.Client == nil || b.Client.Self == nil {
|
||||
return
|
||||
}
|
||||
deaf := !b.Client.Self.SelfDeafened
|
||||
b.Client.Self.SetSelfDeafened(deaf)
|
||||
if deaf && b.transmitting {
|
||||
b.transmitting = false
|
||||
b.Stream.StopSource()
|
||||
}
|
||||
if !deaf {
|
||||
b.Client.Self.SetSelfMuted(false)
|
||||
}
|
||||
b.debugf("deafen=%v", deaf)
|
||||
b.updateStatus()
|
||||
ui.Refresh()
|
||||
}
|
||||
|
||||
func (b *Barnard) OnVoiceToggle(ui *uiterm.Ui, key uiterm.Key) {
|
||||
if b.Client == nil || b.Client.Self == nil {
|
||||
return
|
||||
}
|
||||
if b.Client.Self.SelfDeafened {
|
||||
return
|
||||
}
|
||||
if b.transmitting {
|
||||
b.transmitting = false
|
||||
b.Stream.StopSource()
|
||||
} else {
|
||||
b.UiStatus.Fg = uiterm.ColorWhite | uiterm.AttrBold
|
||||
b.UiStatus.Bg = uiterm.ColorRed
|
||||
b.UiStatus.Text = " Tx "
|
||||
b.transmitting = true
|
||||
b.Stream.StartSource()
|
||||
}
|
||||
b.debugf("transmit=%v", b.transmitting)
|
||||
b.updateStatus()
|
||||
ui.Refresh()
|
||||
}
|
||||
|
||||
@@ -83,8 +125,14 @@ func (b *Barnard) OnScrollOutputBottom(ui *uiterm.Ui, key uiterm.Key) {
|
||||
b.UiOutput.ScrollBottom()
|
||||
}
|
||||
|
||||
var commands = []string{"/clear", "/deafen", "/exit", "/help", "/quit"}
|
||||
|
||||
func (b *Barnard) OnFocusPress(ui *uiterm.Ui, key uiterm.Key) {
|
||||
active := b.Ui.Active()
|
||||
if active == uiViewInput && strings.HasPrefix(b.UiInput.Text, "/") {
|
||||
b.completeCommand()
|
||||
return
|
||||
}
|
||||
if active == uiViewInput {
|
||||
b.Ui.SetActive(uiViewTree)
|
||||
} else if active == uiViewTree {
|
||||
@@ -92,10 +140,51 @@ func (b *Barnard) OnFocusPress(ui *uiterm.Ui, key uiterm.Key) {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Barnard) completeCommand() {
|
||||
text := b.UiInput.Text
|
||||
var matches []string
|
||||
for _, cmd := range commands {
|
||||
if strings.HasPrefix(cmd, text) {
|
||||
matches = append(matches, cmd)
|
||||
}
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return
|
||||
}
|
||||
if len(matches) == 1 {
|
||||
b.UiInput.Text = matches[0]
|
||||
} else {
|
||||
prefix := matches[0]
|
||||
for _, m := range matches[1:] {
|
||||
for !strings.HasPrefix(m, prefix) {
|
||||
prefix = prefix[:len(prefix)-1]
|
||||
}
|
||||
}
|
||||
if len(prefix) > len(text) {
|
||||
b.UiInput.Text = prefix
|
||||
}
|
||||
}
|
||||
b.Ui.Refresh()
|
||||
}
|
||||
|
||||
func (b *Barnard) OnTextInput(ui *uiterm.Ui, textbox *uiterm.Textbox, text string) {
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
switch strings.TrimSpace(text) {
|
||||
case "/quit", "/exit":
|
||||
b.OnQuitPress(ui, 0)
|
||||
return
|
||||
case "/deafen":
|
||||
b.OnDeafenToggle(ui, 0)
|
||||
return
|
||||
case "/clear":
|
||||
b.OnClearPress(ui, 0)
|
||||
return
|
||||
case "/help":
|
||||
b.AddOutputLine("/quit /exit /deafen /clear /help")
|
||||
return
|
||||
}
|
||||
if b.Client != nil && b.Client.Self != nil {
|
||||
b.Client.Self.Channel.Send(text, false)
|
||||
b.AddOutputMessage(b.Client.Self, text)
|
||||
@@ -149,7 +238,8 @@ func (b *Barnard) OnUiInitialize(ui *uiterm.Ui) {
|
||||
ui.Add(uiViewTree, &b.UiTree)
|
||||
|
||||
b.Ui.AddKeyListener(b.OnFocusPress, uiterm.KeyTab)
|
||||
b.Ui.AddKeyListener(b.OnVoiceToggle, uiterm.KeyF1)
|
||||
b.Ui.AddKeyListener(b.OnDeafenToggle, uiterm.KeyF1)
|
||||
b.Ui.AddKeyListener(b.OnVoiceToggle, uiterm.KeyF4)
|
||||
b.Ui.AddKeyListener(b.OnQuitPress, uiterm.KeyF10)
|
||||
b.Ui.AddKeyListener(b.OnClearPress, uiterm.KeyCtrlL)
|
||||
b.Ui.AddKeyListener(b.OnScrollOutputUp, uiterm.KeyPgup)
|
||||
@@ -161,11 +251,12 @@ func (b *Barnard) OnUiInitialize(ui *uiterm.Ui) {
|
||||
}
|
||||
|
||||
func (b *Barnard) OnUiResize(ui *uiterm.Ui, width, height int) {
|
||||
tw := b.TreeWidth
|
||||
ui.SetBounds(uiViewLogo, 0, 0, 9, 1)
|
||||
ui.SetBounds(uiViewTop, 9, 0, width-6, 1)
|
||||
ui.SetBounds(uiViewStatus, width-6, 0, width, 1)
|
||||
ui.SetBounds(uiViewInput, 0, height-1, width, height)
|
||||
ui.SetBounds(uiViewInputStatus, 0, height-2, width, height-1)
|
||||
ui.SetBounds(uiViewOutput, 0, 1, width-20, height-2)
|
||||
ui.SetBounds(uiViewTree, width-20, 1, width, height-2)
|
||||
ui.SetBounds(uiViewOutput, 0, 1, width-tw, height-2)
|
||||
ui.SetBounds(uiViewTree, width-tw, 1, width, height-2)
|
||||
}
|
||||
|
||||
@@ -63,9 +63,12 @@ func (t *Textbox) uiDraw() {
|
||||
func (t *Textbox) uiKeyEvent(mod Modifier, key Key) {
|
||||
redraw := false
|
||||
switch key {
|
||||
case KeyCtrlC:
|
||||
case KeyCtrlC, KeyCtrlU, KeyCtrlK:
|
||||
t.Text = ""
|
||||
redraw = true
|
||||
case KeyCtrlW:
|
||||
t.Text = deleteWord(t.Text)
|
||||
redraw = true
|
||||
case KeyEnter:
|
||||
if t.Input != nil {
|
||||
t.Input(t.ui, t, t.Text)
|
||||
@@ -91,6 +94,16 @@ func (t *Textbox) uiKeyEvent(mod Modifier, key Key) {
|
||||
}
|
||||
}
|
||||
|
||||
func deleteWord(s string) string {
|
||||
// Trim trailing spaces, then trim non-spaces (the word)
|
||||
s = strings.TrimRight(s, " ")
|
||||
i := strings.LastIndex(s, " ")
|
||||
if i < 0 {
|
||||
return ""
|
||||
}
|
||||
return s[:i+1]
|
||||
}
|
||||
|
||||
func (t *Textbox) uiCharacterEvent(chr rune) {
|
||||
if len(t.Text) >= maxTextboxLen {
|
||||
return
|
||||
|
||||
@@ -80,7 +80,7 @@ func (ui *Ui) Active() string {
|
||||
}
|
||||
|
||||
func (ui *Ui) SetActive(name string) {
|
||||
element, _ := ui.elements[name]
|
||||
element := ui.elements[name]
|
||||
if ui.activeElement != nil {
|
||||
ui.activeElement.View.uiSetActive(false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user