mirror of
https://git.mills.io/saltyim/saltyim.git
synced 2024-06-28 17:51:04 +00:00
![mlctrez](/assets/img/avatar_default.png)
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>
178 lines
4.0 KiB
Go
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))
|
|
}
|
|
}
|