]> git.r.bdr.sh - rbdr/mobius/blob - hotline/ui.go
Merge pull request #49 from jhalter/fix_1.2.3_client_no_agreement_behavior
[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 t, _, err := ReadTransaction(buf)
212 if err != nil {
213 break
214 }
215 if err := ui.HLClient.HandleTransaction(t); err != nil {
216 ui.HLClient.Logger.Errorw("Error handling transaction", "err", err)
217 }
218 }
219
220 if scanner.Err() == nil {
221 loginErrModal := tview.NewModal().
222 AddButtons([]string{"Ok"}).
223 SetText("The server connection has closed.").
224 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
225 ui.Pages.SwitchToPage("home")
226 })
227 loginErrModal.Box.SetTitle("Server Connection Error")
228
229 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
230 ui.App.Draw()
231 return
232 }
233 ui.Pages.SwitchToPage("home")
234
235 }()
236
237 return nil
238 }
239
240 func (ui *UI) renderJoinServerForm(name, server, login, password, backPage string, save, defaultConnect bool) *tview.Flex {
241 joinServerForm := tview.NewForm()
242 joinServerForm.
243 // AddInputField("Name", server, 0, func(textToCheck string, lastChar rune) bool {
244 // return false
245 // }, nil).
246 AddInputField("Server", server, 0, nil, nil).
247 AddInputField("Login", login, 0, nil, nil).
248 AddPasswordField("Password", password, 0, '*', nil).
249 AddCheckbox("Save", save, func(checked bool) {
250 ui.HLClient.Logger.Infow("saving bookmark")
251 // TODO: Implement bookmark saving
252
253 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())
254 out, err := yaml.Marshal(ui.HLClient.pref)
255 if err != nil {
256 panic(err)
257 }
258
259 err = ioutil.WriteFile(ui.HLClient.cfgPath, out, 0666)
260 if err != nil {
261 panic(err)
262 }
263 // pref := ui.HLClient.pref
264 }).
265 AddButton("Cancel", func() {
266 ui.Pages.SwitchToPage(backPage)
267 }).
268 AddButton("Connect", func() {
269 srvAddr := joinServerForm.GetFormItem(0).(*tview.InputField).GetText()
270 loginInput := joinServerForm.GetFormItem(1).(*tview.InputField).GetText()
271 err := ui.joinServer(
272 srvAddr,
273 loginInput,
274 joinServerForm.GetFormItem(2).(*tview.InputField).GetText(),
275 )
276 if name == "" {
277 name = fmt.Sprintf("%s@%s", loginInput, srvAddr)
278 }
279 ui.HLClient.serverName = name
280
281 if err != nil {
282 ui.HLClient.Logger.Errorw("login error", "err", err)
283 loginErrModal := tview.NewModal().
284 AddButtons([]string{"Oh no"}).
285 SetText(err.Error()).
286 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
287 ui.Pages.SwitchToPage(backPage)
288 })
289
290 ui.Pages.AddPage("loginErr", loginErrModal, false, true)
291 }
292
293 // Save checkbox
294 if joinServerForm.GetFormItem(3).(*tview.Checkbox).IsChecked() {
295 // TODO: implement bookmark saving
296 }
297 })
298
299 joinServerForm.Box.SetBorder(true).SetTitle("| Connect |")
300 joinServerForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
301 if event.Key() == tcell.KeyEscape {
302 ui.Pages.SwitchToPage(backPage)
303 }
304 return event
305 })
306
307 if defaultConnect {
308 joinServerForm.SetFocus(5)
309 }
310
311 joinServerPage := tview.NewFlex().
312 AddItem(nil, 0, 1, false).
313 AddItem(tview.NewFlex().
314 SetDirection(tview.FlexRow).
315 AddItem(nil, 0, 1, false).
316 AddItem(joinServerForm, 14, 1, true).
317 AddItem(nil, 0, 1, false), 40, 1, true).
318 AddItem(nil, 0, 1, false)
319
320 return joinServerPage
321 }
322
323 func (ui *UI) renderServerUI() *tview.Flex {
324 ui.chatBox.SetText("") // clear any previously existing chatbox text
325 commandList := tview.NewTextView().SetDynamicColors(true)
326 commandList.
327 SetText("[yellow]^n[-::]: Read News [yellow]^p[-::]: Post News\n[yellow]^l[-::]: View Logs [yellow]^f[-::]: View Files\n").
328 SetBorder(true).
329 SetTitle("| Keyboard Shortcuts| ")
330
331 modal := tview.NewModal().
332 SetText("Disconnect from the server?").
333 AddButtons([]string{"Cancel", "Exit"}).
334 SetFocus(1)
335 modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
336 if buttonIndex == 1 {
337 _ = ui.HLClient.Disconnect()
338 ui.Pages.RemovePage(pageServerUI)
339 ui.Pages.SwitchToPage("home")
340 } else {
341 ui.Pages.HidePage("modal")
342 }
343 })
344
345 serverUI := tview.NewFlex().
346 AddItem(tview.NewFlex().
347 SetDirection(tview.FlexRow).
348 AddItem(commandList, 4, 0, false).
349 AddItem(ui.chatBox, 0, 8, false).
350 AddItem(ui.chatInput, 3, 0, true), 0, 1, true).
351 AddItem(ui.userList, 25, 1, false)
352 serverUI.SetBorder(true).SetTitle("| Mobius - Connected to " + ui.HLClient.serverName + " |").SetTitleAlign(tview.AlignLeft)
353 serverUI.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
354 if event.Key() == tcell.KeyEscape {
355 ui.Pages.AddPage("modal", modal, false, true)
356 }
357
358 // List files
359 if event.Key() == tcell.KeyCtrlF {
360 if err := ui.HLClient.Send(*NewTransaction(tranGetFileNameList, nil)); err != nil {
361 ui.HLClient.Logger.Errorw("err", "err", err)
362 }
363 }
364
365 // Show News
366 if event.Key() == tcell.KeyCtrlN {
367 if err := ui.HLClient.Send(*NewTransaction(tranGetMsgs, nil)); err != nil {
368 ui.HLClient.Logger.Errorw("err", "err", err)
369 }
370 }
371
372 // Post news
373 if event.Key() == tcell.KeyCtrlP {
374
375 newsFlex := tview.NewFlex()
376 newsFlex.SetBorderPadding(0, 0, 1, 1)
377 newsPostTextArea := tview.NewTextView()
378 newsPostTextArea.SetBackgroundColor(tcell.ColorDarkSlateGrey)
379 newsPostTextArea.SetChangedFunc(func() {
380 ui.App.Draw() // TODO: docs say this is bad but it's the only way to show content during initial render??
381 })
382
383 newsPostForm := tview.NewForm().
384 SetButtonsAlign(tview.AlignRight).
385 // AddButton("Cancel", nil). // TODO: implement cancel button behavior
386 AddButton("Send", nil)
387 newsPostForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
388 switch event.Key() {
389 case tcell.KeyEscape:
390 ui.Pages.RemovePage("newsInput")
391 case tcell.KeyTab:
392 ui.App.SetFocus(newsPostTextArea)
393 case tcell.KeyEnter:
394 newsText := strings.ReplaceAll(newsPostTextArea.GetText(true), "\n", "\r")
395 if len(newsText) == 0 {
396 return event
397 }
398 err := ui.HLClient.Send(
399 *NewTransaction(tranOldPostNews, nil,
400 NewField(fieldData, []byte(newsText)),
401 ),
402 )
403 if err != nil {
404 ui.HLClient.Logger.Errorw("Error posting news", "err", err)
405 // TODO: display errModal to user
406 }
407 ui.Pages.RemovePage("newsInput")
408 }
409
410 return event
411 })
412
413 newsFlex.
414 SetDirection(tview.FlexRow).
415 SetBorder(true).
416 SetTitle("| Post Message |")
417
418 newsPostTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
419 switch event.Key() {
420 case tcell.KeyEscape:
421 ui.Pages.RemovePage("newsInput")
422 case tcell.KeyTab:
423 ui.App.SetFocus(newsPostForm)
424 case tcell.KeyEnter:
425 _, _ = fmt.Fprintf(newsPostTextArea, "\n")
426 default:
427 const windowsBackspaceRune = 8
428 const macBackspaceRune = 127
429 switch event.Rune() {
430 case macBackspaceRune, windowsBackspaceRune:
431 curTxt := newsPostTextArea.GetText(true)
432 if len(curTxt) > 0 {
433 curTxt = curTxt[:len(curTxt)-1]
434 newsPostTextArea.SetText(curTxt)
435 }
436 default:
437 _, _ = fmt.Fprintf(newsPostTextArea, string(event.Rune()))
438 }
439 }
440
441 return event
442 })
443
444 newsFlex.AddItem(newsPostTextArea, 10, 0, true)
445 newsFlex.AddItem(newsPostForm, 3, 0, false)
446
447 newsPostPage := tview.NewFlex().
448 AddItem(nil, 0, 1, false).
449 AddItem(tview.NewFlex().
450 SetDirection(tview.FlexRow).
451 AddItem(nil, 0, 1, false).
452 AddItem(newsFlex, 15, 1, true).
453 // AddItem(newsPostForm, 3, 0, false).
454 AddItem(nil, 0, 1, false), 40, 1, false).
455 AddItem(nil, 0, 1, false)
456
457 ui.Pages.AddPage("newsInput", newsPostPage, true, true)
458 ui.App.SetFocus(newsPostTextArea)
459 }
460
461 return event
462 })
463 return serverUI
464 }
465
466 func (ui *UI) Start() {
467 home := tview.NewFlex().SetDirection(tview.FlexRow)
468 home.Box.SetBorder(true).SetTitle("| Mobius v" + VERSION + " |").SetTitleAlign(tview.AlignLeft)
469 mainMenu := tview.NewList()
470
471 bannerItem := tview.NewTextView().
472 SetText(randomBanner()).
473 SetDynamicColors(true).
474 SetTextAlign(tview.AlignCenter)
475
476 home.AddItem(
477 tview.NewFlex().AddItem(bannerItem, 0, 1, false),
478 14, 1, false)
479 home.AddItem(tview.NewFlex().
480 AddItem(nil, 0, 1, false).
481 AddItem(mainMenu, 0, 1, true).
482 AddItem(nil, 0, 1, false),
483 0, 1, true,
484 )
485
486 mainMenu.AddItem("Join Server", "", 'j', func() {
487 joinServerPage := ui.renderJoinServerForm("", "", GuestAccount, "", "home", false, false)
488 ui.Pages.AddPage("joinServer", joinServerPage, true, true)
489 }).
490 AddItem("Bookmarks", "", 'b', func() {
491 ui.Pages.AddAndSwitchToPage("bookmarks", ui.showBookmarks(), true)
492 }).
493 AddItem("Browse Tracker", "", 't', func() {
494 ui.trackerList = ui.getTrackerList()
495 ui.Pages.AddAndSwitchToPage("trackerList", ui.trackerList, true)
496 }).
497 AddItem("Settings", "", 's', func() {
498 ui.Pages.AddPage("settings", ui.renderSettingsForm(), true, true)
499 }).
500 AddItem("Quit", "", 'q', func() {
501 ui.App.Stop()
502 })
503
504 ui.Pages.AddPage("home", home, true, true)
505
506 // App level input capture
507 ui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
508 if event.Key() == tcell.KeyCtrlC {
509 ui.HLClient.Logger.Infow("Exiting")
510 ui.App.Stop()
511 os.Exit(0)
512 }
513 // Show Logs
514 if event.Key() == tcell.KeyCtrlL {
515 ui.HLClient.DebugBuf.TextView.ScrollToEnd()
516 ui.HLClient.DebugBuf.TextView.SetBorder(true).SetTitle("Logs")
517 ui.HLClient.DebugBuf.TextView.SetDoneFunc(func(key tcell.Key) {
518 if key == tcell.KeyEscape {
519 ui.Pages.RemovePage("logs")
520 }
521 })
522
523 ui.Pages.AddPage("logs", ui.HLClient.DebugBuf.TextView, true, true)
524 }
525 return event
526 })
527
528 if err := ui.App.SetRoot(ui.Pages, true).SetFocus(ui.Pages).Run(); err != nil {
529 ui.App.Stop()
530 os.Exit(1)
531 }
532 }