]> git.r.bdr.sh - rbdr/mobius/blob - hotline/ui.go
Merge pull request #62 from jhalter/add_basic_stats
[rbdr/mobius] / hotline / ui.go
1 package hotline
2
3 import (
4 "bufio"
5 "fmt"
6 "github.com/gdamore/tcell/v2"
7 "github.com/rivo/tview"
8 "gopkg.in/yaml.v3"
9 "io/ioutil"
10 "os"
11 "strconv"
12 "strings"
13 )
14
15 type UI struct {
16 chatBox *tview.TextView
17 chatInput *tview.InputField
18 App *tview.Application
19 Pages *tview.Pages
20 userList *tview.TextView
21 trackerList *tview.List
22 HLClient *Client
23 }
24
25 // pages
26 const (
27 pageServerUI = "serverUI"
28 )
29
30 func NewUI(c *Client) *UI {
31 app := tview.NewApplication()
32 chatBox := tview.NewTextView().
33 SetScrollable(true).
34 SetDynamicColors(true).
35 SetWordWrap(true).
36 SetChangedFunc(func() {
37 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
38 })
39 chatBox.Box.SetBorder(true).SetTitle("| Chat |")
40
41 chatInput := tview.NewInputField()
42 chatInput.
43 SetLabel("> ").
44 SetFieldBackgroundColor(tcell.ColorDimGray).
45 SetDoneFunc(func(key tcell.Key) {
46 // skip send if user hit enter with no other text
47 if len(chatInput.GetText()) == 0 {
48 return
49 }
50
51 _ = c.Send(
52 *NewTransaction(tranChatSend, nil,
53 NewField(fieldData, []byte(chatInput.GetText())),
54 ),
55 )
56 chatInput.SetText("") // clear the input field after chat send
57 })
58
59 chatInput.Box.SetBorder(true).SetTitle("Send")
60
61 userList := tview.
62 NewTextView().
63 SetDynamicColors(true).
64 SetChangedFunc(func() {
65 app.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
66 })
67 userList.Box.SetBorder(true).SetTitle("Users")
68
69 return &UI{
70 App: app,
71 chatBox: chatBox,
72 Pages: tview.NewPages(),
73 chatInput: chatInput,
74 userList: userList,
75 trackerList: tview.NewList(),
76 HLClient: c,
77 }
78 }
79
80 func (ui *UI) showBookmarks() *tview.List {
81 list := tview.NewList()
82 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
83 if event.Key() == tcell.KeyEsc {
84 ui.Pages.SwitchToPage("home")
85 }
86 return event
87 })
88 list.Box.SetBorder(true).SetTitle("| Bookmarks |")
89
90 shortcut := 97 // rune for "a"
91 for i, srv := range ui.HLClient.pref.Bookmarks {
92 addr := srv.Addr
93 login := srv.Login
94 pass := srv.Password
95 list.AddItem(srv.Name, srv.Addr, rune(shortcut+i), func() {
96 ui.Pages.RemovePage("joinServer")
97
98 newJS := ui.renderJoinServerForm("", addr, login, pass, "bookmarks", true, true)
99
100 ui.Pages.AddPage("joinServer", newJS, true, true)
101 })
102 }
103
104 return list
105 }
106
107 func (ui *UI) getTrackerList() *tview.List {
108 listing, err := GetListing(ui.HLClient.pref.Tracker)
109 if err != nil {
110 // TODO
111 }
112
113 list := tview.NewList()
114 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
115 if event.Key() == tcell.KeyEsc {
116 ui.Pages.SwitchToPage("home")
117 }
118 return event
119 })
120 list.Box.SetBorder(true).SetTitle("| Servers |")
121
122 shortcut := 97 // rune for "a"
123 for i, srv := range listing {
124 addr := srv.Addr()
125 srvName := srv.Name
126 list.AddItem(string(srv.Name), string(srv.Description), rune(shortcut+i), func() {
127 ui.Pages.RemovePage("joinServer")
128
129 newJS := ui.renderJoinServerForm(string(srvName), addr, GuestAccount, "", trackerListPage, false, true)
130
131 ui.Pages.AddPage("joinServer", newJS, true, true)
132 ui.Pages.ShowPage("joinServer")
133 })
134 }
135
136 return list
137 }
138
139 func (ui *UI) renderSettingsForm() *tview.Flex {
140 iconStr := strconv.Itoa(ui.HLClient.pref.IconID)
141 settingsForm := tview.NewForm()
142 settingsForm.AddInputField("Your Name", ui.HLClient.pref.Username, 0, nil, nil)
143 settingsForm.AddInputField("IconID", iconStr, 0, func(idStr string, _ rune) bool {
144 _, err := strconv.Atoi(idStr)
145 return err == nil
146 }, nil)
147 settingsForm.AddInputField("Tracker", ui.HLClient.pref.Tracker, 0, nil, nil)
148 settingsForm.AddButton("Save", func() {
149 usernameInput := settingsForm.GetFormItem(0).(*tview.InputField).GetText()
150 if len(usernameInput) == 0 {
151 usernameInput = "unnamed"
152 }
153 ui.HLClient.pref.Username = usernameInput
154 iconStr = settingsForm.GetFormItem(1).(*tview.InputField).GetText()
155 ui.HLClient.pref.IconID, _ = strconv.Atoi(iconStr)
156 ui.HLClient.pref.Tracker = settingsForm.GetFormItem(2).(*tview.InputField).GetText()
157
158 out, err := yaml.Marshal(&ui.HLClient.pref)
159 if err != nil {
160 // TODO: handle err
161 }
162 // TODO: handle err
163 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
164 if err != nil {
165 println(ui.HLClient.cfgPath)
166 panic(err)
167 }
168 ui.Pages.RemovePage("settings")
169 })
170 settingsForm.SetBorder(true)
171 settingsForm.SetCancelFunc(func() {
172 ui.Pages.RemovePage("settings")
173 })
174 settingsPage := tview.NewFlex().SetDirection(tview.FlexRow)
175 settingsPage.Box.SetBorder(true).SetTitle("Settings")
176 settingsPage.AddItem(settingsForm, 0, 1, true)
177
178 centerFlex := tview.NewFlex().
179 AddItem(nil, 0, 1, false).
180 AddItem(tview.NewFlex().
181 SetDirection(tview.FlexRow).
182 AddItem(nil, 0, 1, false).
183 AddItem(settingsForm, 15, 1, true).
184 AddItem(nil, 0, 1, false), 40, 1, true).
185 AddItem(nil, 0, 1, false)
186
187 return centerFlex
188 }
189
190 func (ui *UI) joinServer(addr, login, password string) error {
191 // append default port to address if no port supplied
192 if len(strings.Split(addr, ":")) == 1 {
193 addr += ":5500"
194 }
195 if err := ui.HLClient.JoinServer(addr, login, password); err != nil {
196 return fmt.Errorf("Error joining server: %v\n", err)
197 }
198
199 go func() {
200 // Create a new scanner for parsing incoming bytes into transaction tokens
201 scanner := bufio.NewScanner(ui.HLClient.Connection)
202 scanner.Split(transactionScanner)
203
204 // Scan for new transactions and handle them as they come in.
205 for scanner.Scan() {
206 // Make a new []byte slice and copy the scanner bytes to it. This is critical to avoid a data race as the
207 // scanner re-uses the buffer for subsequent scans.
208 buf := make([]byte, len(scanner.Bytes()))
209 copy(buf, scanner.Bytes())
210
211 var t Transaction
212 _, err := t.Write(buf)
213 if err != nil {
214 break
215 }
216 if err := ui.HLClient.HandleTransaction(&t); err != nil {
217 ui.HLClient.Logger.Errorw("Error handling transaction", "err", err)
218 }
219 }
220
221 if scanner.Err() == nil {
222 loginErrModal := tview.NewModal().
223 AddButtons([]string{"Ok"}).
224 SetText("The server connection has closed.").
225 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
226 ui.Pages.SwitchToPage("home")
227 })
228 loginErrModal.Box.SetTitle("Server Connection Error")
229
230 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
231 ui.App.Draw()
232 return
233 }
234 ui.Pages.SwitchToPage("home")
235
236 }()
237
238 return nil
239 }
240
241 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
242 joinServerForm := tview.NewForm()
243 joinServerForm.
244 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
245 // return false
246 // }, nil).
247 AddInputField("Server", server, 0, nil, nil).
248 AddInputField("Login", login, 0, nil, nil).
249 AddPasswordField("Password", password, 0, '*', nil).
250 AddCheckbox("Save", save, func(checked bool) {
251 ui.HLClient.Logger.Infow("saving bookmark")
252 // TODO: Implement bookmark saving
253
254 ui.HLClient.pref.AddBookmark(joinServerForm.GetFormItem(0).(*tview.InputField).GetText(), joinServerForm.GetFormItem(0).(*tview.InputField).GetText(), joinServerForm.GetFormItem(1).(*tview.InputField).GetText(), joinServerForm.GetFormItem(2).(*tview.InputField).GetText())
255 out, err := yaml.Marshal(ui.HLClient.pref)
256 if err != nil {
257 panic(err)
258 }
259
260 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
261 if err != nil {
262 panic(err)
263 }
264 // pref := ui.HLClient.pref
265 }).
266 AddButton("Cancel", func() {
267 ui.Pages.SwitchToPage(backPage)
268 }).
269 AddButton("Connect", func() {
270 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
271 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
272 err := ui.joinServer(
273 srvAddr,
274 loginInput,
275 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
276 )
277 if name == "" {
278 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
279 }
280 ui.HLClient.serverName = name
281
282 if err != nil {
283 ui.HLClient.Logger.Errorw("login error", "err", err)
284 loginErrModal := tview.NewModal().
285 AddButtons([]string{"Oh no"}).
286 SetText(err.Error()).
287 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
288 ui.Pages.SwitchToPage(backPage)
289 })
290
291 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
292 }
293
294 // Save checkbox
295 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
296 // TODO: implement bookmark saving
297 }
298 })
299
300 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
301 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
302 if event.Key() == tcell.KeyEscape {
303 ui.Pages.SwitchToPage(backPage)
304 }
305 return event
306 })
307
308 if defaultConnect {
309 joinServerForm.SetFocus(5)
310 }
311
312 joinServerPage := tview.NewFlex().
313 AddItem(nil, 0, 1, false).
314 AddItem(tview.NewFlex().
315 SetDirection(tview.FlexRow).
316 AddItem(nil, 0, 1, false).
317 AddItem(joinServerForm, 14, 1, true).
318 AddItem(nil, 0, 1, false), 40, 1, true).
319 AddItem(nil, 0, 1, false)
320
321 return joinServerPage
322 }
323
324 func (ui *UI) renderServerUI() *tview.Flex {
325 ui.chatBox.SetText("") // clear any previously existing chatbox text
326 commandList := tview.NewTextView().SetDynamicColors(true)
327 commandList.
328 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
329 SetBorder(true).
330 SetTitle("| Keyboard Shortcuts| ")
331
332 modal := tview.NewModal().
333 SetText("Disconnect from the server?").
334 AddButtons([]string{"Cancel", "Exit"}).
335 SetFocus(1)
336 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
337 if buttonIndex == 1 {
338 _ = ui.HLClient.Disconnect()
339 ui.Pages.RemovePage(pageServerUI)
340 ui.Pages.SwitchToPage("home")
341 } else {
342 ui.Pages.HidePage("modal")
343 }
344 })
345
346 serverUI := tview.NewFlex().
347 AddItem(tview.NewFlex().
348 SetDirection(tview.FlexRow).
349 AddItem(commandList, 4, 0, false).
350 AddItem(ui.chatBox, 0, 8, false).
351 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
352 AddItem(ui.userList, 25, 1, false)
353 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
354 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
355 if event.Key() == tcell.KeyEscape {
356 ui.Pages.AddPage("modal", modal, false, true)
357 }
358
359 // List files
360 if event.Key() == tcell.KeyCtrlF {
361 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
362 ui.HLClient.Logger.Errorw("err", "err", err)
363 }
364 }
365
366 // Show News
367 if event.Key() == tcell.KeyCtrlN {
368 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
369 ui.HLClient.Logger.Errorw("err", "err", err)
370 }
371 }
372
373 // Post news
374 if event.Key() == tcell.KeyCtrlP {
375
376 newsFlex := tview.NewFlex()
377 newsFlex.SetBorderPadding(0, 0, 1, 1)
378 newsPostTextArea := tview.NewTextView()
379 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
380 newsPostTextArea.SetChangedFunc(func() {
381 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
382 })
383
384 newsPostForm := tview.NewForm().
385 SetButtonsAlign(tview.AlignRight).
386 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
387 AddButton("Send", nil)
388 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
389 switch event.Key() {
390 case tcell.KeyEscape:
391 ui.Pages.RemovePage("newsInput")
392 case tcell.KeyTab:
393 ui.App.SetFocus(newsPostTextArea)
394 case tcell.KeyEnter:
395 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
396 if len(newsText) == 0 {
397 return event
398 }
399 err := ui.HLClient.Send(
400 *NewTransaction(tranOldPostNews, nil,
401 NewField(fieldData, []byte(newsText)),
402 ),
403 )
404 if err != nil {
405 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
406 // TODO: display errModal to user
407 }
408 ui.Pages.RemovePage("newsInput")
409 }
410
411 return event
412 })
413
414 newsFlex.
415 SetDirection(tview.FlexRow).
416 SetBorder(true).
417 SetTitle("| Post Message |")
418
419 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
420 switch event.Key() {
421 case tcell.KeyEscape:
422 ui.Pages.RemovePage("newsInput")
423 case tcell.KeyTab:
424 ui.App.SetFocus(newsPostForm)
425 case tcell.KeyEnter:
426 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
427 default:
428 const windowsBackspaceRune = 8
429 const macBackspaceRune = 127
430 switch event.Rune() {
431 case macBackspaceRune, windowsBackspaceRune:
432 curTxt := newsPostTextArea.GetText(true)
433 if len(curTxt) > 0 {
434 curTxt = curTxt[:len(curTxt)-1]
435 newsPostTextArea.SetText(curTxt)
436 }
437 default:
438 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
439 }
440 }
441
442 return event
443 })
444
445 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
446 newsFlex.AddItem(newsPostForm, 3, 0, false)
447
448 newsPostPage := tview.NewFlex().
449 AddItem(nil, 0, 1, false).
450 AddItem(tview.NewFlex().
451 SetDirection(tview.FlexRow).
452 AddItem(nil, 0, 1, false).
453 AddItem(newsFlex, 15, 1, true).
454 // AddItem(newsPostForm, 3, 0, false).
455 AddItem(nil, 0, 1, false), 40, 1, false).
456 AddItem(nil, 0, 1, false)
457
458 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
459 ui.App.SetFocus(newsPostTextArea)
460 }
461
462 return event
463 })
464 return serverUI
465 }
466
467 func (ui *UI) Start() {
468 home := tview.NewFlex().SetDirection(tview.FlexRow)
469 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
470 mainMenu := tview.NewList()
471
472 bannerItem := tview.NewTextView().
473 SetText(randomBanner()).
474 SetDynamicColors(true).
475 SetTextAlign(tview.AlignCenter)
476
477 home.AddItem(
478 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
479 14, 1, false)
480 home.AddItem(tview.NewFlex().
481 AddItem(nil, 0, 1, false).
482 AddItem(mainMenu, 0, 1, true).
483 AddItem(nil, 0, 1, false),
484 0, 1, true,
485 )
486
487 mainMenu.AddItem("Join Server", "", 'j', func() {
488 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
489 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
490 }).
491 AddItem("Bookmarks", "", 'b', func() {
492 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
493 }).
494 AddItem("Browse Tracker", "", 't', func() {
495 ui.trackerList = ui.getTrackerList()
496 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
497 }).
498 AddItem("Settings", "", 's', func() {
499 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
500 }).
501 AddItem("Quit", "", 'q', func() {
502 ui.App.Stop()
503 })
504
505 ui.Pages.AddPage("home", home, true, true)
506
507 // App level input capture
508 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
509 if event.Key() == tcell.KeyCtrlC {
510 ui.HLClient.Logger.Infow("Exiting")
511 ui.App.Stop()
512 os.Exit(0)
513 }
514 // Show Logs
515 if event.Key() == tcell.KeyCtrlL {
516 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
517 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
518 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
519 if key == tcell.KeyEscape {
520 ui.Pages.RemovePage("logs")
521 }
522 })
523
524 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
525 }
526 return event
527 })
528
529 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
530 ui.App.Stop()
531 os.Exit(1)
532 }
533 }