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