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