6
1
mirror of https://git.mills.io/saltyim/saltyim.git synced 2024-06-28 17:51:04 +00:00
prologic-saltyim/internal/tui/tui.go
mlctrez 969a263d06 support for contacts, multiple chat threads, and persistence (#77)
Co-authored-by: James Mills <prologic@shortcircuit.net.au>
Co-authored-by: James Mills <james@mills.io>
Co-authored-by: mlctrez <mlctrez@gmail.com>
Reviewed-on: https://git.mills.io/saltyim/saltyim/pulls/77
Co-authored-by: mlctrez <mlctrez@noreply@mills.io>
Co-committed-by: mlctrez <mlctrez@noreply@mills.io>
2022-03-28 21:49:01 +00:00

178 lines
4.0 KiB
Go

package tui
import (
"bytes"
"context"
"fmt"
"strings"
"github.com/dim13/crc24"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/encoding"
"github.com/posener/formatter"
"github.com/rivo/tview"
sync "github.com/sasha-s/go-deadlock"
log "github.com/sirupsen/logrus"
"go.yarn.social/lextwt"
"go.mills.io/saltyim"
)
func getUserColor(s string) tcell.Color {
c := crc24.Sum([]byte(s))
return tcell.NewRGBColor(
int32(c>>0x10),
int32(c>>0x08),
int32(c),
)
}
type ChatTUI struct {
mu sync.RWMutex
cli *saltyim.Client
user string
addr *saltyim.Addr
// Configurations.
palette map[string]string
}
// NewChatTUI initializes a new chatApp.
// Sets up connection with broker, and initializes UI.
func NewChatTUI(cli *saltyim.Client, user string) (*ChatTUI, error) {
addr, err := saltyim.LookupAddr(user)
if err != nil {
return nil, fmt.Errorf("error looking up user %s: %s", user, err)
}
return &ChatTUI{
cli: cli,
user: user,
addr: addr,
palette: map[string]string{
"background": "000000",
"border": "4C5C68",
"title": "46494C",
"date": "E76F51",
"text": "577399",
},
}, nil
}
// newMessageHandler returns an InputHandler that handles outgoing messages.
func (c *ChatTUI) newMessageHandler(outCh chan<- string) InputHandler {
handler := func(message string) {
c.mu.RLock()
outCh <- message
c.mu.RUnlock()
}
return handler
}
// updateChatBox updates chatBox component with incoming messages.
func (c *ChatTUI) updateChatBox(inCh <-chan string, app *tview.Application,
chatBox *tview.TextView) {
for in := range inCh {
s, err := lextwt.ParseSalty(in)
if err != nil {
continue
}
switch s := s.(type) {
case *lextwt.SaltyEvent:
// Ignored for now
case *lextwt.SaltyText:
if s.User.String() != c.cli.Me().String() && s.User.String() != c.user {
continue
}
buf := &bytes.Buffer{}
_, _, width, _ := chatBox.GetInnerRect()
f := formatter.Formatter{
Writer: buf,
Indent: []byte("> "),
Width: width - 1,
}
if _, err := f.Write([]byte(s.LiteralText())); err != nil {
log.WithError(err).Error("error formatting message")
continue
}
c.mu.RLock()
app.QueueUpdateDraw(func() {
fmt.Fprintf(chatBox,
"[#%s]%s [#%x]%s\n[#%s]%s\n\n",
c.palette["date"],
s.Timestamp.DateTime().Local().Format(saltyim.DateTimeFormat),
getUserColor(s.User.String()).Hex(),
s.User.String(),
c.palette["text"],
buf.String(),
)
})
c.mu.RUnlock()
}
}
}
// setScreen initializes the layout and UI components.
func (c *ChatTUI) SetScreen(inCh <-chan string, outCh chan<- string) {
encoding.Register()
app := tview.NewApplication()
chatTitle := fmt.Sprintf("Chatting to %s via %s", c.user, c.addr.Endpoint().String())
inputTitle := fmt.Sprintf("Connected to %s as %s", c.cli.Me().Endpoint(), c.cli.Me())
// Generate UI components.
c.mu.RLock()
chatBox := NewChatBox(c.palette, chatTitle)
inputField := NewChatInput(c.palette, inputTitle, c.newMessageHandler(outCh))
c.mu.RUnlock()
// Layout the widgets in flex view.
flex := tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(chatBox, 0, 1, false).
AddItem(inputField, 3, 0, true)
go c.updateChatBox(inCh, app, chatBox)
err := app.SetRoot(flex, true).SetFocus(inputField).EnableMouse(true).Run()
if err != nil {
log.Fatal(err)
}
}
// Open bi-directional stream between client and server.
func (c *ChatTUI) RunChat(inCh chan<- string, outCh <-chan string) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Receives incoming messages on a separate goroutine to be non-blocking.
go func() {
for msg := range c.cli.Subscribe(ctx, "", "", "") {
inCh <- msg.Text
}
}()
// Forward/send outgoing messages.
for msg := range outCh {
if strings.TrimSpace(msg) == "" {
continue
}
if err := c.cli.Send(c.user, msg); err != nil {
log.WithError(err).Fatal("error sending message")
}
inCh <- string(saltyim.PackMessage(c.cli.Me(), msg))
}
}