mirror of
https://source.quilibrium.com/quilibrium/ceremonyclient.git
synced 2024-12-27 00:55:17 +00:00
186 lines
4.9 KiB
Go
186 lines
4.9 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"time"
|
||
|
|
||
|
"github.com/gdamore/tcell/v2"
|
||
|
"github.com/rivo/tview"
|
||
|
)
|
||
|
|
||
|
// ChatUI is a Text User Interface (TUI) for a ChatRoom.
|
||
|
// The Run method will draw the UI to the terminal in "fullscreen"
|
||
|
// mode. You can quit with Ctrl-C, or by typing "/quit" into the
|
||
|
// chat prompt.
|
||
|
type ChatUI struct {
|
||
|
cr *ChatRoom
|
||
|
app *tview.Application
|
||
|
peersList *tview.TextView
|
||
|
|
||
|
msgW io.Writer
|
||
|
inputCh chan string
|
||
|
doneCh chan struct{}
|
||
|
}
|
||
|
|
||
|
// NewChatUI returns a new ChatUI struct that controls the text UI.
|
||
|
// It won't actually do anything until you call Run().
|
||
|
func NewChatUI(cr *ChatRoom) *ChatUI {
|
||
|
app := tview.NewApplication()
|
||
|
|
||
|
// make a text view to contain our chat messages
|
||
|
msgBox := tview.NewTextView()
|
||
|
msgBox.SetDynamicColors(true)
|
||
|
msgBox.SetBorder(true)
|
||
|
msgBox.SetTitle(fmt.Sprintf("Room: %s", cr.roomName))
|
||
|
|
||
|
// text views are io.Writers, but they don't automatically refresh.
|
||
|
// this sets a change handler to force the app to redraw when we get
|
||
|
// new messages to display.
|
||
|
msgBox.SetChangedFunc(func() {
|
||
|
app.Draw()
|
||
|
})
|
||
|
|
||
|
// an input field for typing messages into
|
||
|
inputCh := make(chan string, 32)
|
||
|
input := tview.NewInputField().
|
||
|
SetLabel(cr.nick + " > ").
|
||
|
SetFieldWidth(0).
|
||
|
SetFieldBackgroundColor(tcell.ColorBlack)
|
||
|
|
||
|
// the done func is called when the user hits enter, or tabs out of the field
|
||
|
input.SetDoneFunc(func(key tcell.Key) {
|
||
|
if key != tcell.KeyEnter {
|
||
|
// we don't want to do anything if they just tabbed away
|
||
|
return
|
||
|
}
|
||
|
line := input.GetText()
|
||
|
if len(line) == 0 {
|
||
|
// ignore blank lines
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// bail if requested
|
||
|
if line == "/quit" {
|
||
|
app.Stop()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// send the line onto the input chan and reset the field text
|
||
|
inputCh <- line
|
||
|
input.SetText("")
|
||
|
})
|
||
|
|
||
|
// make a text view to hold the list of peers in the room, updated by ui.refreshPeers()
|
||
|
peersList := tview.NewTextView()
|
||
|
peersList.SetBorder(true)
|
||
|
peersList.SetTitle("Peers")
|
||
|
peersList.SetChangedFunc(func() { app.Draw() })
|
||
|
|
||
|
// chatPanel is a horizontal box with messages on the left and peers on the right
|
||
|
// the peers list takes 20 columns, and the messages take the remaining space
|
||
|
chatPanel := tview.NewFlex().
|
||
|
AddItem(msgBox, 0, 1, false).
|
||
|
AddItem(peersList, 20, 1, false)
|
||
|
|
||
|
// flex is a vertical box with the chatPanel on top and the input field at the bottom.
|
||
|
|
||
|
flex := tview.NewFlex().
|
||
|
SetDirection(tview.FlexRow).
|
||
|
AddItem(chatPanel, 0, 1, false).
|
||
|
AddItem(input, 1, 1, true)
|
||
|
|
||
|
app.SetRoot(flex, true)
|
||
|
|
||
|
return &ChatUI{
|
||
|
cr: cr,
|
||
|
app: app,
|
||
|
peersList: peersList,
|
||
|
msgW: msgBox,
|
||
|
inputCh: inputCh,
|
||
|
doneCh: make(chan struct{}, 1),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Run starts the chat event loop in the background, then starts
|
||
|
// the event loop for the text UI.
|
||
|
func (ui *ChatUI) Run() error {
|
||
|
go ui.handleEvents()
|
||
|
defer ui.end()
|
||
|
|
||
|
return ui.app.Run()
|
||
|
}
|
||
|
|
||
|
// end signals the event loop to exit gracefully
|
||
|
func (ui *ChatUI) end() {
|
||
|
ui.doneCh <- struct{}{}
|
||
|
}
|
||
|
|
||
|
// refreshPeers pulls the list of peers currently in the chat room and
|
||
|
// displays the last 8 chars of their peer id in the Peers panel in the ui.
|
||
|
func (ui *ChatUI) refreshPeers() {
|
||
|
peers := ui.cr.ListPeers()
|
||
|
|
||
|
// clear is thread-safe
|
||
|
ui.peersList.Clear()
|
||
|
|
||
|
for _, p := range peers {
|
||
|
fmt.Fprintln(ui.peersList, shortID(p))
|
||
|
}
|
||
|
|
||
|
ui.app.Draw()
|
||
|
}
|
||
|
|
||
|
// displayChatMessage writes a ChatMessage from the room to the message window,
|
||
|
// with the sender's nick highlighted in green.
|
||
|
func (ui *ChatUI) displayChatMessage(cm *ChatMessage) {
|
||
|
prompt := withColor("green", fmt.Sprintf("<%s>:", cm.SenderNick))
|
||
|
fmt.Fprintf(ui.msgW, "%s %s\n", prompt, cm.Message)
|
||
|
}
|
||
|
|
||
|
// displaySelfMessage writes a message from ourselves to the message window,
|
||
|
// with our nick highlighted in yellow.
|
||
|
func (ui *ChatUI) displaySelfMessage(msg string) {
|
||
|
prompt := withColor("yellow", fmt.Sprintf("<%s>:", ui.cr.nick))
|
||
|
fmt.Fprintf(ui.msgW, "%s %s\n", prompt, msg)
|
||
|
}
|
||
|
|
||
|
// handleEvents runs an event loop that sends user input to the chat room
|
||
|
// and displays messages received from the chat room. It also periodically
|
||
|
// refreshes the list of peers in the UI.
|
||
|
func (ui *ChatUI) handleEvents() {
|
||
|
peerRefreshTicker := time.NewTicker(time.Second)
|
||
|
defer peerRefreshTicker.Stop()
|
||
|
|
||
|
for {
|
||
|
select {
|
||
|
case input := <-ui.inputCh:
|
||
|
// when the user types in a line, publish it to the chat room and print to the message window
|
||
|
err := ui.cr.Publish(input)
|
||
|
if err != nil {
|
||
|
printErr("publish error: %s", err)
|
||
|
}
|
||
|
ui.displaySelfMessage(input)
|
||
|
|
||
|
case m := <-ui.cr.Messages:
|
||
|
// when we receive a message from the chat room, print it to the message window
|
||
|
ui.displayChatMessage(m)
|
||
|
|
||
|
case <-peerRefreshTicker.C:
|
||
|
// refresh the list of peers in the chat room periodically
|
||
|
ui.refreshPeers()
|
||
|
|
||
|
case <-ui.cr.ctx.Done():
|
||
|
return
|
||
|
|
||
|
case <-ui.doneCh:
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// withColor wraps a string with color tags for display in the messages text box.
|
||
|
func withColor(color, msg string) string {
|
||
|
return fmt.Sprintf("[%s]%s[-]", color, msg)
|
||
|
}
|