mirror of
				https://source.quilibrium.com/quilibrium/ceremonyclient.git
				synced 2025-11-04 03:07:28 +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)
 | 
						|
}
 |