-
Notifications
You must be signed in to change notification settings - Fork 32
/
ui.go
215 lines (178 loc) · 5.77 KB
/
ui.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
package main
import (
"fmt"
"io"
"log"
"time"
"github.com/gdamore/tcell/v2"
"github.com/libp2p/go-libp2p/core/peer"
"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
sysW 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()
})
// make a text view to contain our error messages
sysBox := tview.NewTextView()
sysBox.SetDynamicColors(true)
sysBox.SetBorder(true)
sysBox.SetTitle("System")
// 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.
sysBox.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, 3, false).
AddItem(sysBox, 0, 2, false).
AddItem(input, 2, 1, true)
app.SetRoot(flex, true)
return &ChatUI{
cr: cr,
app: app,
peersList: peersList,
msgW: msgBox,
sysW: sysBox,
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) {
p := peer.ID(cm.SenderID)
prompt := withColor("green", fmt.Sprintf("<%s>:", shortID(p)))
fmt.Fprintf(ui.msgW, "%s %s\n", prompt, cm.Message)
}
// displayChatMessage writes a ChatMessage from the room to the message window,
// with the sender's nick highlighted in green.
func (ui *ChatUI) displaySysMessage(cm *ChatMessage) {
fmt.Fprintf(ui.sysW, "%s\n", cm.Message)
log.Println(cm.Message)
}
// displaySelfMessage writes a message from ourself 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 s := <-ui.cr.SysMessages:
// when we receive a message from the chat room, print it to the message window
ui.displaySysMessage(s)
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)
}